mirror of
https://github.com/ArcaneChat/android.git
synced 2026-07-03 14:05:24 +02:00
Compare commits
115 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f89ecea5c | |||
| 30a63dd13b | |||
| a5d2642c37 | |||
| 79e5eedab0 | |||
| 27da217eb7 | |||
| a0c71a5387 | |||
| 4d19a59cb6 | |||
| 819a9f415a | |||
| dff1ef3778 | |||
| 7131f5774e | |||
| aa572508dc | |||
| 1f880efb3f | |||
| 72ace5c156 | |||
| 2f34a6ffa4 | |||
| afdfe7bbaa | |||
| 14f43f2a79 | |||
| f3353cf6e9 | |||
| 722f2cad1e | |||
| b135093628 | |||
| be44789b01 | |||
| 233197095b | |||
| 494cb728c3 | |||
| 60e9a9101a | |||
| c23dac6f99 | |||
| 3965319ef4 | |||
| c4db7c515f | |||
| fbf88e102b | |||
| d5ea043893 | |||
| f70c79ba4a | |||
| 0b2a26e2f1 | |||
| b968ba67b3 | |||
| e234cc864b | |||
| b47e75eed6 | |||
| 6b684bf030 | |||
| ea89d37461 | |||
| 814af177d1 | |||
| 1ccbb7177f | |||
| 3118dbd32b | |||
| 1a5e062dac | |||
| 0bcaea6f01 | |||
| 8283a6c4ad | |||
| cd159bac6c | |||
| 21f917004f | |||
| 0abf2ff00c | |||
| 0ca92ec6b5 | |||
| e66fa53537 | |||
| a51013cc00 | |||
| 171500a97d | |||
| 9a4851961e | |||
| e5ddbacb2e | |||
| 03c7d78e2b | |||
| 00498b02b3 | |||
| ad6e2d1a9f | |||
| 5cfa71cd72 | |||
| 0551044ef9 | |||
| 5126b3eeb2 | |||
| 8d7078f96f | |||
| c446440537 | |||
| 333e4c9eca | |||
| fb1f5df234 | |||
| cc419a9f62 | |||
| 2469b6efef | |||
| b7446c9a93 | |||
| 0c9b5bbaf4 | |||
| 3126544f00 | |||
| ccdad0469d | |||
| 9f3eeb3d0d | |||
| 92bc53b672 | |||
| 1355303360 | |||
| 7227f513a5 | |||
| 4933d66000 | |||
| 977b21618c | |||
| 5cecd5bec3 | |||
| d8db39f9c7 | |||
| 736fc44870 | |||
| 537127ec4e | |||
| a5167f3739 | |||
| d481541ad8 | |||
| 7ab19fb8af | |||
| 715a2c0653 | |||
| 350cd0539c | |||
| 9f8fe08c75 | |||
| 5048239563 | |||
| 6e135e06a9 | |||
| d52647b288 | |||
| c0a729978c | |||
| 041c95c6d2 | |||
| 8de0fed18e | |||
| dd75a8e858 | |||
| 2e77db23d5 | |||
| 04a4a02235 | |||
| 2910730bc1 | |||
| 23458a6c64 | |||
| 9679b22072 | |||
| 3fae301ec2 | |||
| d08069f7b0 | |||
| 56a0607a5e | |||
| 9c08617f7d | |||
| 812dc8d738 | |||
| d8b278a1f5 | |||
| 4368a4f63e | |||
| 72ae90ae93 | |||
| c36ce7a8ab | |||
| 404097fe30 | |||
| 07d5c719d4 | |||
| 13a43473cc | |||
| 5a84198829 | |||
| 1f443cd87c | |||
| 4901b92ee2 | |||
| cd48e15071 | |||
| 2d4b570f48 | |||
| 3f0b36bd4b | |||
| b342fb9c30 | |||
| 0974655366 | |||
| 9675c419ac |
@@ -35,7 +35,7 @@ jobs:
|
||||
- uses: nttld/setup-ndk@v1
|
||||
id: setup-ndk
|
||||
with:
|
||||
ndk-version: r23c
|
||||
ndk-version: r27
|
||||
|
||||
- name: Compile core
|
||||
env:
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
- uses: nttld/setup-ndk@v1
|
||||
id: setup-ndk
|
||||
with:
|
||||
ndk-version: r23c
|
||||
ndk-version: r27
|
||||
|
||||
- name: Compile core
|
||||
env:
|
||||
|
||||
+39
-3
@@ -1,6 +1,34 @@
|
||||
# ArcaneChat Android Changelog
|
||||
|
||||
## v1.48.1
|
||||
## v1.49.0
|
||||
2024-11
|
||||
|
||||
* New emoji picker with support for more emojis
|
||||
* Webxdc apps can now trigger notifications
|
||||
* Webxdc apps can now deep-link to internal sections when you click their info-messages in chat
|
||||
* Add "Show in Chat" to the menu of opened Webxdc apps
|
||||
* Reverse order of messages in the notification group
|
||||
* Notify reactions to own messages
|
||||
* Improve the button to start Webxdc apps
|
||||
* Make account deletion confirmation dialog faster
|
||||
* Rename "Back up Chats to External Storage" to "Export Backup"
|
||||
* Improve compatibility with classic email clients in the outgoing messages
|
||||
* Removed internal font scaling setting in favor of the better system settings
|
||||
* Use privacy-preserving webxdc addresses
|
||||
* Use Rustls for connections with strict TLS
|
||||
* fix: Trim whitespace from scanned QR codes.
|
||||
* fix quotes: Line-before-quote may be up to 120 character long instead of 80
|
||||
* fix: Prevent accidental wrong-password-notifications
|
||||
* fix: Remove footers from "Show Full Message..."
|
||||
* fix: Only add "member added/removed" messages if they actually do that
|
||||
* fix: Update state of message when fully downloading it
|
||||
* fix: send message: Do not fail if the message does not exist anymore
|
||||
* fix: Do not percent-encode dot when passing to autoconfig server (so, fix handling of some servers)
|
||||
* fix displaynames not being updated when intially scanned by a QR code
|
||||
* several bug fixes
|
||||
* update to core 1.151.2
|
||||
|
||||
## v1.48.3
|
||||
2024-10
|
||||
|
||||
* new Proxy settings screen available at "Advanced / Proxy"
|
||||
@@ -10,6 +38,8 @@
|
||||
* scan Proxies' QR code and use them
|
||||
* make Proxy URLs inside Delta Chat tappable
|
||||
* open Delta Chat when tapping Proxy URLs in other apps
|
||||
* support for realtime webxdc apps moved out of experimental and enabled by default
|
||||
* realtime webxdc apps can be disabled at "Settings / Advanced"
|
||||
* "New Contact / Link" button to view, share or copy the invite line
|
||||
* "New Contact / Scan" button to easier access the scanner functionality
|
||||
* open "New Contact" scan/show activities directly, do not try to be too smart and open the last active tab
|
||||
@@ -44,8 +74,10 @@
|
||||
* assign messages to ad-hoc group with matching name and members
|
||||
* use stricter TLS checks for HTTPS downloads (images in HTML mails, Autoconfig)
|
||||
* improve logging for failed QR code scans, AEAP, Autocrypt, notification permissions and sending errors
|
||||
* improve logging of multi account setup (log account ID)
|
||||
* show more context for the "Cannot establish guaranteed..." info message
|
||||
* show original file name in "Message Info"
|
||||
* show file name in "Message Info"
|
||||
* show root SMTP connection failure in connectivity view
|
||||
* fix: Sort received outgoing message down if it's fresher than all non fresh messages
|
||||
* fix: avoid app being killed when processing a PUSH notification
|
||||
* fix crash when refreshing avatar
|
||||
@@ -65,8 +97,12 @@
|
||||
* fix: normalize proxy URLs
|
||||
* fix connections getting stuck in "Updating..." sometimes
|
||||
* fix scanning "add second device" QR code from scanner above chatlist
|
||||
* fix warning about wrong password
|
||||
* fix app getting stale when receiving a PUSH notifications takes longer
|
||||
* fix app getting stale on network changes
|
||||
* fix: skip IDLE if we got unsolicited FETCH
|
||||
* update translations and local help
|
||||
* update to core 1.148.4
|
||||
* update to core 1.148.6
|
||||
|
||||
|
||||
## v1.46.14
|
||||
|
||||
+3
-3
@@ -34,10 +34,10 @@ ENV PATH ${PATH}:${ANDROID_SDK_ROOT}/cmdline-tools/bin
|
||||
# Install NDK manually. Other SDK parts are installed automatically by gradle.
|
||||
#
|
||||
# If you change the NDK version here, also change it in `flake.nix`.
|
||||
# NDK version r23c LTS aka 23.2.8568313
|
||||
RUN sdkmanager --sdk_root=${ANDROID_SDK_ROOT} 'ndk;23.2.8568313'
|
||||
# NDK version r27 LTS aka 27.0.11902837
|
||||
RUN sdkmanager --sdk_root=${ANDROID_SDK_ROOT} 'ndk;27.0.11902837'
|
||||
|
||||
ENV ANDROID_NDK_ROOT ${ANDROID_SDK_ROOT}/ndk/23.2.8568313
|
||||
ENV ANDROID_NDK_ROOT ${ANDROID_SDK_ROOT}/ndk/27.0.11902837
|
||||
ENV PATH ${PATH}:${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/
|
||||
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain none
|
||||
|
||||
+13
-8
@@ -48,7 +48,8 @@ the following steps are done in a PR called `prep-VERSION` (no leading "v"):
|
||||
6. build APKs:
|
||||
a) generate debug APK at "Build / Build Bundle(s)/APK / Build APK(s)"
|
||||
b) generate release APK at "Build / Generate Signed Bundle or APK",
|
||||
select "APK", add keys, flavor `gplayRelease`
|
||||
select "APK", add keys, flavor `gplayRelease`.
|
||||
this APK will go to the stores and is located at `gplay/release`
|
||||
|
||||
|
||||
## Push Test Releases
|
||||
@@ -79,16 +80,21 @@ only afterwards, push the APK to stores. **consider a blog post.**
|
||||
|
||||
on <https://play.google.com/apps/publish/>:
|
||||
|
||||
9. a) open "Delta Chat/Release/Production"
|
||||
then "Create new release" and upload APK from above
|
||||
9. a) open "Delta Chat / Test and release / Production"
|
||||
then "Create new release" and upload APK from above
|
||||
b) fill out "Release details/Release notes" (500 chars), add the line
|
||||
"These features will roll out over the coming days. Thanks for using Delta Chat!";
|
||||
release name should be default ("123 (1.2.3)")
|
||||
c) click "Next", set "Rollout Percentage" to 1% (later 2%, 5%, 10%, 20%, 50%, 100%),
|
||||
click "Save"
|
||||
c) click "Next", set "Rollout Percentage" to 50%, click "Save"
|
||||
d) Go to "Publishing Overview", "Managed publishing" is usually off;
|
||||
click "Send change for review", confirm
|
||||
|
||||
2 days later, change "Rollout Percentage" to 99%. Two more days later to 100%.
|
||||
Rollout is anyways slower in practise, however,
|
||||
only as long as we do not enter 100%, we can retract the version
|
||||
(Once we reach 100%, we have to submit a new version for approval.
|
||||
During these up to 4 days, sometimes longer, we cannot do anything on existing rollout)
|
||||
|
||||
|
||||
## Tag for F-Droid and create Github release
|
||||
|
||||
@@ -111,9 +117,8 @@ on <https://developer.amazon.com/dashboard>:
|
||||
12. a) for "Delta Chat", select "Add upcoming version" on the left
|
||||
b) at "Step 1 / Existing file(s)" hit "Replace", upload the APK from above
|
||||
c) on the "Step 1" page, add "Release notes" from CHANGELOG.md, hit "Next"
|
||||
d) on "Step 2" page: "Does your app collect or transfer user data to third parties?" -> No, then "Next"
|
||||
e) on "Step 3" page: "Next"
|
||||
f) on "Step 4" page: "Submit app"
|
||||
d) on "Step 2" and "Step 3" pages, hit "Next"
|
||||
e) on "Step 4" page: "Submit app"
|
||||
|
||||
|
||||
## Release on Huawei AppGallery
|
||||
|
||||
+4
-4
@@ -29,12 +29,12 @@ android {
|
||||
// > Task :stripFatDebugDebugSymbols
|
||||
// Unable to strip the following libraries, packaging them as they are: libanimation-decoder-gif.so, libnative-utils.so.
|
||||
// See <https://issuetracker.google.com/issues/237187538> for details.
|
||||
ndkVersion "23.2.8568313"
|
||||
ndkVersion "27.0.12077973"
|
||||
useLibrary 'org.apache.http.legacy'
|
||||
|
||||
defaultConfig {
|
||||
versionCode 30000693
|
||||
versionName "1.48.1"
|
||||
versionCode 30000698
|
||||
versionName "1.49.0"
|
||||
|
||||
applicationId "chat.delta.lite"
|
||||
multiDexEnabled true
|
||||
@@ -206,7 +206,7 @@ dependencies {
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel:2.6.2'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
|
||||
implementation 'androidx.work:work-runtime:2.9.1'
|
||||
implementation 'androidx.emoji2:emoji2-emojipicker:1.4.0'
|
||||
implementation 'androidx.emoji2:emoji2-emojipicker:1.5.0'
|
||||
implementation 'com.google.guava:guava:31.1-android'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1' // plays video and audio
|
||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.1'
|
||||
|
||||
Generated
+21
-55
@@ -7,11 +7,11 @@
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710633978,
|
||||
"narHash": "sha256-yemnwSvW7cdWtXGpivFA5jDO35rGPs6fqxlQ4l6ODXs=",
|
||||
"lastModified": 1720988215,
|
||||
"narHash": "sha256-nQ0Zx0vAWJo0IOGNFjCOdIkDSgOpMa//GalR8tbTl3A=",
|
||||
"owner": "tadfisher",
|
||||
"repo": "android-nixpkgs",
|
||||
"rev": "e91fb3d8517538e5ad9b422c9a4f604b56008a9e",
|
||||
"rev": "5a052c62cdb51b210bc0717177d5bd014cba3df1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -29,11 +29,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1708939976,
|
||||
"narHash": "sha256-O5+nFozxz2Vubpdl1YZtPrilcIXPcRAjqNdNE8oCRoA=",
|
||||
"lastModified": 1717408969,
|
||||
"narHash": "sha256-Q0OEFqe35fZbbRPPRdrjTUUChKVhhWXz3T9ZSKmaoVY=",
|
||||
"owner": "numtide",
|
||||
"repo": "devshell",
|
||||
"rev": "5ddecd67edbd568ebe0a55905273e56cc82aabe3",
|
||||
"rev": "1ebbe68d57457c8cae98145410b164b5477761f4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -65,11 +65,11 @@
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709126324,
|
||||
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -96,31 +96,13 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_4": {
|
||||
"inputs": {
|
||||
"systems": "systems_4"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1705309234,
|
||||
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1709237383,
|
||||
"narHash": "sha256-cy6ArO4k5qTx+l5o+0mL9f5fa86tYUX3ozE1S+Txlds=",
|
||||
"lastModified": 1720768451,
|
||||
"narHash": "sha256-EYekUHJE2gxeo2pM/zM9Wlqw1Uw2XTJXOSAO79ksc4Y=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "1536926ef5621b09bba54035ae2bb6d806d72ac8",
|
||||
"rev": "7e7c39ea35c5cdd002cd4588b03a3fb9ece6fad9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -132,11 +114,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1710889954,
|
||||
"narHash": "sha256-Pr6F5Pmd7JnNEMHHmspZ0qVqIBVxyZ13ik1pJtm2QXk=",
|
||||
"lastModified": 1723221148,
|
||||
"narHash": "sha256-7pjpeQlZUNQ4eeVntytU3jkw9dFK3k1Htgk2iuXjaD8=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "7872526e9c5332274ea5932a0c3270d6e4724f3b",
|
||||
"rev": "154bcb95ad51bc257c2ce4043a725de6ca700ef6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -148,11 +130,11 @@
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1706487304,
|
||||
"narHash": "sha256-LE8lVX28MV2jWJsidW13D2qrHU/RUUONendL2Q/WlJg=",
|
||||
"lastModified": 1718428119,
|
||||
"narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "90f456026d284c22b3e3497be980b2e47d0b28ac",
|
||||
"rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -172,15 +154,14 @@
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_4",
|
||||
"nixpkgs": "nixpkgs_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1711073443,
|
||||
"narHash": "sha256-PpNb4xq7U5Q/DdX40qe7CijUsqhVVM3VZrhN0+c6Lcw=",
|
||||
"lastModified": 1723429325,
|
||||
"narHash": "sha256-4x/32xTCd+xCwFoI/kKSiCr5LQA2ZlyTRYXKEni5HR8=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "eec55ba9fcde6be4c63942827247e42afef7fafe",
|
||||
"rev": "65e3dc0fe079fe8df087cd38f1fe6836a0373aad",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -233,21 +214,6 @@
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_4": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
ndk-bundle
|
||||
platform-tools
|
||||
platforms-android-34
|
||||
ndk-23-2-8568313
|
||||
ndk-27-0-11902837
|
||||
]);
|
||||
rust-version = pkgs.lib.removeSuffix "\n"
|
||||
(builtins.readFile ./scripts/rust-toolchain);
|
||||
@@ -28,7 +28,7 @@
|
||||
devShells.default = pkgs.mkShell {
|
||||
ANDROID_SDK_ROOT = "${android-sdk}/share/android-sdk";
|
||||
ANDROID_NDK_ROOT =
|
||||
"${android-sdk}/share/android-sdk/ndk/23.2.8568313";
|
||||
"${android-sdk}/share/android-sdk/ndk/27.0.11902837";
|
||||
buildInputs = [
|
||||
android-sdk
|
||||
pkgs.openjdk17
|
||||
|
||||
+11
-55
@@ -7,57 +7,6 @@
|
||||
#include "deltachat-core-rust/deltachat-ffi/deltachat.h"
|
||||
|
||||
|
||||
#if __ANDROID_API__ == 16
|
||||
unsigned long getauxval(unsigned long type) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
int sendmmsg(int sockfd, struct mmsghdr *msgvec, unsigned int vlen,
|
||||
int flags)
|
||||
{
|
||||
if (flags != 0) {
|
||||
// Not supported by the fallback.
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (vlen == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ssize_t n = sendmsg(sockfd, &msgvec->msg_hdr, flags);
|
||||
if (n == -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
(*msgvec).msg_len = n;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int recvmmsg(int sockfd, struct mmsghdr *msgvec, unsigned int vlen,
|
||||
int flags, struct timespec *timeout)
|
||||
{
|
||||
if (flags != 0) {
|
||||
// Not supported by the fallback.
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (vlen == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int n = recvmsg(sockfd, &msgvec->msg_hdr, flags);
|
||||
if (n == -1) {
|
||||
return -1;
|
||||
}
|
||||
(*msgvec).msg_len = n;
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
static dc_msg_t* get_dc_msg(JNIEnv *env, jobject obj);
|
||||
|
||||
|
||||
@@ -780,12 +729,10 @@ JNIEXPORT jint Java_com_b44t_messenger_DcContext_sendVideochatInvitation(JNIEnv
|
||||
}
|
||||
|
||||
|
||||
JNIEXPORT jboolean Java_com_b44t_messenger_DcContext_sendWebxdcStatusUpdate(JNIEnv *env, jobject obj, jint msg_id, jstring payload, jstring descr)
|
||||
JNIEXPORT jboolean Java_com_b44t_messenger_DcContext_sendWebxdcStatusUpdate(JNIEnv *env, jobject obj, jint msg_id, jstring payload)
|
||||
{
|
||||
CHAR_REF(payload);
|
||||
CHAR_REF(descr);
|
||||
jboolean ret = dc_send_webxdc_status_update(get_dc_context(env, obj), msg_id, payloadPtr, descrPtr) != 0;
|
||||
CHAR_UNREF(descr);
|
||||
jboolean ret = dc_send_webxdc_status_update(get_dc_context(env, obj), msg_id, payloadPtr, NULL) != 0;
|
||||
CHAR_UNREF(payload);
|
||||
return ret;
|
||||
}
|
||||
@@ -1626,6 +1573,15 @@ JNIEXPORT jstring Java_com_b44t_messenger_DcMsg_getWebxdcInfoJson(JNIEnv *env, j
|
||||
}
|
||||
|
||||
|
||||
JNIEXPORT jstring Java_com_b44t_messenger_DcMsg_getWebxdcHref(JNIEnv *env, jobject obj)
|
||||
{
|
||||
char* temp = dc_msg_get_webxdc_href(get_dc_msg(env, obj));
|
||||
jstring ret = JSTRING_NEW(temp);
|
||||
dc_str_unref(temp);
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
JNIEXPORT jboolean Java_com_b44t_messenger_DcMsg_isForwarded(JNIEnv *env, jobject obj)
|
||||
{
|
||||
return dc_msg_is_forwarded(get_dc_msg(env, obj))!=0;
|
||||
|
||||
+1
-1
Submodule jni/deltachat-core-rust updated: f66fbc2e29...405cd37550
@@ -1 +1 @@
|
||||
../deltachat-pages/tools/create-local-help.py ../deltachat-pages/_site src/main/assets/help
|
||||
../deltachat-pages/tools/create-local-help.py ../deltachat-pages/result src/main/assets/help
|
||||
|
||||
+4
-4
@@ -77,9 +77,9 @@ if test -z "$NDK_HOST_TAG"; then
|
||||
fi
|
||||
|
||||
TOOLCHAIN="$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/$NDK_HOST_TAG"
|
||||
export CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="$TOOLCHAIN/bin/armv7a-linux-androideabi16-clang"
|
||||
export CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="$TOOLCHAIN/bin/armv7a-linux-androideabi21-clang"
|
||||
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$TOOLCHAIN/bin/aarch64-linux-android21-clang"
|
||||
export CARGO_TARGET_I686_LINUX_ANDROID_LINKER="$TOOLCHAIN/bin/i686-linux-android16-clang"
|
||||
export CARGO_TARGET_I686_LINUX_ANDROID_LINKER="$TOOLCHAIN/bin/i686-linux-android21-clang"
|
||||
export CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER="$TOOLCHAIN/bin/x86_64-linux-android21-clang"
|
||||
|
||||
export RUSTUP_TOOLCHAIN=$(cat "$(dirname "$0")/rust-toolchain")
|
||||
@@ -125,7 +125,7 @@ unset CPATH
|
||||
|
||||
if test -z $1 || test $1 = armeabi-v7a; then
|
||||
echo "-- cross compiling to armv7-linux-androideabi (arm) --"
|
||||
TARGET_CC="$TOOLCHAIN/bin/armv7a-linux-androideabi16-clang" \
|
||||
TARGET_CC="$TOOLCHAIN/bin/armv7a-linux-androideabi21-clang" \
|
||||
TARGET_AR="$TOOLCHAIN/bin/llvm-ar" \
|
||||
TARGET_RANLIB="$TOOLCHAIN/bin/llvm-ranlib" \
|
||||
cargo build $RELEASEFLAG --target armv7-linux-androideabi -p deltachat_ffi --features jsonrpc
|
||||
@@ -143,7 +143,7 @@ fi
|
||||
|
||||
if test -z $1 || test $1 = x86; then
|
||||
echo "-- cross compiling to i686-linux-android (x86) --"
|
||||
TARGET_CC="$TOOLCHAIN/bin/i686-linux-android16-clang" \
|
||||
TARGET_CC="$TOOLCHAIN/bin/i686-linux-android21-clang" \
|
||||
TARGET_AR="$TOOLCHAIN/bin/llvm-ar" \
|
||||
TARGET_RANLIB="$TOOLCHAIN/bin/llvm-ranlib" \
|
||||
cargo build $RELEASEFLAG --target i686-linux-android -p deltachat_ffi --features jsonrpc
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
VERSION=$1
|
||||
|
||||
|
||||
@@ -28,11 +28,6 @@ public class FcmReceiveService extends FirebaseMessagingService {
|
||||
private static volatile String prefixedToken;
|
||||
|
||||
public static void register(Context context) {
|
||||
if (Build.VERSION.SDK_INT < 19) {
|
||||
Log.w(TAG, "FCM not available on SDK < 19");
|
||||
triedRegistering = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (FcmReceiveService.prefixedToken != null) {
|
||||
Log.i(TAG, "FCM already registered");
|
||||
|
||||
@@ -66,6 +66,10 @@
|
||||
android:resource="@xml/automotive_app_desc" />
|
||||
-->
|
||||
|
||||
<!-- Opt out of metrics collection: https://developer.android.com/develop/ui/views/layout/webapps/managing-webview#metrics -->
|
||||
<meta-data android:name="android.webkit.WebView.MetricsOptOut"
|
||||
android:value="true" />
|
||||
|
||||
<activity android:name=".ShareActivity"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:excludeFromRecents="true"
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
<li><a href="#lze-delta-chat-používat-s-protonmail--tutanota--criptext">Lze Delta Chat používat s Protonmail / Tutanota / Criptext?</a></li>
|
||||
<li><a href="#remove-account">How can I delete my account?</a></li>
|
||||
<li><a href="#mám-zájem-o-technické-podrobnosti-kde-najdu-víc">Mám zájem o technické podrobnosti. Kde najdu víc?</a></li>
|
||||
<li><a href="#where-can-my-friends-find-delta-chat">Where can my friends find Delta Chat?</a></li>
|
||||
<li><a href="#jak-je-financován-vývoj-delta-chatu">Jak je financován vývoj Delta Chatu?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -297,8 +298,7 @@ Archived chats remain accessible above the chat list or via search.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>To archive or pin a chat, long tap (Android), use the chat’s menu (Android/Desktop) or swipe to the left (iOS);
|
||||
to mute a chat, use the chat’s menu (Android/Desktop) or the chat’s profile (iOS).</p>
|
||||
<p>To use the functions, long tap or right click a chat in the chat list.</p>
|
||||
|
||||
<h3 id="what-does-the-green-dot-mean">
|
||||
|
||||
@@ -1309,6 +1309,12 @@ One device is not needed for the other to work.</p>
|
||||
and make sure, <strong>Private Network</strong> is selected as “Network profile type”
|
||||
(after transfer, you can change back to the original value)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On <strong>iOS</strong>, make sure “System Settings / Apps / Delta Chat / <strong>Local Network</strong>” access is granted</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On <strong>macOS</strong>, enable “System Settings / Privacy & Security / <strong>Local Network</strong> / Delta Chat”</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Your system might have a “personal firewall”,
|
||||
which is known to cause problems (especially on Windows).
|
||||
@@ -1425,6 +1431,7 @@ you only open attachments from senders you trust, and not from spammers.</li>
|
||||
<ul>
|
||||
<li>In general, anyone can share webxdc apps with each
|
||||
other without restrictions.</li>
|
||||
<li>From <a href="https://webxdc.org/apps/">webxdc.org/apps</a>.</li>
|
||||
<li>You can <a href="https://delta.chat/en/2023-08-11-xstore">send ‘hi’ to xstore@testrun.org</a>
|
||||
to see an experimental webxdc appstore.
|
||||
All of the apps are open source and for free.</li>
|
||||
@@ -1874,6 +1881,35 @@ Otherwise you might receive undecryptable messages from those group chats.</p>
|
||||
<li>Dobrý začátek je <a href="https://github.com/deltachat/deltachat-core-rust/blob/master/standards.md#standards-used-in-delta-chat">Standards used in Delta Chat</a>.</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="where-can-my-friends-find-delta-chat">
|
||||
|
||||
|
||||
Where can my friends find Delta Chat? <a href="#where-can-my-friends-find-delta-chat" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Delta Chat is available for all major and some minor platforms:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>The <strong>official website</strong>, <a href="https://delta.chat/download">https://delta.chat/download</a> shows all options in detail</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If unavailable, use the <strong>mirror</strong> at <a href="https://deltachat.github.io/deltachat-pages">https://deltachat.github.io/deltachat-pages</a></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Open one of the following <strong>app stores and search for “Delta Chat”:</strong>
|
||||
Google Play Store, F-Droid, Huawei App Gallery, Amazon App Store, iOS and macOS App Store, Microsoft Store</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Check the <strong>package manager</strong> of your Linux distributions</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Android APKs</strong> are also available on <a href="https://github.com/deltachat/deltachat-android/releases">https://github.com/deltachat/deltachat-android/releases</a></p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="jak-je-financován-vývoj-delta-chatu">
|
||||
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
<li><a href="#ist-delta-chat-kompatibel-mit-protonmail--tutanota--criptext">Ist Delta Chat kompatibel mit Protonmail / Tutanota / Criptext?</a></li>
|
||||
<li><a href="#remove-account">Wie kann ich mein Konto löschen?</a></li>
|
||||
<li><a href="#ich-bin-an-technischen-details-interessiert-gibt-es-hierzu-weitere-infos">Ich bin an technischen Details interessiert. Gibt es hierzu weitere Infos?</a></li>
|
||||
<li><a href="#wo-können-meine-freunde-delta-chat-finden">Wo können meine Freunde Delta Chat finden?</a></li>
|
||||
<li><a href="#wie-wird-delta-chat-finanziert">Wie wird Delta Chat finanziert?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -286,7 +287,7 @@ unter dem Text Ihrer Nachrichten.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Zum Archivieren oder Anheften, tippen Sie lange auf den Chat (Android), verwenden Sie das Chatmenü (Android/Desktop) oder wischen Sie nach links (iOS); zum Stummschalten, verwenden Sie das Chatmenü (Android/Desktop) oder das Chatprofil (iOS).</p>
|
||||
<p>Um die Funktionen zu nutzen, lang auf einen Chat in der Chatliste tippen oder den Chat mit der rechten Maustaste anklicken.</p>
|
||||
|
||||
<h3 id="was-bedeutet-der-grüne-punkt">
|
||||
|
||||
@@ -1215,6 +1216,12 @@ Es wurden keine kritischen Probleme gefunden, aber zwei Probleme mit hohem Schwe
|
||||
und sicherstellen, dass <strong>Privates Netzwerk</strong> als “Netzwerkprofiltyp” ausgewählt ist.
|
||||
(nach der Übertragung kann wieder der ursprüngliche Wert verwendet werden)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Auf <strong>iOS</strong>, sicherstellen, dass „Systemeinstellungen / Apps / Delta Chat / <strong>Lokales Netzwerk</strong>“ eingeschaltet ist</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Auf <strong>macOS,</strong> „Systemeinstellungen / Datenschutz & Sicherheit / <strong>Lokales Netzwerk</strong> / Delta Chat“ aktivieren</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Ihr System verfügt möglicherweise über eine “Personal Firewall”, diese sind dafür bekannt, Probleme zu verursachen (insbesondere bei Windows). <strong>Deaktivieren Sie die Personal Firewall</strong> für Delta Chat auf beiden Seiten und versuchen Sie es erneut</p>
|
||||
</li>
|
||||
@@ -1305,6 +1312,7 @@ Verwenden Sie nach Möglichkeit ein Nicht-Gast-Netzwerk. Wenn Sie Zugriff auf de
|
||||
|
||||
<ul>
|
||||
<li>Grundsätzlich kann jeder Webxdc Apps mit anderen ohne Einschränkungen teilen.</li>
|
||||
<li>Von <a href="https://webxdc.org/apps/">webxdc.org/apps</a>.</li>
|
||||
<li>Sie können <a href="https://delta.chat/en/2023-08-11-xstore">‘hallo’ an xstore@testrun.org senden</a>
|
||||
um einen experimentellen Webxdc Appstore zu sehen.
|
||||
Alle Apps dort sind Open Source und umsonst.</li>
|
||||
@@ -1680,6 +1688,34 @@ Andernfalls könnten Sie unverschlüsselte Nachrichten aus diesen Gruppenchats e
|
||||
<li>Siehe hierzu <a href="https://github.com/deltachat/deltachat-core-rust/blob/master/standards.md#standards-used-in-delta-chat">in Delta Chat genutzte Standards</a>.</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="wo-können-meine-freunde-delta-chat-finden">
|
||||
|
||||
|
||||
Wo können meine Freunde Delta Chat finden? <a href="#wo-können-meine-freunde-delta-chat-finden" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Delta Chat ist für alle großen und einige kleinere Plattformen verfügbar:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Die <strong>offizielle Website</strong>, <a href="https://delta.chat/download">https://delta.chat/download</a> zeigt alle Optionen im Detail</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Falls nicht verfügbar, verwenden Sie den <strong>Mirror</strong> auf <a href="https://deltachat.github.io/deltachat-pages">https://deltachat.github.io/deltachat-pages</a></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Öffnen Sie einen der folgenden <strong>App-Stores und suchen Sie nach „Delta Chat“:</strong> Google Play Store, F-Droid, Huawei App Gallery, Amazon App Store, iOS und macOS App Store, Microsoft Store</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Im <strong>Paketmanager</strong> Ihrer Linux-Distribution nachschauen</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Android APKs</strong> sind auch auf <a href="https://github.com/deltachat/deltachat-android/releases">https://github.com/deltachat/deltachat-android/releases</a> verfügbar.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="wie-wird-delta-chat-finanziert">
|
||||
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
<li><a href="#is-delta-chat-compatible-with-protonmail--tutanota--criptext">Is Delta Chat compatible with Protonmail / Tutanota / Criptext?</a></li>
|
||||
<li><a href="#remove-account">How can I delete my account?</a></li>
|
||||
<li><a href="#im-interested-in-the-technical-details-can-you-tell-me-more">I’m interested in the technical details. Can you tell me more?</a></li>
|
||||
<li><a href="#where-can-my-friends-find-delta-chat">Where can my friends find Delta Chat?</a></li>
|
||||
<li><a href="#how-are-delta-chat-developments-funded">How are Delta Chat developments funded?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -301,8 +302,7 @@ Archived chats remain accessible above the chat list or via search.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>To archive or pin a chat, long tap (Android), use the chat’s menu (Android/Desktop) or swipe to the left (iOS);
|
||||
to mute a chat, use the chat’s menu (Android/Desktop) or the chat’s profile (iOS).</p>
|
||||
<p>To use the functions, long tap or right click a chat in the chat list.</p>
|
||||
|
||||
<h3 id="what-does-the-green-dot-mean">
|
||||
|
||||
@@ -1315,6 +1315,12 @@ One device is not needed for the other to work.</p>
|
||||
and make sure, <strong>Private Network</strong> is selected as “Network profile type”
|
||||
(after transfer, you can change back to the original value)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On <strong>iOS</strong>, make sure “System Settings / Apps / Delta Chat / <strong>Local Network</strong>” access is granted</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On <strong>macOS</strong>, enable “System Settings / Privacy & Security / <strong>Local Network</strong> / Delta Chat”</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Your system might have a “personal firewall”,
|
||||
which is known to cause problems (especially on Windows).
|
||||
@@ -1432,6 +1438,7 @@ you only open attachments from senders you trust, and not from spammers.</li>
|
||||
<ul>
|
||||
<li>In general, anyone can share webxdc apps with each
|
||||
other without restrictions.</li>
|
||||
<li>From <a href="https://webxdc.org/apps/">webxdc.org/apps</a>.</li>
|
||||
<li>You can <a href="https://delta.chat/en/2023-08-11-xstore">send ‘hi’ to xstore@testrun.org</a>
|
||||
to see an experimental webxdc appstore.
|
||||
All of the apps are open source and for free.</li>
|
||||
@@ -1883,6 +1890,35 @@ Otherwise you might receive undecryptable messages from those group chats.</p>
|
||||
<li>See <a href="https://github.com/deltachat/deltachat-core-rust/blob/master/standards.md#standards-used-in-delta-chat">Standards used in Delta Chat</a>.</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="where-can-my-friends-find-delta-chat">
|
||||
|
||||
|
||||
Where can my friends find Delta Chat? <a href="#where-can-my-friends-find-delta-chat" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Delta Chat is available for all major and some minor platforms:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>The <strong>official website</strong>, <a href="https://delta.chat/download">https://delta.chat/download</a> shows all options in detail</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If unavailable, use the <strong>mirror</strong> at <a href="https://deltachat.github.io/deltachat-pages">https://deltachat.github.io/deltachat-pages</a></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Open one of the following <strong>app stores and search for “Delta Chat”:</strong>
|
||||
Google Play Store, F-Droid, Huawei App Gallery, Amazon App Store, iOS and macOS App Store, Microsoft Store</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Check the <strong>package manager</strong> of your Linux distributions</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Android APKs</strong> are also available on <a href="https://github.com/deltachat/deltachat-android/releases">https://github.com/deltachat/deltachat-android/releases</a></p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="how-are-delta-chat-developments-funded">
|
||||
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
<li><a href="#delta-chat-es-compatible-con-protonmail--tutanota--criptext">¿Delta Chat es compatible con Protonmail / Tutanota / Criptext?</a></li>
|
||||
<li><a href="#remove-account">¿Cómo puedo eliminar mi cuenta?</a></li>
|
||||
<li><a href="#estoy-interesado-en-los-detalles-técnicos-pueden-decirme-más">Estoy interesado en los detalles técnicos. ¿Pueden decirme más?</a></li>
|
||||
<li><a href="#where-can-my-friends-find-delta-chat">Where can my friends find Delta Chat?</a></li>
|
||||
<li><a href="#cómo-se-financia-el-desarrollo-de-delta-chat">¿Cómo se financia el desarrollo de Delta Chat?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -1277,6 +1278,12 @@ No es necesario un dispositivo para que el otro funcione.</p>
|
||||
and make sure, <strong>Private Network</strong> is selected as “Network profile type”
|
||||
(after transfer, you can change back to the original value)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On <strong>iOS</strong>, make sure “System Settings / Apps / Delta Chat / <strong>Local Network</strong>” access is granted</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On <strong>macOS</strong>, enable “System Settings / Privacy & Security / <strong>Local Network</strong> / Delta Chat”</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Your system might have a “personal firewall”,
|
||||
which is known to cause problems (especially on Windows).
|
||||
@@ -1394,6 +1401,7 @@ solo abre archivos adjuntos de remitentes en los que confía y no de spammers.</
|
||||
<ul>
|
||||
<li>In general, anyone can share webxdc apps with each
|
||||
other without restrictions.</li>
|
||||
<li>From <a href="https://webxdc.org/apps/">webxdc.org/apps</a>.</li>
|
||||
<li>You can <a href="https://delta.chat/en/2023-08-11-xstore">send ‘hi’ to xstore@testrun.org</a>
|
||||
to see an experimental webxdc appstore.
|
||||
All of the apps are open source and for free.</li>
|
||||
@@ -1827,6 +1835,35 @@ Otherwise you might receive undecryptable messages from those group chats.</p>
|
||||
<li>Visita la página <a href="https://github.com/deltachat/deltachat-core-rust/blob/master/standards.md#standards-used-in-delta-chat">Estándares usados en Delta Chat</a>.</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="where-can-my-friends-find-delta-chat">
|
||||
|
||||
|
||||
Where can my friends find Delta Chat? <a href="#where-can-my-friends-find-delta-chat" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Delta Chat is available for all major and some minor platforms:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>The <strong>official website</strong>, <a href="https://delta.chat/download">https://delta.chat/download</a> shows all options in detail</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If unavailable, use the <strong>mirror</strong> at <a href="https://deltachat.github.io/deltachat-pages">https://deltachat.github.io/deltachat-pages</a></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Open one of the following <strong>app stores and search for “Delta Chat”:</strong>
|
||||
Google Play Store, F-Droid, Huawei App Gallery, Amazon App Store, iOS and macOS App Store, Microsoft Store</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Check the <strong>package manager</strong> of your Linux distributions</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Android APKs</strong> are also available on <a href="https://github.com/deltachat/deltachat-android/releases">https://github.com/deltachat/deltachat-android/releases</a></p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="cómo-se-financia-el-desarrollo-de-delta-chat">
|
||||
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
<li><a href="#delta-chat-est-il-compatible-avec-protonmail--tutanota--criptext-">Delta Chat est-il compatible avec Protonmail / Tutanota / Criptext ?</a></li>
|
||||
<li><a href="#remove-account">Comment supprimer mon compte ?</a></li>
|
||||
<li><a href="#les-détails-techniques-mintéressent-pouvez-vous-men-dire-plus-">Les détails techniques m’intéressent. Pouvez-vous m’en dire plus ?</a></li>
|
||||
<li><a href="#where-can-my-friends-find-delta-chat">Where can my friends find Delta Chat?</a></li>
|
||||
<li><a href="#comment-est-financé-le-développement-de-delta-chat-">Comment est financé le développement de Delta Chat ?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -1262,6 +1263,12 @@ L’un n’a pas besoin de l’autre pour pouvoir fonctionner.</p>
|
||||
and make sure, <strong>Private Network</strong> is selected as “Network profile type”
|
||||
(after transfer, you can change back to the original value)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On <strong>iOS</strong>, make sure “System Settings / Apps / Delta Chat / <strong>Local Network</strong>” access is granted</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On <strong>macOS</strong>, enable “System Settings / Privacy & Security / <strong>Local Network</strong> / Delta Chat”</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Il se peut que votre système dispose d’un “pare-feu personnalisé”,
|
||||
source bien connue de dysfonctionnements (en particulier sur Windows).
|
||||
@@ -1375,6 +1382,7 @@ you only open attachments from senders you trust, and not from spammers.</li>
|
||||
<ul>
|
||||
<li>In general, anyone can share webxdc apps with each
|
||||
other without restrictions.</li>
|
||||
<li>From <a href="https://webxdc.org/apps/">webxdc.org/apps</a>.</li>
|
||||
<li>You can <a href="https://delta.chat/en/2023-08-11-xstore">send ‘hi’ to xstore@testrun.org</a>
|
||||
to see an experimental webxdc appstore.
|
||||
All of the apps are open source and for free.</li>
|
||||
@@ -1789,6 +1797,35 @@ Otherwise you might receive undecryptable messages from those group chats.</p>
|
||||
<li>Consultez les <a href="https://github.com/deltachat/deltachat-core-rust/blob/master/standards.md#standards-used-in-delta-chat">standards utilisés dans Delta Chat</a>.</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="where-can-my-friends-find-delta-chat">
|
||||
|
||||
|
||||
Where can my friends find Delta Chat? <a href="#where-can-my-friends-find-delta-chat" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Delta Chat is available for all major and some minor platforms:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>The <strong>official website</strong>, <a href="https://delta.chat/download">https://delta.chat/download</a> shows all options in detail</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If unavailable, use the <strong>mirror</strong> at <a href="https://deltachat.github.io/deltachat-pages">https://deltachat.github.io/deltachat-pages</a></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Open one of the following <strong>app stores and search for “Delta Chat”:</strong>
|
||||
Google Play Store, F-Droid, Huawei App Gallery, Amazon App Store, iOS and macOS App Store, Microsoft Store</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Check the <strong>package manager</strong> of your Linux distributions</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Android APKs</strong> are also available on <a href="https://github.com/deltachat/deltachat-android/releases">https://github.com/deltachat/deltachat-android/releases</a></p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="comment-est-financé-le-développement-de-delta-chat-">
|
||||
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
<li><a href="#is-delta-chat-compatible-with-protonmail--tutanota--criptext">Is Delta Chat compatible with Protonmail / Tutanota / Criptext?</a></li>
|
||||
<li><a href="#remove-account">How can I delete my account?</a></li>
|
||||
<li><a href="#im-interested-in-the-technical-details-can-you-tell-me-more">I’m interested in the technical details. Can you tell me more?</a></li>
|
||||
<li><a href="#where-can-my-friends-find-delta-chat">Where can my friends find Delta Chat?</a></li>
|
||||
<li><a href="#how-are-delta-chat-developments-funded">How are Delta Chat developments funded?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -301,8 +302,7 @@ Archived chats remain accessible above the chat list or via search.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>To archive or pin a chat, long tap (Android), use the chat’s menu (Android/Desktop) or swipe to the left (iOS);
|
||||
to mute a chat, use the chat’s menu (Android/Desktop) or the chat’s profile (iOS).</p>
|
||||
<p>To use the functions, long tap or right click a chat in the chat list.</p>
|
||||
|
||||
<h3 id="what-does-the-green-dot-mean">
|
||||
|
||||
@@ -1315,6 +1315,12 @@ One device is not needed for the other to work.</p>
|
||||
and make sure, <strong>Private Network</strong> is selected as “Network profile type”
|
||||
(after transfer, you can change back to the original value)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On <strong>iOS</strong>, make sure “System Settings / Apps / Delta Chat / <strong>Local Network</strong>” access is granted</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On <strong>macOS</strong>, enable “System Settings / Privacy & Security / <strong>Local Network</strong> / Delta Chat”</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Your system might have a “personal firewall”,
|
||||
which is known to cause problems (especially on Windows).
|
||||
@@ -1432,6 +1438,7 @@ you only open attachments from senders you trust, and not from spammers.</li>
|
||||
<ul>
|
||||
<li>In general, anyone can share webxdc apps with each
|
||||
other without restrictions.</li>
|
||||
<li>From <a href="https://webxdc.org/apps/">webxdc.org/apps</a>.</li>
|
||||
<li>You can <a href="https://delta.chat/en/2023-08-11-xstore">send ‘hi’ to xstore@testrun.org</a>
|
||||
to see an experimental webxdc appstore.
|
||||
All of the apps are open source and for free.</li>
|
||||
@@ -1883,6 +1890,35 @@ Otherwise you might receive undecryptable messages from those group chats.</p>
|
||||
<li>See <a href="https://github.com/deltachat/deltachat-core-rust/blob/master/standards.md#standards-used-in-delta-chat">Standards used in Delta Chat</a>.</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="where-can-my-friends-find-delta-chat">
|
||||
|
||||
|
||||
Where can my friends find Delta Chat? <a href="#where-can-my-friends-find-delta-chat" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Delta Chat is available for all major and some minor platforms:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>The <strong>official website</strong>, <a href="https://delta.chat/download">https://delta.chat/download</a> shows all options in detail</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If unavailable, use the <strong>mirror</strong> at <a href="https://deltachat.github.io/deltachat-pages">https://deltachat.github.io/deltachat-pages</a></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Open one of the following <strong>app stores and search for “Delta Chat”:</strong>
|
||||
Google Play Store, F-Droid, Huawei App Gallery, Amazon App Store, iOS and macOS App Store, Microsoft Store</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Check the <strong>package manager</strong> of your Linux distributions</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Android APKs</strong> are also available on <a href="https://github.com/deltachat/deltachat-android/releases">https://github.com/deltachat/deltachat-android/releases</a></p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="how-are-delta-chat-developments-funded">
|
||||
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
<li><a href="#delta-chat-è-compatibile-con-protonmail--tutanota--criptext">Delta Chat è compatibile con Protonmail / Tutanota / Criptext?</a></li>
|
||||
<li><a href="#remove-account">Come posso eliminare il mio profilo?</a></li>
|
||||
<li><a href="#sono-interessato-ai-dettagli-tecnici-mi-puoi-dire-di-più">Sono interessato ai dettagli tecnici. Mi puoi dire di più?</a></li>
|
||||
<li><a href="#dove-possono-trovare-delta-chat-i-miei-amici">Dove possono trovare Delta Chat i miei amici?</a></li>
|
||||
<li><a href="#come-viene-finanziato-lo-sviluppo-di-delta-chat">Come viene finanziato lo sviluppo di Delta Chat?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -1302,6 +1303,12 @@ Un dispositivo non è necessario perché l’altro funzioni.</p>
|
||||
e assicurati che <strong>Rete Privata</strong> sia selezionata come “Tipo di profilo di rete”
|
||||
(dopo il trasferimento è possibile ripristinare il valore originale)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Su <strong>iOS</strong>, assicurati che l’accesso a “Impostazioni di Sistema / App / Delta Chat / <strong>Rete locale</strong>” sia concesso</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Su <strong>macOS</strong>, abilita “Impostazioni di Sistema / Privacy & Sicurezza / <strong>Rete locale</strong> / Delta Chat”</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Il sistema potrebbe avere un “personal firewall”,
|
||||
che è noto per causare problemi (soprattutto su Windows).
|
||||
@@ -1845,6 +1852,35 @@ Altrimenti potresti ricevere messaggi non decifrabili da quelle chat di gruppo.<
|
||||
<li>Vedi <a href="https://github.com/deltachat/deltachat-core-rust/blob/master/standards.md#standards-used-in-delta-chat">Standard usati in Delta Chat</a>.</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="dove-possono-trovare-delta-chat-i-miei-amici">
|
||||
|
||||
|
||||
Dove possono trovare Delta Chat i miei amici? <a href="#dove-possono-trovare-delta-chat-i-miei-amici" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Delta Chat è disponibile per tutte le piattaforme principali e alcune minori:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Il <strong>sito ufficiale</strong>, <a href="https://delta.chat/download">https://delta.chat/download</a> mostra tutte le opzioni in dettaglio</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Se non disponibile, utilizzare <strong>mirror</strong> su <a href="https://deltachat.github.io/deltachat-pages">https://deltachat.github.io/deltachat-pages</a></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Apri uno dei seguenti <strong>app store e cerca “Delta Chat”:</strong>
|
||||
Google Play Store, F-Droid, Huawei App Gallery, Amazon App Store, iOS e macOS App Store, Microsoft Store</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Controlla il <strong>gestore pacchetti</strong> delle tue distribuzioni Linux</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>APK Android</strong> sono disponibili anche su <a href="https://github.com/deltachat/deltachat-android/releases">https://github.com/deltachat/deltachat-android/releases</a></p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="come-viene-finanziato-lo-sviluppo-di-delta-chat">
|
||||
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
<li><a href="#is-delta-chat-compatibel-met-protonmailtutanotacriptext">Is Delta Chat compatibel met Protonmail/Tutanota/Criptext?</a></li>
|
||||
<li><a href="#remove-account">Hoe kan ik mijn account verwijderen?</a></li>
|
||||
<li><a href="#ik-wil-graag-meer-weten-over-de-gebruikte-technieken-waar-kan-ik-meer-informatie-vinden">Ik wil graag meer weten over de gebruikte technieken. Waar kan ik meer informatie vinden?</a></li>
|
||||
<li><a href="#where-can-my-friends-find-delta-chat">Where can my friends find Delta Chat?</a></li>
|
||||
<li><a href="#hoe-wordt-de-ontwikkeling-van-delta-chat-gefinancierd">Hoe wordt de ontwikkeling van Delta Chat gefinancierd?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -1306,6 +1307,12 @@ op beide apparaten</strong>. Hierdoor hoef je niet het ene apparaat bij de hand
|
||||
and make sure, <strong>Private Network</strong> is selected as “Network profile type”
|
||||
(after transfer, you can change back to the original value)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On <strong>iOS</strong>, make sure “System Settings / Apps / Delta Chat / <strong>Local Network</strong>” access is granted</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On <strong>macOS</strong>, enable “System Settings / Privacy & Security / <strong>Local Network</strong> / Delta Chat”</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Wellicht is een firewall actief op je apparaat,
|
||||
welke problemen kan veroorzaken (met name op Windows).
|
||||
@@ -1423,6 +1430,7 @@ you only open attachments from senders you trust, and not from spammers.</li>
|
||||
<ul>
|
||||
<li>In general, anyone can share webxdc apps with each
|
||||
other without restrictions.</li>
|
||||
<li>From <a href="https://webxdc.org/apps/">webxdc.org/apps</a>.</li>
|
||||
<li>You can <a href="https://delta.chat/en/2023-08-11-xstore">send ‘hi’ to xstore@testrun.org</a>
|
||||
to see an experimental webxdc appstore.
|
||||
All of the apps are open source and for free.</li>
|
||||
@@ -1868,6 +1876,35 @@ Otherwise you might receive undecryptable messages from those group chats.</p>
|
||||
<li>Bekijk de pagina <a href="https://github.com/deltachat/deltachat-core-rust/blob/master/standards.md#standards-used-in-delta-chat">Door Delta Chat gebruikte standaarden</a>.</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="where-can-my-friends-find-delta-chat">
|
||||
|
||||
|
||||
Where can my friends find Delta Chat? <a href="#where-can-my-friends-find-delta-chat" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Delta Chat is available for all major and some minor platforms:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>The <strong>official website</strong>, <a href="https://delta.chat/download">https://delta.chat/download</a> shows all options in detail</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If unavailable, use the <strong>mirror</strong> at <a href="https://deltachat.github.io/deltachat-pages">https://deltachat.github.io/deltachat-pages</a></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Open one of the following <strong>app stores and search for “Delta Chat”:</strong>
|
||||
Google Play Store, F-Droid, Huawei App Gallery, Amazon App Store, iOS and macOS App Store, Microsoft Store</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Check the <strong>package manager</strong> of your Linux distributions</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Android APKs</strong> are also available on <a href="https://github.com/deltachat/deltachat-android/releases">https://github.com/deltachat/deltachat-android/releases</a></p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="hoe-wordt-de-ontwikkeling-van-delta-chat-gefinancierd">
|
||||
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
<li><a href="#czy-delta-chat-jest-kompatybilny-z-protonmail--tutanota--criptext">Czy Delta Chat jest kompatybilny z Protonmail / Tutanota / Criptext?</a></li>
|
||||
<li><a href="#remove-account">Jak mogę usunąć swoje konto?</a></li>
|
||||
<li><a href="#interesują-mnie-szczegóły-techniczne-możesz-powiedzieć-mi-coś-więcej">Interesują mnie szczegóły techniczne. Możesz powiedzieć mi coś więcej?</a></li>
|
||||
<li><a href="#gdzie-moi-znajomi-mogą-znaleźć-delta-chat">Gdzie moi znajomi mogą znaleźć Delta Chat?</a></li>
|
||||
<li><a href="#w-jaki-sposób-finansowany-jest-rozwój-delta-chat">W jaki sposób finansowany jest rozwój Delta Chat?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -265,8 +266,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Aby zarchiwizować lub przypiąć czat, dotknij i przytrzymaj (Android), użyj menu czatu (Android/Desktop) lub przesuń palcem w lewo (iOS);
|
||||
aby wyciszyć czat, użyj menu czatu (Android/Desktop) lub profilu czatu (iOS).</p>
|
||||
<p>Aby skorzystać z tych funkcji, przytrzymaj dłużej lub kliknij prawym przyciskiem myszy czat na liście czatów.</p>
|
||||
|
||||
<h3 id="co-oznacza-zielona-kropka">
|
||||
|
||||
@@ -941,6 +941,12 @@ W przypadku innych programów można znaleźć rozwiązanie online.</p>
|
||||
<p>Na <strong>Windowsie</strong>, przejdź do <strong>Panel sterowania / Sieć i internet</strong> i upewnij się, że <strong>Sieć prywatna</strong> jest wybrana jako “Typ profilu sieci”
|
||||
(po przeniesieniu możesz wrócić do pierwotnej wartości)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>W systemie <strong>iOS</strong> upewnij się, że jest przydzielony dostęp do opcji „Ustawienia » Aplikacje » Delta Chat » <strong>Sieć lokalna</strong>”</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>W systemie <strong>macOS</strong> włącz „Ustawienia systemowe » Prywatność i bezpieczeństwo » <strong>Sieć lokalna</strong> » Delta Chat”</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Twój system może mieć „zaporę ogniową”, o której wiadomo, że powoduje problemy (szczególnie w systemie Windows).
|
||||
<strong>Wyłącz zaporę</strong> dla Delta Chat po obu stronach i spróbuj ponownie</p>
|
||||
@@ -1031,6 +1037,7 @@ W przypadku innych programów można znaleźć rozwiązanie online.</p>
|
||||
|
||||
<ul>
|
||||
<li>Ogólnie rzecz biorąc, każdy może udostępniać sobie aplikacje webxdc bez ograniczeń.</li>
|
||||
<li>Od <a href="https://webxdc.org/apps/">webxdc.org/apps</a>.</li>
|
||||
<li>Możesz <a href="https://delta.chat/en/2023-08-11-xstore">wysłać „hi” na adres xstore@testrun.org</a>, aby zobaczyć eksperymentalny sklep z aplikacjami webxdc. Wszystkie aplikacje są otwarto źródłowe i są bezpłatne.</li>
|
||||
<li>Wiele osób pisze własne, aplikacje webxdc i publikuje je na <a href="https://support.delta.chat/c/webxdc/20">forum Delta Chat</a>.</li>
|
||||
</ul>
|
||||
@@ -1356,6 +1363,34 @@ jeśli chcesz sprawdzić, czy Twoje poświadczenia są przetwarzane w bezpieczny
|
||||
<li>Zobacz <a href="https://github.com/deltachat/deltachat-core-rust/blob/master/standards.md#standards-used-in-delta-chat">Standardy używane w Delta Chat</a>.</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="gdzie-moi-znajomi-mogą-znaleźć-delta-chat">
|
||||
|
||||
|
||||
Gdzie moi znajomi mogą znaleźć Delta Chat? <a href="#gdzie-moi-znajomi-mogą-znaleźć-delta-chat" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Delta Chat jest dostępny na wszystkich głównych i niektórych mniejszych platformach:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Oficjalna <strong>strona internetowa</strong> <a href="https://delta.chat/download">https://delta.chat/download</a> pokazuje wszystkie opcje szczegółowo</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Jeśli nie jest dostępna, użyj <strong>kopii strony</strong> pod adresem <a href="https://deltachat.github.io/deltachat-pages">https://deltachat.github.io/deltachat-pages</a></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Otwórz jeden z następujących <strong>sklepów z aplikacjami i wyszukaj „Delta Chat”</strong>: Google Play Store, F-Droid, Huawei App Gallery, Amazon App Store, iOS i macOS App Store, Microsoft Store</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Sprawdź <strong>menedżera pakietów</strong> swoich dystrybucji Linuksa</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Aplikacje na Androida</strong> są również dostępne na <a href="https://github.com/deltachat/deltachat-android/releases">https://github.com/deltachat/deltachat-android/releases</a></p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="w-jaki-sposób-finansowany-jest-rozwój-delta-chat">
|
||||
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
<li><a href="#o-delta-chat-é-compatível-com-protonmail--tutanota--criptext">O Delta Chat é compatível com Protonmail / Tutanota / Criptext?</a></li>
|
||||
<li><a href="#remove-account">How can I delete my account?</a></li>
|
||||
<li><a href="#estou-interessado-nos-detalhes-técnicos-pode-me-dizer-mais">Estou interessado nos detalhes técnicos. Pode me dizer mais?</a></li>
|
||||
<li><a href="#where-can-my-friends-find-delta-chat">Where can my friends find Delta Chat?</a></li>
|
||||
<li><a href="#como-são-os-desenvolvimentos-do-delta-chat-financiados">Como são os desenvolvimentos do Delta Chat financiados?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -297,8 +298,7 @@ Archived chats remain accessible above the chat list or via search.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>To archive or pin a chat, long tap (Android), use the chat’s menu (Android/Desktop) or swipe to the left (iOS);
|
||||
to mute a chat, use the chat’s menu (Android/Desktop) or the chat’s profile (iOS).</p>
|
||||
<p>To use the functions, long tap or right click a chat in the chat list.</p>
|
||||
|
||||
<h3 id="what-does-the-green-dot-mean">
|
||||
|
||||
@@ -1309,6 +1309,12 @@ One device is not needed for the other to work.</p>
|
||||
and make sure, <strong>Private Network</strong> is selected as “Network profile type”
|
||||
(after transfer, you can change back to the original value)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On <strong>iOS</strong>, make sure “System Settings / Apps / Delta Chat / <strong>Local Network</strong>” access is granted</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On <strong>macOS</strong>, enable “System Settings / Privacy & Security / <strong>Local Network</strong> / Delta Chat”</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Your system might have a “personal firewall”,
|
||||
which is known to cause problems (especially on Windows).
|
||||
@@ -1426,6 +1432,7 @@ you only open attachments from senders you trust, and not from spammers.</li>
|
||||
<ul>
|
||||
<li>In general, anyone can share webxdc apps with each
|
||||
other without restrictions.</li>
|
||||
<li>From <a href="https://webxdc.org/apps/">webxdc.org/apps</a>.</li>
|
||||
<li>You can <a href="https://delta.chat/en/2023-08-11-xstore">send ‘hi’ to xstore@testrun.org</a>
|
||||
to see an experimental webxdc appstore.
|
||||
All of the apps are open source and for free.</li>
|
||||
@@ -1869,6 +1876,35 @@ Otherwise you might receive undecryptable messages from those group chats.</p>
|
||||
<li>Veja <a href="https://github.com/deltachat/deltachat-core-rust/blob/master/standards.md#standards-used-in-delta-chat">As normas usadas no Delta Chat</a>.</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="where-can-my-friends-find-delta-chat">
|
||||
|
||||
|
||||
Where can my friends find Delta Chat? <a href="#where-can-my-friends-find-delta-chat" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Delta Chat is available for all major and some minor platforms:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>The <strong>official website</strong>, <a href="https://delta.chat/download">https://delta.chat/download</a> shows all options in detail</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If unavailable, use the <strong>mirror</strong> at <a href="https://deltachat.github.io/deltachat-pages">https://deltachat.github.io/deltachat-pages</a></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Open one of the following <strong>app stores and search for “Delta Chat”:</strong>
|
||||
Google Play Store, F-Droid, Huawei App Gallery, Amazon App Store, iOS and macOS App Store, Microsoft Store</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Check the <strong>package manager</strong> of your Linux distributions</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Android APKs</strong> are also available on <a href="https://github.com/deltachat/deltachat-android/releases">https://github.com/deltachat/deltachat-android/releases</a></p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="como-são-os-desenvolvimentos-do-delta-chat-financiados">
|
||||
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
<li><a href="#разное">Разное</a>
|
||||
<ul>
|
||||
<li><a href="#какие-разрешения-нужны-delta-chat">Какие разрешения нужны Delta Chat?</a></li>
|
||||
<li><a href="#работает-ли-delta-chat-работу-с-моим-провайдером-электронной-почты">Работает ли Delta Chat работу с <em>моим</em> провайдером электронной почты?</a></li>
|
||||
<li><a href="#работает-ли-delta-chat-с-моим-провайдером-электронной-почты">Работает ли Delta Chat с <em>моим</em> провайдером электронной почты?</a></li>
|
||||
<li><a href="#я-хочу-управлять-своим-собственным-почтовым-сервером-для-delta-chat-что-вы-посоветуете">Я хочу управлять своим собственным почтовым сервером для Delta Chat. Что вы посоветуете?</a></li>
|
||||
<li><a href="#почему-я-должен-вводить-свой-пароль-от-электронной-почты-в-delta-chat-это-безопасно">Почему я должен вводить свой пароль от электронной почты в Delta Chat? Это безопасно?</a></li>
|
||||
<li><a href="#какие-сообщения-отображаются-в-delta-chat">Какие сообщения отображаются в Delta Chat?</a></li>
|
||||
@@ -99,6 +99,7 @@
|
||||
<li><a href="#совместим-ли-delta-chat-с-protonmail--tutanota--criptext">Совместим ли Delta Chat с Protonmail / Tutanota / Criptext?</a></li>
|
||||
<li><a href="#remove-account">Как я могу удалить свой аккаунт?</a></li>
|
||||
<li><a href="#меня-интересуют-технические-детали-можете-рассказать-больше">Меня интересуют технические детали. Можете рассказать больше?</a></li>
|
||||
<li><a href="#где-мои-друзья-могут-найти-delta-chat">Где мои друзья могут найти Delta Chat?</a></li>
|
||||
<li><a href="#как-финансируются-разработки-delta-chat">Как финансируются разработки Delta Chat?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -217,7 +218,7 @@
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Профиль - это <strong>имя, изображение</strong> и некоторая дополнительная информация для шифрования сообщений.
|
||||
<p>Профиль — это <strong>имя, изображение</strong> и некоторая дополнительная информация для шифрования сообщений.
|
||||
Профиль существует только на вашем устройстве(ах)
|
||||
и использует серверы chatmail или обычные серверы электронной почты для передачи сообщений.</p>
|
||||
|
||||
@@ -301,8 +302,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Чтобы архивировать или закрепить чат, нажмите на него долгим нажатием (Android), воспользуйтесь меню чата (Android/Desktop) или проведите пальцем влево (iOS);
|
||||
чтобы отключить уведомления, воспользуйтесь меню чата (Android/Desktop) или профиль чата (iOS).</p>
|
||||
<p>Чтобы использовать функции, нажмите долгим нажатием или щелкните правой кнопкой мыши по чату в списке чатов.</p>
|
||||
|
||||
<h3 id="что-означает-зеленая-точка">
|
||||
|
||||
@@ -350,7 +350,7 @@
|
||||
приложение Delta Chat каждого участника чата позаботится
|
||||
об удалении сообщений
|
||||
по истечении выбранного периода времени.
|
||||
Отсчет времени начинается
|
||||
Отсчет времени начинается,
|
||||
когда получатель впервые видит сообщение в Delta Chat.
|
||||
Сообщения удаляются
|
||||
как в каждой учетной записи электронной почты на сервере,
|
||||
@@ -429,7 +429,7 @@
|
||||
Если позже вы снова захотите присоединиться к группе, попросите другого участника группы добавить вас.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Или, вместо этого, вы можете “отключить уведомления” для группы - это означает, что вы будете получать все сообщения и сможете их писать, но больше не будете получать уведомления о новых сообщениях.</p>
|
||||
<p>Или, вместо этого, вы можете “отключить уведомления” для группы — это означает, что вы будете получать все сообщения и сможете их писать, но больше не будете получать уведомления о новых сообщениях.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -474,7 +474,7 @@
|
||||
По истечении этого срока все сообщения, полученные Delta Chat, будут удалены с сервера.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Обратите внимание, что если вы используете Delta Chat на нескольких устройствах,
|
||||
<p>Обратите внимание, что, если вы используете Delta Chat на нескольких устройствах,
|
||||
необходимо оставлять сообщения на сервере с достаточным временным интервалом,
|
||||
чтобы другое устройство(а) тоже смогло их загрузить.</p>
|
||||
</li>
|
||||
@@ -575,8 +575,8 @@ Push-уведомления недоступны.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Принудительное соединение в фоновом режиме: это запасной вариант
|
||||
если предыдущие варианты недоступны или или не обеспечивают “мгновенную доставку”.
|
||||
Включение этого параметра вызывает постоянное уведомление на вашем телефоне
|
||||
если предыдущие варианты недоступны или не обеспечивают “мгновенную доставку”.
|
||||
Включение этого параметра вызывает постоянное уведомление на вашем телефоне,
|
||||
которое иногда может быть “сжато” на последних телефонах Android.</p>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -642,7 +642,7 @@ Push-уведомления недоступны.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Delta Chat - это бесплатный децентрализованный мессенджер с открытым исходным кодом и возможностью выбора сервера,
|
||||
<p>Delta Chat — это бесплатный децентрализованный мессенджер с открытым исходным кодом и возможностью выбора сервера,
|
||||
но мы хотим, чтобы пользователи гарантированно получали “мгновенную доставку” сообщений,
|
||||
такую же, что и в приложениях WhatsApp, Signal или Telegram,
|
||||
не задавая вопросов, которые больше подходят для опытных пользователей или разработчиков.</p>
|
||||
@@ -671,7 +671,7 @@ Push-уведомления недоступны.</p>
|
||||
<p><a href="https://autocrypt.org">Autocrypt</a> используется для автоматической
|
||||
установки сквозного шифрования при работе с контактами и групповыми чатами.
|
||||
Autocrypt использует ограниченное и <a href="#openpgp-secure">безопасное подмножество стандарта OpenPGP</a>.
|
||||
Сообщения, зашифрованные сквозным шифрованием помечаются замком
|
||||
Сообщения, зашифрованные сквозным шифрованием, помечаются замком
|
||||
<img style="vertical-align:middle; width:1.2em; margin:1px" src="../lock-icon.png" alt="padlock" />.</p>
|
||||
|
||||
<p><a href="https://securejoin.delta.chat/en/latest/new.html">Протоколы Secure-Join</a>
|
||||
@@ -689,7 +689,7 @@ Autocrypt использует ограниченное и <a href="#openpgp-sec
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Все сообщения зашифрованые сквозным шифрованием имеют значок замка:</p>
|
||||
<p>Все сообщения, зашифрованные сквозным шифрованием, имеют значок замка:</p>
|
||||
|
||||
<p><img style="width:160px; margin:1px" src="../lock-screenshot.png" alt="Значок замка в пузырьке" /></p>
|
||||
|
||||
@@ -779,7 +779,7 @@ Autocrypt использует ограниченное и <a href="#openpgp-sec
|
||||
В профиле контакта можно несколько раз нажать на текст “Представлен …” несколько раз
|
||||
пока не дойдёте до того, с кем вы непосредственно провели <a href="#howtoe2ee">QR-сканирование</a>.</p>
|
||||
|
||||
<p>Обратите внимание, что в профиле контакта вы можете увидеть и присоеденённых знакомых
|
||||
<p>Обратите внимание, что в профиле контакта вы можете увидеть и присоединённых знакомых,
|
||||
но в заголовке профиля нет зелёной галочки.
|
||||
Обычно это означает, что контакт <a href="#nocryptanymore">“отправил сообщение с другого устройства”</a>.</p>
|
||||
|
||||
@@ -797,10 +797,10 @@ Autocrypt использует ограниченное и <a href="#openpgp-sec
|
||||
</h3>
|
||||
|
||||
<p>Ваш чат с контактом утратил гарантированное сквозное шифрование.
|
||||
Зелёная галочка была удалёна для этого чата и контакта, и когда вы увидели предупреждение.
|
||||
Зелёная галочка была удалена для этого чата и контакта, и когда вы увидели предупреждение.
|
||||
<strong>Если вы обнаружили внезапное прекращение гарантированного сквозного шифрования
|
||||
для этого контакта, не принимайте предупреждение!</strong>
|
||||
Вместо этого свяжитесь со своим контактом по второму каналу
|
||||
Вместо этого свяжитесь со своим контактом по второму каналу,
|
||||
например, видеозвонок, другой мессенджер или телефонный звонок,
|
||||
чтобы узнать, что произошло.</p>
|
||||
|
||||
@@ -935,7 +935,7 @@ Delta Chat вместо этого использует реализацию Ope
|
||||
и возвращает <a href="https://docs.rs/pgp/latest/pgp/errors/enum.Error.html#variant.MdcError">ошибку</a>
|
||||
если код обнаружения модификаций не совпадает.</p>
|
||||
|
||||
<p>Delta Chat также никогда не был уязвим к атаке “Direct Exfiltration” EFAIL
|
||||
<p>Delta Chat также никогда не был уязвим к атаке “Direct Exfiltration” EFAIL,
|
||||
потому что он расшифровывает только <code class="language-plaintext highlighter-rouge">многочастные/зашифрованные</code> сообщения,
|
||||
которые содержат ровно одну зашифрованную и подписанную часть,
|
||||
как определено спецификацией Autocrypt Level 1.</p>
|
||||
@@ -1172,16 +1172,16 @@ Look for something like <strong>Start Autocrypt Setup Transfer</strong> in the s
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Наиболее вероятная причина - это то, что ваш ключ зашифрован и/или имеет пароль.
|
||||
<p>Наиболее вероятная причина — это то, что ваш ключ зашифрован и/или имеет пароль.
|
||||
Delta Chat не поддерживает такие ключи.
|
||||
Вы можете снять шифрование пароля и пароль, а затем повторить попытку импорта.</p>
|
||||
|
||||
<p>Другая распространенная ошибка - неправильное расширение файла.
|
||||
Используйте формат ASCII armored и расширение файла <code class="language-plaintext highlighter-rouge">.asc</code>.</p>
|
||||
|
||||
<p>Delta Chat поддерживает расспространённые форматы закрытых ключей OpenPGP, однако
|
||||
<p>Delta Chat поддерживает распространённые форматы закрытых ключей OpenPGP, однако
|
||||
маловероятно, что мы будем поддерживать все закрытые ключи из любых источников. Это
|
||||
не является основной целью Delta Chat. На самом деле, подавляющее большинство пользователей
|
||||
не является основной целью Delta Chat. На самом деле, подавляющее большинство пользователей
|
||||
Delta Chat не будут иметь никакого ключа, прежде чем они начнут использовать его.
|
||||
Однако мы стремимся поддерживать закрытые ключи из как можно большего числа источников.</p>
|
||||
|
||||
@@ -1305,13 +1305,19 @@ Chat <a href="https://github.com/rpgp/rpgp">PGP</a> и
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Перепроверьте, что оба устройства находятся в <strong>в одной Wi-Fi или локальной сети</strong>.</p>
|
||||
<p>Перепроверьте, что оба устройства находятся <strong>в одной Wi-Fi или локальной сети</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>В <strong>Windows</strong> перейдите в <strong>Панель управления / Сеть и Интернет</strong>
|
||||
и убедитесь, что в качестве “Типа сетевого профиля” выбрана <strong>Частная сеть</strong>.
|
||||
(после передачи, вы можете изменить обратно на исходное значение)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>На <strong>iOS</strong>, убедитесь, что предоставлен доступ “Настройки системы / Приложения / Delta Chat / <strong>Локальная сеть</strong>”</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>На <strong>macOS</strong>, включите “Системные настройки / Конфиденциальность и безопасность / <strong>Локальная сеть</strong> / Delta Chat”</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>В вашей системе может быть установлен “персональный брандмауэр”,
|
||||
который может вызвать проблемы (особенно на Windows).
|
||||
@@ -1407,12 +1413,12 @@ PIN-код разблокировки экрана, графический кл
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>Приложения webxdc webxdc не могут отправлять данные в Интернет или скачивать что-либо.</li>
|
||||
<li>Приложения webxdc не могут отправлять данные в Интернет или скачивать что-либо.</li>
|
||||
<li>Приложение webxdc может обмениваться данными только внутри чата Delta Chat, с его
|
||||
копиями на устройствах ваших собеседников по чату.
|
||||
В остальном, оно полностью
|
||||
изолировано от Интернета.</li>
|
||||
<li>Конфиденциальность, которую обеспечивает приложение webxdc - это конфиденциальность вашего чата - пока
|
||||
<li>Конфиденциальность, которую обеспечивает приложение webxdc — это конфиденциальность вашего чата - пока
|
||||
вы доверяете людям, с которыми вы общаетесь, вы можете доверять приложению webxdc.</li>
|
||||
<li>Это также означает, что открытие приложений webxdc в чатах с участниками, которым вы
|
||||
не доверяете, тоже самое, что и с вложениями электронной почты, когда вы открываете
|
||||
@@ -1430,11 +1436,11 @@ PIN-код разблокировки экрана, графический кл
|
||||
<ul>
|
||||
<li>В общем, каждый может поделиться приложениями webxdc
|
||||
друг с другом без ограничений.</li>
|
||||
<li>Из <a href="https://webxdc.org/apps/">webxdc.org/apps</a>.</li>
|
||||
<li>Вы можете <a href="https://delta.chat/en/2023-08-11-xstore">отправить ‘hi’ на xstore@testrun.org</a>
|
||||
чтобы увидеть экспериментальный магазин приложений webxdc.
|
||||
Все приложения с открытым исходным кодом и бесплатны.</li>
|
||||
<li>Многие люди пишут свои собственные приложения webxdc и публикуют их на <a href="https://support.delta.chat/c/webxdc/20">форуме
|
||||
Delta Chat</a>.</li>
|
||||
<li>Многие люди создают свои собственные приложения webxdc и публикуют их на <a href="https://support.delta.chat/c/webxdc/20">форуме Delta Chat</a>.</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="как-я-могу-создать-свои-собственные-приложения-webxdc">
|
||||
@@ -1446,7 +1452,7 @@ Delta Chat</a>.</li>
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>Приложения webxdc - это просто архивы zip, содержащие код html, css и javascript.</li>
|
||||
<li>Приложения webxdc — это просто архивы zip, содержащие код html, css и javascript.</li>
|
||||
<li>Вы можете расширить <a href="https://github.com/webxdc/hello">пример приложения Hello World</a>
|
||||
для начала.</li>
|
||||
<li>Все остальное, что вам нужно знать, написано в
|
||||
@@ -1572,7 +1578,7 @@ Delta Chat</a>.</li>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Измените свой адрес в разделе “Настройки → Дополнительные параметры → Пароль и аккаунт” и
|
||||
введите пароль вашей новой электронной почты (и если необходимо, настройки сервера).
|
||||
введите пароль вашей новой электронной почты (и, если необходимо, настройки сервера).
|
||||
Вы получите информационное сообщение о том, что переходите на новый адрес.
|
||||
Дополнительное уведомление также появится в вашем чате “Сообщения устройства”.</p>
|
||||
</li>
|
||||
@@ -1656,10 +1662,10 @@ Delta Chat</a>.</li>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="работает-ли-delta-chat-работу-с-моим-провайдером-электронной-почты">
|
||||
<h3 id="работает-ли-delta-chat-с-моим-провайдером-электронной-почты">
|
||||
|
||||
|
||||
Работает ли Delta Chat работу с <em>моим</em> провайдером электронной почты? <a href="#работает-ли-delta-chat-работу-с-моим-провайдером-электронной-почты" class="anchor"></a>
|
||||
Работает ли Delta Chat с <em>моим</em> провайдером электронной почты? <a href="#работает-ли-delta-chat-с-моим-провайдером-электронной-почты" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
@@ -1791,7 +1797,7 @@ Delta Chat необходим пароль, чтобы использовать
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Единственная причина, по которой вы можете захотеть наблюдать за папкой “Отправленные”, - это если вы используете другую
|
||||
<p>Единственная причина, по которой вы можете захотеть наблюдать за папкой “Отправленные”, — это если вы используете другую
|
||||
почтовую программу (например, Thunderbird) вместе с приложением Delta Chat, и хотите, чтобы ваш почтовый клиент
|
||||
мог участвовать в разговорах в чате.</p>
|
||||
|
||||
@@ -1879,6 +1885,35 @@ Chat.</li>
|
||||
<li>Смотрите <a href="https://github.com/deltachat/deltachat-core-rust/blob/master/standards.md#standards-used-in-delta-chat">Стандарты, используемые в Delta Chat</a>.</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="где-мои-друзья-могут-найти-delta-chat">
|
||||
|
||||
|
||||
Где мои друзья могут найти Delta Chat? <a href="#где-мои-друзья-могут-найти-delta-chat" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Delta Chat доступен на всех популярных и некоторых менее известных платформах:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>На <strong>официальной странице загрузки</strong>, <a href="https://delta.chat/download">https://delta.chat/download</a> можно найти подробную информацию о всех вариантах</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Если основной сайт недоступен, используйте <strong>зеркало</strong> <a href="https://deltachat.github.io/deltachat-pages">https://deltachat.github.io/deltachat-pages</a></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Откройте один из следующих <strong>онлайн-магазинов приложений и введите запрос “Delta Chat”:</strong>
|
||||
Google Play Store, F-Droid, Huawei App Gallery, Amazon App Store, App Store для iOS и macOS, Microsoft Store</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Проверьте <strong>менеджер пакетов</strong> вашего дистрибутива Linux</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Файлы <strong>APK для Android</strong> доступны на <a href="https://github.com/deltachat/deltachat-android/releases">https://github.com/deltachat/deltachat-android/releases</a></p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="как-финансируются-разработки-delta-chat">
|
||||
|
||||
|
||||
@@ -1895,7 +1930,7 @@ Chat.</li>
|
||||
основанную на Свободном и Открытом исходном коде.</p>
|
||||
|
||||
<p>В частности, разработка Delta Chat финансировалась из следующих источников,
|
||||
перечисленны в хронологическом порядке:</p>
|
||||
перечислены в хронологическом порядке:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
@@ -1910,7 +1945,7 @@ Chat.</li>
|
||||
и выпустили первую бета-версию приложения для настольных систем, а также провели
|
||||
исследования в области UX в контексте прав человека,
|
||||
см. наш заключительный отчет <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
|
||||
Второй грант, полученый в 2019/2020 году (~$300тыс.), помог нам
|
||||
Второй грант, полученный в 2019/2020 году (~$300 тыс.), помог нам
|
||||
выпустить версии Delta/iOS, перевести наш основной код на Rust и
|
||||
предоставить новые функции для всех платформ.</p>
|
||||
</li>
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
<li><a href="#je-delta-chat-kompatibilný-s-protonmail--tutanota--criptext">Je Delta Chat kompatibilný s Protonmail / Tutanota / Criptext?</a></li>
|
||||
<li><a href="#remove-account">How can I delete my account?</a></li>
|
||||
<li><a href="#zaujímajú-ma-technické-detaily-môžete-mi-povedať-viac">Zaujímajú ma technické detaily. Môžete mi povedať viac?</a></li>
|
||||
<li><a href="#where-can-my-friends-find-delta-chat">Where can my friends find Delta Chat?</a></li>
|
||||
<li><a href="#ako-sa-financuje-vývoj-delta-chat">Ako sa financuje vývoj Delta Chat?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -301,8 +302,7 @@ Archived chats remain accessible above the chat list or via search.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>To archive or pin a chat, long tap (Android), use the chat’s menu (Android/Desktop) or swipe to the left (iOS);
|
||||
to mute a chat, use the chat’s menu (Android/Desktop) or the chat’s profile (iOS).</p>
|
||||
<p>To use the functions, long tap or right click a chat in the chat list.</p>
|
||||
|
||||
<h3 id="what-does-the-green-dot-mean">
|
||||
|
||||
@@ -1315,6 +1315,12 @@ One device is not needed for the other to work.</p>
|
||||
and make sure, <strong>Private Network</strong> is selected as “Network profile type”
|
||||
(after transfer, you can change back to the original value)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On <strong>iOS</strong>, make sure “System Settings / Apps / Delta Chat / <strong>Local Network</strong>” access is granted</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On <strong>macOS</strong>, enable “System Settings / Privacy & Security / <strong>Local Network</strong> / Delta Chat”</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Your system might have a “personal firewall”,
|
||||
which is known to cause problems (especially on Windows).
|
||||
@@ -1432,6 +1438,7 @@ you only open attachments from senders you trust, and not from spammers.</li>
|
||||
<ul>
|
||||
<li>In general, anyone can share webxdc apps with each
|
||||
other without restrictions.</li>
|
||||
<li>From <a href="https://webxdc.org/apps/">webxdc.org/apps</a>.</li>
|
||||
<li>You can <a href="https://delta.chat/en/2023-08-11-xstore">send ‘hi’ to xstore@testrun.org</a>
|
||||
to see an experimental webxdc appstore.
|
||||
All of the apps are open source and for free.</li>
|
||||
@@ -1879,6 +1886,35 @@ Otherwise you might receive undecryptable messages from those group chats.</p>
|
||||
<li>Pozrite si <a href="https://github.com/deltachat/deltachat-core-rust/blob/master/standards.md#standards-used-in-delta-chat">Štandardy používané v Delta Chate</a>.</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="where-can-my-friends-find-delta-chat">
|
||||
|
||||
|
||||
Where can my friends find Delta Chat? <a href="#where-can-my-friends-find-delta-chat" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Delta Chat is available for all major and some minor platforms:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>The <strong>official website</strong>, <a href="https://delta.chat/download">https://delta.chat/download</a> shows all options in detail</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If unavailable, use the <strong>mirror</strong> at <a href="https://deltachat.github.io/deltachat-pages">https://deltachat.github.io/deltachat-pages</a></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Open one of the following <strong>app stores and search for “Delta Chat”:</strong>
|
||||
Google Play Store, F-Droid, Huawei App Gallery, Amazon App Store, iOS and macOS App Store, Microsoft Store</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Check the <strong>package manager</strong> of your Linux distributions</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Android APKs</strong> are also available on <a href="https://github.com/deltachat/deltachat-android/releases">https://github.com/deltachat/deltachat-android/releases</a></p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="ako-sa-financuje-vývoj-delta-chat">
|
||||
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
<li><a href="#a-është-i-përputhshëm-delta-chat-i-me-protonmail-in--tutanota-n--criptext-in">A është i përputhshëm Delta Chat-i me Protonmail-in / Tutanota-n / Criptext-in?</a></li>
|
||||
<li><a href="#remove-account">Si mund ta fshij llogarinë time?</a></li>
|
||||
<li><a href="#më-interesojnë-hollësitë-teknike-mund-të-më-tregoni-diçka-më-tepër">Më interesojnë hollësitë teknike. Mund të më tregoni diçka më tepër?</a></li>
|
||||
<li><a href="#where-can-my-friends-find-delta-chat">Where can my friends find Delta Chat?</a></li>
|
||||
<li><a href="#si-financohet-zhvillimi-i-delta-chat-it">Si financohet zhvillimi i Delta Chat-it?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -1315,6 +1316,12 @@ Njëra pajisja s’ka nevojë për tjetrën që të funksionojë.</p>
|
||||
and make sure, <strong>Private Network</strong> is selected as “Network profile type”
|
||||
(after transfer, you can change back to the original value)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On <strong>iOS</strong>, make sure “System Settings / Apps / Delta Chat / <strong>Local Network</strong>” access is granted</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On <strong>macOS</strong>, enable “System Settings / Privacy & Security / <strong>Local Network</strong> / Delta Chat”</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Sistemi juaj mund të ketë një “firewall personal”,
|
||||
që dihet se shkakton probleme (veçanërisht në Windows).
|
||||
@@ -1890,6 +1897,35 @@ Otherwise you might receive undecryptable messages from those group chats.</p>
|
||||
<li>Shihni <a href="https://github.com/deltachat/deltachat-core-rust/blob/master/standards.md#standards-used-in-delta-chat">Standarde të përdorur në Delta Chat</a>.</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="where-can-my-friends-find-delta-chat">
|
||||
|
||||
|
||||
Where can my friends find Delta Chat? <a href="#where-can-my-friends-find-delta-chat" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Delta Chat is available for all major and some minor platforms:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>The <strong>official website</strong>, <a href="https://delta.chat/download">https://delta.chat/download</a> shows all options in detail</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If unavailable, use the <strong>mirror</strong> at <a href="https://deltachat.github.io/deltachat-pages">https://deltachat.github.io/deltachat-pages</a></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Open one of the following <strong>app stores and search for “Delta Chat”:</strong>
|
||||
Google Play Store, F-Droid, Huawei App Gallery, Amazon App Store, iOS and macOS App Store, Microsoft Store</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Check the <strong>package manager</strong> of your Linux distributions</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Android APKs</strong> are also available on <a href="https://github.com/deltachat/deltachat-android/releases">https://github.com/deltachat/deltachat-android/releases</a></p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="si-financohet-zhvillimi-i-delta-chat-it">
|
||||
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
<li><a href="#чи-сумісний-delta-chat-із-protonmail--tutanota--criptext">Чи сумісний Delta Chat із Protonmail / Tutanota / Criptext?</a></li>
|
||||
<li><a href="#remove-account">Як мені видалити свій обліковий запис?</a></li>
|
||||
<li><a href="#мене-цікавлять-технічні-деталі-можете-розповісти-більше">Мене цікавлять технічні деталі. Можете розповісти більше?</a></li>
|
||||
<li><a href="#де-мої-друзі-можуть-знайти-delta-chat">Де мої друзі можуть знайти Delta Chat?</a></li>
|
||||
<li><a href="#як-фінансується-розробка-delta-chat">Як фінансується розробка Delta Chat?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -267,7 +268,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Щоб заархівувати або закріпити чат, довго торкніться (Android), скористайтеся меню чату (Android/комп’ютер) або проведіть пальцем ліворуч (iOS); щоб вимкнути звук чату, скористайтеся меню чату (Android/комп’ютер) або профілем чату (iOS).</p>
|
||||
<p>Щоб скористатися функціями, утримуйте натиснутою клавішу або клацніть правою кнопкою миші на чаті у списку чатів.</p>
|
||||
|
||||
<h3 id="що-означає-зелена-точка">
|
||||
|
||||
@@ -944,6 +945,12 @@ Look for something like <strong>Start Autocrypt Setup Transfer</strong> in the s
|
||||
<li>
|
||||
<p>У <strong>Windows</strong> перейдіть до <strong>Панель керування / Мережа та Інтернет</strong> і переконайтеся, що <strong>Приватна мережа</strong> вибрано як “Тип мережевого профілю” (після перенесення ви можете повернути початкове значення)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>На <strong>iOS</strong> переконайтеся, що доступ до “Системні налаштування / Програми / Delta Chat / <strong>Локальна мережа</strong>” дозволено</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>У <strong>macOS</strong> увімкніть “Системні налаштування / Конфіденційність і безпека / <strong>Локальна мережа</strong> / Delta Chat”</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Ваша система може мати “персональний брандмауер”, який, як відомо, викликає проблеми (особливо у Windows). <strong>Вимкніть персональний брандмауер</strong> для Delta Chat на обох кінцях і повторіть спробу</p>
|
||||
</li>
|
||||
@@ -1032,9 +1039,11 @@ Look for something like <strong>Start Autocrypt Setup Transfer</strong> in the s
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>Загалом, будь-хто може ділитися додатками webxdc один з одним один з одним без обмежень.</li>
|
||||
<li>Ви можете <a href="https://delta.chat/en/2023-08-11-xstore">надіслати “hi” на xstore@testrun.org</a> щоб побачити експериментальний магазин додатків webxdc. Всі програми мають відкритий вихідний код і є безкоштовними.</li>
|
||||
<li>Багато людей пишуть власні програми для webxdc і публікують їх на <a href="https://support.delta.chat/c/webxdc/20">форумі Delta Chat</a>.</li>
|
||||
<li>Загалом, будь-хто може ділитися додатками webxdc один з одним без обмежень.</li>
|
||||
<li>З <a href="https://webxdc.org/apps/">webxdc.org/apps</a>.</li>
|
||||
<li>Ви можете <a href="https://delta.chat/en/2023-08-11-xstore">надіслати “привіт” на xstore@testrun.org</a> щоб побачити експериментальний магазин додатків webxdc.
|
||||
Всі програми мають відкритий вихідний код і є безкоштовними.</li>
|
||||
<li>Багато людей пишуть власні програми webxdc і публікують їх на <a href="https://support.delta.chat/c/webxdc/20">форумі Delta Chat</a>.</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="як-я-можу-створювати-власні-програми-webxdc">
|
||||
@@ -1374,6 +1383,34 @@ Look for something like <strong>Start Autocrypt Setup Transfer</strong> in the s
|
||||
<li>Дивіться <a href="https://github.com/deltachat/deltachat-core-rust/blob/master/standards.md#standards-used-in-delta-chat">Стандарти, що використовуються у Delta Chat</a>.</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="де-мої-друзі-можуть-знайти-delta-chat">
|
||||
|
||||
|
||||
Де мої друзі можуть знайти Delta Chat? <a href="#де-мої-друзі-можуть-знайти-delta-chat" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Delta Chat доступний для всіх основних і деяких другорядних платформ:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>На <strong>офіційному сайті</strong>, <a href="https://delta.chat/download">https://delta.chat/download</a> детально описані всі варіанти</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Якщо вона недоступна, скористайтеся <strong>дзеркалом</strong> за адресою <a href="https://deltachat.github.io/deltachat-pages">https://deltachat.github.io/deltachat-pages</a></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Відкрийте один з наступних <strong>магазинів додатків і введіть у пошук “Delta Chat”:</strong> Google Play Store, F-Droid, Huawei App Gallery, Amazon App Store, iOS та macOS App Store, Microsoft Store</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Перевірте <strong>менеджер пакунків</strong> вашого дистрибутива Linux</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>APK для Android</strong> також доступні на <a href="https://github.com/deltachat/deltachat-android/releases">https://github.com/deltachat/deltachat-android/releases</a></p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="як-фінансується-розробка-delta-chat">
|
||||
|
||||
|
||||
|
||||
@@ -27,9 +27,9 @@
|
||||
</li>
|
||||
<li><a href="#instant-delivery">Instant message delivery and Push Notifications</a>
|
||||
<ul>
|
||||
<li><a href="#what-are-push-notifications-how-can-i-get-instant-message-delivery">What are Push Notifications? How can I get instant message delivery?</a></li>
|
||||
<li><a href="#are-push-notifications-enabled-on-ios-devices-is-there-an-alternative">Are Push Notifications enabled on iOS devices? Is there an alternative?</a></li>
|
||||
<li><a href="#android-push">Are Push notifications enabled / needed on Android devices?</a></li>
|
||||
<li><a href="#什么是推送通知如何获得即时消息传递">什么是推送通知?如何获得即时消息传递?</a></li>
|
||||
<li><a href="#ios-设备上是否启用了推送通知我还有其他的选择吗">iOS 设备上是否启用了推送通知?我还有其他的选择吗?</a></li>
|
||||
<li><a href="#android-设备上是否启用需要推送通知android-push">Android 设备上是否启用/需要推送通知?{#android-push}</a></li>
|
||||
<li><a href="#privacy-notifications">How private are Delta Chat Push Notifications?</a></li>
|
||||
<li><a href="#why-does-delta-chat-integrate-with-centralized-proprietary-applegoogle-push-services">Why does Delta Chat integrate with centralized proprietary Apple/Google push services?</a></li>
|
||||
</ul>
|
||||
@@ -44,7 +44,7 @@
|
||||
<li><a href="#are-attachments-pictures-files-audio-etc-end-to-end-encrypted">Are attachments (pictures, files, audio etc.) end-to-end encrypted?</a></li>
|
||||
<li><a href="#openpgp-secure">Is OpenPGP secure?</a></li>
|
||||
<li><a href="#openpgp-alternatives">Did you consider using alternatives to OpenPGP for end-to-end -encryption?</a></li>
|
||||
<li><a href="#is-delta-chat-vulnerable-to-efail">Is Delta Chat vulnerable to EFAIL?</a></li>
|
||||
<li><a href="#delta-chat-是否容易受到-efail-攻击">Delta Chat 是否容易受到 EFAIL 攻击?</a></li>
|
||||
<li><a href="#tls">Is a message exposed in cleartext if end-to-end encryption is not available?</a></li>
|
||||
<li><a href="#message-metadata">How does Delta Chat protect metadata in messages?</a></li>
|
||||
<li><a href="#device-seizure">How to protect metadata and contacts when a device is seized?</a></li>
|
||||
@@ -53,7 +53,7 @@
|
||||
<li><a href="#why-do-i-see-unencrypted-messages">Why do I see unencrypted messages?</a></li>
|
||||
<li><a href="#how-can-i-get-an-end-to-end-encrypted-chat-with-a-delta-chat-contact-who-sometimes-uses-webmail-or-another-non-autocrypt-e-mail-app">How can i get an end-to-end encrypted chat with a Delta Chat contact who sometimes uses webmail or another non-Autocrypt e-mail app?</a></li>
|
||||
<li><a href="#how-can-i-ensure-message-end-to-end-encryption-and-deletion">How can I ensure message end-to-end encryption and deletion?</a></li>
|
||||
<li><a href="#pfs">Does Delta Chat support Perfect Forward Secrecy?</a></li>
|
||||
<li><a href="#pfs">Delta Chat 是否支持完美前向保密?</a></li>
|
||||
<li><a href="#is-end-to-end-encryption-of-delta-chat-as-safe-as-signal">Is end-to-end encryption of Delta Chat as safe as Signal?</a></li>
|
||||
<li><a href="#importkey">我可以重复使用现有的私钥吗?</a></li>
|
||||
<li><a href="#我无法将现有的-pgp-密钥导入-delta-chat">我无法将现有的 PGP 密钥导入 Delta Chat。</a></li>
|
||||
@@ -99,6 +99,7 @@
|
||||
<li><a href="#delta-chat-与-protonmail--tutanota--criptext-兼容吗">Delta Chat 与 Protonmail / Tutanota / Criptext 兼容吗?</a></li>
|
||||
<li><a href="#remove-account">如何删除我的账户?</a></li>
|
||||
<li><a href="#我对技术细节很感兴趣能告诉我更多吗">我对技术细节很感兴趣。能告诉我更多吗?</a></li>
|
||||
<li><a href="#where-can-my-friends-find-delta-chat">Where can my friends find Delta Chat?</a></li>
|
||||
<li><a href="#delta-chat-的开发是如何被资助的">Delta Chat 的开发是如何被资助的?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -337,10 +338,8 @@ The messages are deleted
|
||||
both in each email account on the server,
|
||||
and in the app itself.</p>
|
||||
|
||||
<p>Note that you can rely on disappearing messages
|
||||
only as long as you trust your chat partners;
|
||||
malicious chat partners can take photos,
|
||||
or otherwise save, copy or forward messages before deletion.</p>
|
||||
<p>请注意,只有当您信任您的聊天伙伴时,您才可以依赖“消息定时销毁”;
|
||||
不怀好意的人可能会拍照,或者在删除之前以其他方式保存、复制或转发消息。</p>
|
||||
|
||||
<p>Apart from that,
|
||||
if one chat partner uninstalls Delta Chat,
|
||||
@@ -473,64 +472,59 @@ Delta Chat 收到的所有电子邮件都将在此期限后从服务器中删除
|
||||
|
||||
</h2>
|
||||
|
||||
<h3 id="what-are-push-notifications-how-can-i-get-instant-message-delivery">
|
||||
<h3 id="什么是推送通知如何获得即时消息传递">
|
||||
|
||||
|
||||
What are Push Notifications? How can I get instant message delivery? <a href="#what-are-push-notifications-how-can-i-get-instant-message-delivery" class="anchor"></a>
|
||||
什么是推送通知?如何获得即时消息传递? <a href="#什么是推送通知如何获得即时消息传递" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Push Notifications are sent by Apple and Google “Push services” to a user’s device
|
||||
so that an inactive Delta Chat app can fetch messages in the background
|
||||
and show notifications on a user’s phone if needed.</p>
|
||||
<p>推送通知由 Apple 和 Google 的“推送服务”发送到用户的设备,以便非活动状态的 Delta Chat 应用可以在后台获取消息,并在需要时在用户的手机上显示通知。</p>
|
||||
|
||||
<p>Push Notifications work with all <a href="https://delta.chat/chatmail">chatmail</a> servers on</p>
|
||||
<p>推送通知适用于以下所有 <a href="https://delta.chat/chatmail">chatmail</a> 服务器:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>iOS devices, by integrating with Apple Push services.</p>
|
||||
<p>iOS 设备通过与 Apple Push 服务集成。</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Android devices, by integrating with the Google FCM Push service,
|
||||
including on devices that use <a href="https://microg.org">microG</a>
|
||||
instead of proprietary Google code on the phone.</p>
|
||||
<p>Android 设备,通过与 Google FCM Push 服务集成,
|
||||
包括使用 <a href="https://microg.org">microG</a>
|
||||
而不是手机上专有 Google 代码的设备。</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>As of May 2024, classic e-mail servers do not support Push Notifications
|
||||
for Delta Chat users.</p>
|
||||
<p>自 2024 年 5 月起,传统电子邮件服务器不再支持 Delta Chat 用户的推送通知。</p>
|
||||
|
||||
<h3 id="are-push-notifications-enabled-on-ios-devices-is-there-an-alternative">
|
||||
<h3 id="ios-设备上是否启用了推送通知我还有其他的选择吗">
|
||||
|
||||
|
||||
Are Push Notifications enabled on iOS devices? Is there an alternative? <a href="#are-push-notifications-enabled-on-ios-devices-is-there-an-alternative" class="anchor"></a>
|
||||
iOS 设备上是否启用了推送通知?我还有其他的选择吗? <a href="#ios-设备上是否启用了推送通知我还有其他的选择吗" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Yes, Delta Chat automatically uses Push Notifications for <a href="https://delta.chat/chatmail">chatmail</a> profiles.
|
||||
And no, there is no alternative on Apple’s phones to achieve instant message delivery
|
||||
because Apple devices do not allow Delta Chat to fetch data in the background.
|
||||
Push notifications are automatically activated for iOS users because
|
||||
<a href="#privacy-notifications">Delta Chat’s privacy-preserving Push Notification system</a>
|
||||
does not expose data to Apple that it doesn’t already have.</p>
|
||||
<p>是的,Delta Chat 会自动使用推送通知来接收 <a href="https://delta.chat/chatmail">chatmail</a> 个人资料。
|
||||
而且,Apple 手机上没有其他方式可以实现即时消息传递
|
||||
因为 Apple 设备不允许 Delta Chat 在后台获取数据。
|
||||
推送通知会自动为 iOS 用户激活,因为
|
||||
<a href="#privacy-notifications">Delta Chat 的隐私保护推送通知系统</a>
|
||||
不会向 Apple 泄露其尚未拥有的数据。</p>
|
||||
|
||||
<h3 id="android-push">
|
||||
<h3 id="android-设备上是否启用需要推送通知android-push">
|
||||
|
||||
|
||||
Are Push notifications enabled / needed on Android devices? <a href="#android-push" class="anchor"></a>
|
||||
Android 设备上是否启用/需要推送通知?{#android-push} <a href="#android-设备上是否启用需要推送通知android-push" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>If a “Push Service” is available, Delta Chat enables Push Notifications
|
||||
to achieve instant message delivery for all chatmail users.
|
||||
If you are using a classic e-mail provider instead of <a href="https://delta.chat/chatmail">chatmail</a> servers,
|
||||
Push Notifications are not available.</p>
|
||||
<p>如果有“推送服务”可用,Delta Chat 将启用推送通知,
|
||||
以实现所有聊天邮件用户的即时消息传递。
|
||||
如果您使用的是传统电子邮件提供商而不是 <a href="https://delta.chat/chatmail">chatmail</a> 服务器,则无法使用推送通知。</p>
|
||||
|
||||
<p>In the Delta Chat “Notifications” settings for “Instant delivery”
|
||||
you can change the following settings effecting all chat profiles:</p>
|
||||
<p>在 Delta Chat“通知”的“推送通知”设置中,您可以更改以下影响所有聊天配置文件的设置:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
@@ -710,17 +704,13 @@ both sides will eventually see a (group or direct) chat with a green checkmark
|
||||
next to the title.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If one of the devices is offline, the green checkmarks will only
|
||||
appear later when the device is internet-connected again
|
||||
and the Secure-Join network protocol completed.</p>
|
||||
<p>如果其中一个设备处于离线状态,则只有当设备再次连接互联网且安全加入网络协议完成时,绿色复选标记才会出现。</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Congratulations!
|
||||
You now will automatically use guaranteed end-to-end encryption
|
||||
with this contact and both of you can add each other to green-checkmarked groups
|
||||
<img style="vertical-align:middle; width:1.5em; margin:1px" src="../green-checkmark.png" alt="green checkmark" />,
|
||||
thereby automatically spreading guaranteed end-to-end encryption among its members.</p>
|
||||
<p>恭喜!
|
||||
从现在开始您将会自动使用安全的端到端加密与此联系人联系,并且双方都可以将对方添加到绿色复选标记的群组
|
||||
<img style="vertical-align:middle; width:1.5em; margin:1px" src="../green-checkmark.png" alt="green checkmark" />,从而自动在其成员之间传播有保障的端到端加密。</p>
|
||||
|
||||
<h3 id="e2eeguarantee">
|
||||
|
||||
@@ -891,19 +881,18 @@ only one part of “user outcomes”,
|
||||
see also our answers to <a href="#device-seizure">device-seizure</a>
|
||||
and <a href="#message-metadata">message-metadata</a> questions.</p>
|
||||
|
||||
<h3 id="is-delta-chat-vulnerable-to-efail">
|
||||
<h3 id="delta-chat-是否容易受到-efail-攻击">
|
||||
|
||||
|
||||
Is Delta Chat vulnerable to EFAIL? <a href="#is-delta-chat-vulnerable-to-efail" class="anchor"></a>
|
||||
Delta Chat 是否容易受到 EFAIL 攻击? <a href="#delta-chat-是否容易受到-efail-攻击" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, <a href="https://delta.chat/en/2018-05-15-delta-chat-not-vulnerable-to-efail">Delta Chat never was vulnerable to EFAIL</a>
|
||||
because its OpenPGP implementation <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
uses Modification Detection Code when encrypting messages
|
||||
and returns <a href="https://docs.rs/pgp/latest/pgp/errors/enum.Error.html#variant.MdcError">an error</a>
|
||||
if the Modification Detection Code is incorrect.</p>
|
||||
<p>不,<a href="https://delta.chat/en/2018-05-15-delta-chat-not-vulnerable-to-efail">Delta Chat 从未受到任何 EFAIL 攻击</a>
|
||||
因为所使用的 OpenPGP 实现了 <a href="https://github.com/rpgp/rpgp">rPGP</a>
|
||||
在加密消息时“修改检测代码”
|
||||
并且如果“修改检测代码”不正确则返回 <a href="https://docs.rs/pgp/latest/pgp/errors/enum.Error.html#variant.MdcError">错误</a>。</p>
|
||||
|
||||
<p>Delta Chat also never was vulnerable to the “Direct Exfiltration” EFAIL attack
|
||||
because it only decrypts <code class="language-plaintext highlighter-rouge">multipart/encrypted</code> messages
|
||||
@@ -1072,21 +1061,21 @@ you can also turn on <a href="#delold">“delete messages from server automatica
|
||||
<h3 id="pfs">
|
||||
|
||||
|
||||
Does Delta Chat support Perfect Forward Secrecy? <a href="#pfs" class="anchor"></a>
|
||||
Delta Chat 是否支持完美前向保密? <a href="#pfs" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>No, Delta Chat doesn’t support Perfect Forward Secrecy (PFS).
|
||||
This means that if your Delta Chat private decryption key is leaked,
|
||||
and someone has collected your prior in-transit messages,
|
||||
they will be able to decrypt and read them using the leaked decryption key.</p>
|
||||
<p>不,Delta Chat 不支持完美前向保密 (PFS)。
|
||||
这意味着,如果您的 Delta Chat 私人解密密钥泄露,
|
||||
并且有人收集了您之前传输的消息,
|
||||
他们将能够使用泄露的解密密钥解密并阅读这些消息。</p>
|
||||
|
||||
<p>Note, however, that if anyone obtains to your decryption keys,
|
||||
they will typically also be able to obtain your messages,
|
||||
irrespective if Perfect Forward Secrecy is in place or not.
|
||||
The typical real-world situation for leaked decryption keys is device seizure
|
||||
which we discuss in our answer <a href="#device-seizure">on metadata and device seizure</a>.</p>
|
||||
<p>但请注意,如果有人获得您的解密密钥,
|
||||
他们通常也能够获得您的消息,
|
||||
无论是否实施了完全前向保密。
|
||||
泄露解密密钥的典型现实情况是设备扣押,
|
||||
我们在<a href="#device-seizure">关于元数据和设备扣押</a>的回答中讨论了这一点。</p>
|
||||
|
||||
<p>It is possible that Delta Chat evolves to support Perfect Forward Secrecy,
|
||||
because OpenPGP is just a container for encrypted messages
|
||||
@@ -1272,6 +1261,12 @@ One device is not needed for the other to work.</p>
|
||||
and make sure, <strong>Private Network</strong> is selected as “Network profile type”
|
||||
(after transfer, you can change back to the original value)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On <strong>iOS</strong>, make sure “System Settings / Apps / Delta Chat / <strong>Local Network</strong>” access is granted</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On <strong>macOS</strong>, enable “System Settings / Privacy & Security / <strong>Local Network</strong> / Delta Chat”</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Your system might have a “personal firewall”,
|
||||
which is known to cause problems (especially on Windows).
|
||||
@@ -1384,6 +1379,7 @@ you only open attachments from senders you trust, and not from spammers.</li>
|
||||
<ul>
|
||||
<li>In general, anyone can share webxdc apps with each
|
||||
other without restrictions.</li>
|
||||
<li>From <a href="https://webxdc.org/apps/">webxdc.org/apps</a>.</li>
|
||||
<li>You can <a href="https://delta.chat/en/2023-08-11-xstore">send ‘hi’ to xstore@testrun.org</a>
|
||||
to see an experimental webxdc appstore.
|
||||
All of the apps are open source and for free.</li>
|
||||
@@ -1794,6 +1790,35 @@ Otherwise you might receive undecryptable messages from those group chats.</p>
|
||||
<li>请参阅 <a href="https://github.com/deltachat/deltachat-core-rust/blob/master/standards.md#standards-used-in-delta-chat">Delta Chat 中使用的标准</a>。</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="where-can-my-friends-find-delta-chat">
|
||||
|
||||
|
||||
Where can my friends find Delta Chat? <a href="#where-can-my-friends-find-delta-chat" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Delta Chat is available for all major and some minor platforms:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>The <strong>official website</strong>, <a href="https://delta.chat/download">https://delta.chat/download</a> shows all options in detail</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If unavailable, use the <strong>mirror</strong> at <a href="https://deltachat.github.io/deltachat-pages">https://deltachat.github.io/deltachat-pages</a></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Open one of the following <strong>app stores and search for “Delta Chat”:</strong>
|
||||
Google Play Store, F-Droid, Huawei App Gallery, Amazon App Store, iOS and macOS App Store, Microsoft Store</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Check the <strong>package manager</strong> of your Linux distributions</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Android APKs</strong> are also available on <a href="https://github.com/deltachat/deltachat-android/releases">https://github.com/deltachat/deltachat-android/releases</a></p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="delta-chat-的开发是如何被资助的">
|
||||
|
||||
|
||||
|
||||
@@ -2,16 +2,14 @@ package com.b44t.messenger;
|
||||
|
||||
public class DcContext {
|
||||
|
||||
public final static int DC_PREF_DEFAULT_MDNS_ENABLED = 1;
|
||||
public final static int DC_PREF_DEFAULT_TRIM_ENABLED = 0;
|
||||
public final static int DC_PREF_DEFAULT_TRIM_LENGTH = 500;
|
||||
|
||||
public final static int DC_EVENT_INFO = 100;
|
||||
public final static int DC_EVENT_WARNING = 300;
|
||||
public final static int DC_EVENT_ERROR = 400;
|
||||
public final static int DC_EVENT_ERROR_SELF_NOT_IN_GROUP = 410;
|
||||
public final static int DC_EVENT_MSGS_CHANGED = 2000;
|
||||
public final static int DC_EVENT_REACTIONS_CHANGED = 2001;
|
||||
public final static int DC_EVENT_INCOMING_REACTION = 2002;
|
||||
public final static int DC_EVENT_INCOMING_WEBXDC_NOTIFY = 2003;
|
||||
public final static int DC_EVENT_INCOMING_MSG = 2005;
|
||||
public final static int DC_EVENT_MSGS_NOTICED = 2008;
|
||||
public final static int DC_EVENT_MSG_DELIVERED = 2010;
|
||||
@@ -82,16 +80,13 @@ public class DcContext {
|
||||
public final static int DC_MEDIA_QUALITY_BALANCED = 0;
|
||||
public final static int DC_MEDIA_QUALITY_WORSE = 1;
|
||||
|
||||
public final static int DC_DECISION_START_CHAT = 0;
|
||||
public final static int DC_DECISION_BLOCK = 1;
|
||||
public final static int DC_DECISION_NOT_NOW = 2;
|
||||
|
||||
public final static int DC_CONNECTIVITY_NOT_CONNECTED = 1000;
|
||||
public final static int DC_CONNECTIVITY_CONNECTING = 2000;
|
||||
public final static int DC_CONNECTIVITY_WORKING = 3000;
|
||||
public final static int DC_CONNECTIVITY_CONNECTED = 4000;
|
||||
|
||||
private static final String CONFIG_ACCOUNT_ENABLED = "ui.enabled";
|
||||
private static final String CONFIG_MENTION_NOTIF_ENABLED = "ui.notify_mentions";
|
||||
|
||||
// when using DcAccounts, use DcAccounts.addAccount() instead
|
||||
public DcContext(String osName, String dbfile) {
|
||||
@@ -204,7 +199,7 @@ public class DcContext {
|
||||
public native int sendMsg (int chat_id, DcMsg msg);
|
||||
public native int sendTextMsg (int chat_id, String text);
|
||||
public native int sendVideochatInvitation(int chat_id);
|
||||
public native boolean sendWebxdcStatusUpdate(int msg_id, String payload, String descr);
|
||||
public native boolean sendWebxdcStatusUpdate(int msg_id, String payload);
|
||||
public native String getWebxdcStatusUpdates(int msg_id, int last_known_serial);
|
||||
public native void setWebxdcIntegration (String file);
|
||||
public native int initWebxdcIntegration(int chat_id);
|
||||
@@ -232,22 +227,12 @@ public class DcContext {
|
||||
}
|
||||
}
|
||||
|
||||
public String getNameNAddr() {
|
||||
String displayname = getConfig("displayname");
|
||||
String addr = getConfig("addr");
|
||||
String ret = "";
|
||||
public boolean isMentionsEnabled() {
|
||||
return !"0".equals(getConfig(CONFIG_MENTION_NOTIF_ENABLED));
|
||||
}
|
||||
|
||||
if (!displayname.isEmpty() && !addr.isEmpty()) {
|
||||
ret = String.format("%s (%s)", displayname, addr);
|
||||
} else if (!addr.isEmpty()) {
|
||||
ret = addr;
|
||||
}
|
||||
|
||||
if (ret.isEmpty() || isConfigured() == 0) {
|
||||
ret += " (not configured)";
|
||||
}
|
||||
|
||||
return ret.trim();
|
||||
public void setMentionsEnabled(boolean enabled) {
|
||||
setConfigInt(CONFIG_MENTION_NOTIF_ENABLED, enabled? 1 : 0);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
|
||||
@@ -148,6 +148,7 @@ public class DcMsg {
|
||||
return new JSONObject();
|
||||
}
|
||||
}
|
||||
public native String getWebxdcHref ();
|
||||
public native boolean isForwarded ();
|
||||
public native boolean isInfo ();
|
||||
public native boolean isSetupMessage ();
|
||||
@@ -170,10 +171,10 @@ public class DcMsg {
|
||||
public native boolean isOutgoing();
|
||||
public native int getSenderColor();
|
||||
|
||||
public String getSenderName(DcContact dcContact, boolean markOverride) {
|
||||
public String getSenderName(DcContact dcContact) {
|
||||
String overrideName = getOverrideSenderName();
|
||||
if (overrideName != null) {
|
||||
return (markOverride ? "~" : "") + overrideName;
|
||||
return "~" + overrideName;
|
||||
} else {
|
||||
return dcContact.getDisplayName();
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ import com.b44t.messenger.DcEvent;
|
||||
import com.b44t.messenger.DcEventEmitter;
|
||||
import com.b44t.messenger.rpc.Rpc;
|
||||
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiProvider;
|
||||
import org.thoughtcrime.securesms.connect.AccountManager;
|
||||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
@@ -37,7 +36,6 @@ import org.thoughtcrime.securesms.connect.KeepAliveService;
|
||||
import org.thoughtcrime.securesms.connect.NetworkStateReceiver;
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
|
||||
import org.thoughtcrime.securesms.crypto.PRNGFixes;
|
||||
import org.thoughtcrime.securesms.geolocation.DcLocationManager;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.notifications.FcmReceiveService;
|
||||
@@ -107,11 +105,6 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
|
||||
Log.i("DeltaChat", "++++++++++++++++++ ApplicationContext.onCreate() ++++++++++++++++++");
|
||||
|
||||
// The first call to `getInstance` takes about 100ms-300ms, so, do it on a background thread
|
||||
Thread t = new Thread(() -> EmojiProvider.getInstance(this), "InitEmojiProviderThread");
|
||||
t.setPriority(Thread.MIN_PRIORITY);
|
||||
t.start();
|
||||
|
||||
System.loadLibrary("native-utils");
|
||||
|
||||
dcAccounts = new DcAccounts(new File(getFilesDir(), "accounts").getAbsolutePath());
|
||||
@@ -209,7 +202,6 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
|
||||
KeepAliveService.maybeStartSelf(this);
|
||||
|
||||
initializeRandomNumberFix();
|
||||
initializeLogging();
|
||||
initializeJobManager();
|
||||
InChatSounds.getInstance(this);
|
||||
@@ -258,10 +250,6 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
return jobManager;
|
||||
}
|
||||
|
||||
private void initializeRandomNumberFix() {
|
||||
PRNGFixes.apply();
|
||||
}
|
||||
|
||||
private void initializeLogging() {
|
||||
SignalProtocolLoggerProvider.setProvider(new AndroidSignalProtocolLogger());
|
||||
}
|
||||
|
||||
@@ -156,10 +156,6 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
this.findPreference(PREFERENCE_CATEGORY_HELP)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_HELP));
|
||||
|
||||
if (VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
tintIcons(getActivity());
|
||||
}
|
||||
|
||||
DcHelper.getEventCenter(getActivity()).addObserver(DcContext.DC_EVENT_CONNECTIVITY_CHANGED, this);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,9 +33,6 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
onPreCreate();
|
||||
if (BaseActivity.isMenuWorkaroundRequired()) {
|
||||
forceOverflowMenu();
|
||||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@@ -46,20 +43,6 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
return (keyCode == KeyEvent.KEYCODE_MENU && BaseActivity.isMenuWorkaroundRequired()) || super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_MENU && BaseActivity.isMenuWorkaroundRequired()) {
|
||||
openOptionsMenu();
|
||||
return true;
|
||||
}
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
private void initializeScreenshotSecurity() {
|
||||
if (Prefs.isScreenSecurityEnabled(this)) {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import android.view.KeyEvent;
|
||||
|
||||
public abstract class BaseActivity extends FragmentActivity {
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
return (keyCode == KeyEvent.KEYCODE_MENU && isMenuWorkaroundRequired()) || super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_MENU && isMenuWorkaroundRequired()) {
|
||||
openOptionsMenu();
|
||||
return true;
|
||||
}
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
public static boolean isMenuWorkaroundRequired() {
|
||||
return VERSION.SDK_INT < VERSION_CODES.KITKAT && ("LGE".equalsIgnoreCase(Build.MANUFACTURER) || "E6710".equalsIgnoreCase(Build.DEVICE));
|
||||
}
|
||||
}
|
||||
@@ -364,9 +364,7 @@ public abstract class BaseConversationListFragment extends Fragment implements A
|
||||
|
||||
mode.setTitle("1");
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
requireActivity().getWindow().setStatusBarColor(getResources().getColor(R.color.action_mode_status_bar));
|
||||
}
|
||||
requireActivity().getWindow().setStatusBarColor(getResources().getColor(R.color.action_mode_status_bar));
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -395,11 +393,9 @@ public abstract class BaseConversationListFragment extends Fragment implements A
|
||||
actionMode = null;
|
||||
getListAdapter().initializeBatchMode(false);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
TypedArray color = requireActivity().getTheme().obtainStyledAttributes(new int[] {android.R.attr.statusBarColor});
|
||||
requireActivity().getWindow().setStatusBarColor(color.getColor(0, Color.BLACK));
|
||||
color.recycle();
|
||||
}
|
||||
TypedArray color = requireActivity().getTheme().obtainStyledAttributes(new int[]{android.R.attr.statusBarColor});
|
||||
requireActivity().getWindow().setStatusBarColor(color.getColor(0, Color.BLACK));
|
||||
color.recycle();
|
||||
|
||||
Context context = getContext();
|
||||
if (context != null) {
|
||||
|
||||
@@ -136,9 +136,7 @@ public class ContactSelectionListFragment extends Fragment
|
||||
public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
|
||||
MenuInflater inflater = getActivity().getMenuInflater();
|
||||
inflater.inflate(R.menu.contact_list, menu);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
getActivity().getWindow().setStatusBarColor(getResources().getColor(R.color.action_mode_status_bar));
|
||||
}
|
||||
getActivity().getWindow().setStatusBarColor(getResources().getColor(R.color.action_mode_status_bar));
|
||||
setCorrectMenuVisibility(menu);
|
||||
actionMode.setTitle("1");
|
||||
return true;
|
||||
@@ -170,11 +168,9 @@ public class ContactSelectionListFragment extends Fragment
|
||||
ContactSelectionListFragment.this.actionMode = null;
|
||||
getContactSelectionListAdapter().resetActionModeSelection();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
TypedArray color = getActivity().getTheme().obtainStyledAttributes(new int[] {android.R.attr.statusBarColor});
|
||||
getActivity().getWindow().setStatusBarColor(color.getColor(0, Color.BLACK));
|
||||
color.recycle();
|
||||
}
|
||||
TypedArray color = getActivity().getTheme().obtainStyledAttributes(new int[]{android.R.attr.statusBarColor});
|
||||
getActivity().getWindow().setStatusBarColor(color.getColor(0, Color.BLACK));
|
||||
color.recycle();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -89,8 +89,6 @@ import org.thoughtcrime.securesms.components.InputPanel;
|
||||
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardShownListener;
|
||||
import org.thoughtcrime.securesms.components.ScaleStableImageView;
|
||||
import org.thoughtcrime.securesms.components.SendButton;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiProvider;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.connect.AccountManager;
|
||||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||
@@ -710,7 +708,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
DcMsg draft = dcContext.getDraft(chatId);
|
||||
final String sharedText = RelayUtil.getSharedText(this);
|
||||
|
||||
if (draft == null) {
|
||||
if (!draft.isOk()) {
|
||||
if (TextUtils.isEmpty(sharedText)) {
|
||||
composeText.setText("");
|
||||
future.set(false);
|
||||
@@ -931,12 +929,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
case AttachmentTypeSelector.TAKE_PHOTO:
|
||||
attachmentManager.capturePhoto(this, TAKE_PHOTO); break;
|
||||
case AttachmentTypeSelector.RECORD_VIDEO:
|
||||
if(VideoRecoder.canRecode()) {
|
||||
attachmentManager.captureVideo(this, RECORD_VIDEO);
|
||||
}
|
||||
else {
|
||||
Toast.makeText(this, "This device does not support video-compression (requires Android 4.4 KitKat)", Toast.LENGTH_LONG).show();
|
||||
}
|
||||
attachmentManager.captureVideo(this, RECORD_VIDEO);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1238,7 +1231,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
@Override
|
||||
public void onEmojiToggle() {
|
||||
if (!emojiDrawerStub.resolved()) {
|
||||
initializeMediaKeyboardProviders(emojiDrawerStub.get(), false);
|
||||
inputPanel.setMediaKeyboard(emojiDrawerStub.get());
|
||||
}
|
||||
|
||||
@@ -1277,13 +1269,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
dcContext.sendMsg(chatId, msg);
|
||||
}
|
||||
|
||||
private void initializeMediaKeyboardProviders(@NonNull MediaKeyboard mediaKeyboard, boolean stickersAvailable) {
|
||||
boolean isSystemEmojiPreferred = Prefs.isSystemEmojiPreferred(this);
|
||||
if (!isSystemEmojiPreferred) {
|
||||
mediaKeyboard.setProviders(0, new EmojiKeyboardProvider(this, inputPanel));
|
||||
}
|
||||
}
|
||||
|
||||
// Listeners
|
||||
|
||||
private class AttachmentTypeListener implements AttachmentTypeSelector.AttachmentClickedListener {
|
||||
|
||||
@@ -24,7 +24,6 @@ import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
@@ -442,7 +441,7 @@ public class ConversationFragment extends MessageSelectorFragment
|
||||
|
||||
if (msg.getFromId() != prevMsg.getFromId() && !singleMsg) {
|
||||
DcContact contact = dcContext.getContact(msg.getFromId());
|
||||
result.append(msg.getSenderName(contact, false)).append(":\n");
|
||||
result.append(msg.getSenderName(contact)).append(":\n");
|
||||
}
|
||||
if (msg.getType() == DcMsg.DC_MSG_TEXT || (singleMsg && !msg.getText().isEmpty())) {
|
||||
result.append(msg.getText());
|
||||
@@ -810,7 +809,7 @@ public class ConversationFragment extends MessageSelectorFragment
|
||||
DozeReminder.dozeReminderTapped(getContext());
|
||||
}
|
||||
else if(messageRecord.getInfoType() == DcMsg.DC_INFO_WEBXDC_INFO_MESSAGE) {
|
||||
scrollMaybeSmoothToMsgId(messageRecord.getParent().getId());
|
||||
WebxdcActivity.openWebxdcActivity(getContext(), messageRecord.getParent(), messageRecord.getWebxdcHref());
|
||||
}
|
||||
else {
|
||||
String self_mail = dcContext.getConfig("configured_mail_user");
|
||||
@@ -911,11 +910,9 @@ public class ConversationFragment extends MessageSelectorFragment
|
||||
|
||||
mode.setTitle("1");
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
Window window = getActivity().getWindow();
|
||||
statusBarColor = window.getStatusBarColor();
|
||||
window.setStatusBarColor(getResources().getColor(R.color.action_mode_status_bar));
|
||||
}
|
||||
Window window = getActivity().getWindow();
|
||||
statusBarColor = window.getStatusBarColor();
|
||||
window.setStatusBarColor(getResources().getColor(R.color.action_mode_status_bar));
|
||||
|
||||
setCorrectMenuVisibility(menu);
|
||||
ConversationAdaptiveActionsToolbar.adjustMenuActions(menu, 10, requireActivity().getWindow().getDecorView().getMeasuredWidth());
|
||||
@@ -932,9 +929,7 @@ public class ConversationFragment extends MessageSelectorFragment
|
||||
((ConversationAdapter)list.getAdapter()).clearSelection();
|
||||
list.getAdapter().notifyDataSetChanged();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
getActivity().getWindow().setStatusBarColor(statusBarColor);
|
||||
}
|
||||
getActivity().getWindow().setStatusBarColor(statusBarColor);
|
||||
|
||||
actionMode = null;
|
||||
hideAddReactionView();
|
||||
|
||||
@@ -22,12 +22,10 @@ import android.content.res.TypedArray;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.text.Spannable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
@@ -56,7 +54,6 @@ import org.thoughtcrime.securesms.components.DocumentView;
|
||||
import org.thoughtcrime.securesms.components.QuoteView;
|
||||
import org.thoughtcrime.securesms.components.VcardView;
|
||||
import org.thoughtcrime.securesms.components.WebxdcView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.mms.DocumentSlide;
|
||||
@@ -69,10 +66,10 @@ import org.thoughtcrime.securesms.mms.StickerSlide;
|
||||
import org.thoughtcrime.securesms.mms.VcardSlide;
|
||||
import org.thoughtcrime.securesms.reactions.ReactionsConversationView;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.Linkifier;
|
||||
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
|
||||
import org.thoughtcrime.securesms.util.MarkdownUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Prefs;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.views.Stub;
|
||||
@@ -112,6 +109,7 @@ public class ConversationItem extends BaseConversationItem
|
||||
protected ViewGroup contactPhotoHolder;
|
||||
private ViewGroup container;
|
||||
private Button msgActionButton;
|
||||
private Button showFullButton;
|
||||
|
||||
private @NonNull Stub<ConversationItemThumbnail> mediaThumbnailStub;
|
||||
private @NonNull Stub<AudioView> audioViewStub;
|
||||
@@ -158,6 +156,7 @@ public class ConversationItem extends BaseConversationItem
|
||||
this.container = findViewById(R.id.container);
|
||||
this.replyView = findViewById(R.id.reply_icon);
|
||||
this.msgActionButton = findViewById(R.id.msg_action_button);
|
||||
this.showFullButton = findViewById(R.id.show_full_button);
|
||||
|
||||
setOnClickListener(new ClickListener(null));
|
||||
|
||||
@@ -385,7 +384,6 @@ public class ConversationItem extends BaseConversationItem
|
||||
private void setBodyText(DcMsg messageRecord) {
|
||||
bodyText.setClickable(false);
|
||||
bodyText.setFocusable(false);
|
||||
bodyText.setTextSize(TypedValue.COMPLEX_UNIT_SP, Prefs.getMessageBodyTextSize(context));
|
||||
|
||||
String text = messageRecord.getText();
|
||||
|
||||
@@ -399,7 +397,7 @@ public class ConversationItem extends BaseConversationItem
|
||||
else {
|
||||
Spannable spannable = (Spannable) MarkdownUtil.toMarkdown(context, text);
|
||||
if (batchSelected.isEmpty()) {
|
||||
spannable = EmojiTextView.linkify(spannable);
|
||||
spannable = Linkifier.linkify(spannable);
|
||||
}
|
||||
bodyText.setText(spannable);
|
||||
bodyText.setVisibility(View.VISIBLE);
|
||||
@@ -407,6 +405,7 @@ public class ConversationItem extends BaseConversationItem
|
||||
|
||||
int downloadState = messageRecord.getDownloadState();
|
||||
if (downloadState == DcMsg.DC_DOWNLOAD_AVAILABLE || downloadState == DcMsg.DC_DOWNLOAD_FAILURE || downloadState == DcMsg.DC_DOWNLOAD_IN_PROGRESS) {
|
||||
showFullButton.setVisibility(View.GONE);
|
||||
msgActionButton.setVisibility(View.VISIBLE);
|
||||
if (downloadState==DcMsg.DC_DOWNLOAD_IN_PROGRESS) {
|
||||
msgActionButton.setEnabled(false);
|
||||
@@ -427,6 +426,7 @@ public class ConversationItem extends BaseConversationItem
|
||||
}
|
||||
});
|
||||
} else if (messageRecord.getType() == DcMsg.DC_MSG_WEBXDC) {
|
||||
showFullButton.setVisibility(View.GONE);
|
||||
msgActionButton.setVisibility(View.VISIBLE);
|
||||
msgActionButton.setEnabled(true);
|
||||
msgActionButton.setText(webxdcViewStub.get().isCommunity()? R.string.join: R.string.start_app);
|
||||
@@ -439,10 +439,11 @@ public class ConversationItem extends BaseConversationItem
|
||||
});
|
||||
}
|
||||
else if (messageRecord.hasHtml()) {
|
||||
msgActionButton.setVisibility(View.VISIBLE);
|
||||
msgActionButton.setEnabled(true);
|
||||
msgActionButton.setText(R.string.show_full_message);
|
||||
msgActionButton.setOnClickListener(view -> {
|
||||
msgActionButton.setVisibility(View.GONE);
|
||||
showFullButton.setVisibility(View.VISIBLE);
|
||||
showFullButton.setEnabled(true);
|
||||
showFullButton.setText(R.string.show_full_message);
|
||||
showFullButton.setOnClickListener(view -> {
|
||||
if (eventListener != null && batchSelected.isEmpty()) {
|
||||
eventListener.onShowFullClicked(messageRecord);
|
||||
} else {
|
||||
@@ -451,6 +452,7 @@ public class ConversationItem extends BaseConversationItem
|
||||
});
|
||||
} else {
|
||||
msgActionButton.setVisibility(View.GONE);
|
||||
showFullButton.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,9 +494,7 @@ public class ConversationItem extends BaseConversationItem
|
||||
audioViewStub.get().setAudio(new AudioSlide(context, messageRecord), duration);
|
||||
audioViewStub.get().setOnClickListener(passthroughClickListener);
|
||||
audioViewStub.get().setOnLongClickListener(passthroughClickListener);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
audioViewStub.get().setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
|
||||
}
|
||||
audioViewStub.get().setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
|
||||
|
||||
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
@@ -512,9 +512,7 @@ public class ConversationItem extends BaseConversationItem
|
||||
documentViewStub.get().setDocument(new DocumentSlide(context, messageRecord));
|
||||
documentViewStub.get().setDocumentClickListener(new ThumbnailClickListener());
|
||||
documentViewStub.get().setOnLongClickListener(passthroughClickListener);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
documentViewStub.get().setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
|
||||
}
|
||||
documentViewStub.get().setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
|
||||
|
||||
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
@@ -531,9 +529,7 @@ public class ConversationItem extends BaseConversationItem
|
||||
webxdcViewStub.get().setWebxdc(messageRecord, context.getString(R.string.webxdc_app));
|
||||
webxdcViewStub.get().setWebxdcClickListener(new ThumbnailClickListener());
|
||||
webxdcViewStub.get().setOnLongClickListener(passthroughClickListener);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
webxdcViewStub.get().setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
|
||||
}
|
||||
webxdcViewStub.get().setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
|
||||
|
||||
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
@@ -551,9 +547,7 @@ public class ConversationItem extends BaseConversationItem
|
||||
vcardViewStub.get().setVcardClickListener(new ThumbnailClickListener());
|
||||
vcardViewStub.get().setOnLongClickListener(passthroughClickListener);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
vcardViewStub.get().setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
|
||||
}
|
||||
vcardViewStub.get().setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
|
||||
|
||||
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
@@ -589,9 +583,7 @@ public class ConversationItem extends BaseConversationItem
|
||||
mediaThumbnailStub.get().setOnLongClickListener(passthroughClickListener);
|
||||
mediaThumbnailStub.get().setOnClickListener(passthroughClickListener);
|
||||
mediaThumbnailStub.get().showShade(TextUtils.isEmpty(messageRecord.getText()));
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
mediaThumbnailStub.get().setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
|
||||
}
|
||||
mediaThumbnailStub.get().setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
|
||||
|
||||
setThumbnailOutlineCorners(messageRecord, showSender);
|
||||
|
||||
@@ -614,9 +606,7 @@ public class ConversationItem extends BaseConversationItem
|
||||
stickerStub.get().setThumbnailClickListener(new StickerClickListener());
|
||||
stickerStub.get().setOnLongClickListener(passthroughClickListener);
|
||||
stickerStub.get().setOnClickListener(passthroughClickListener);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
stickerStub.get().setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
|
||||
}
|
||||
stickerStub.get().setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
|
||||
|
||||
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
@@ -677,7 +667,7 @@ public class ConversationItem extends BaseConversationItem
|
||||
contactPhoto.setVisibility(View.GONE);
|
||||
} else {
|
||||
int color = messageRecord.getSenderColor();
|
||||
Recipient recipient = new Recipient(context, dcContact, messageRecord.getSenderName(dcContact, !dcContext.isCommunity()), color);
|
||||
Recipient recipient = new Recipient(context, dcContact, messageRecord.getSenderName(dcContact), color);
|
||||
contactPhoto.setAvatar(glideRequests, recipient, true);
|
||||
contactPhoto.setVisibility(View.VISIBLE);
|
||||
}
|
||||
@@ -802,14 +792,14 @@ public class ConversationItem extends BaseConversationItem
|
||||
|
||||
if (messageRecord.isForwarded()) {
|
||||
if (showSender && dcContact !=null) {
|
||||
this.groupSender.setText(context.getString(R.string.forwarded_by, messageRecord.getSenderName(dcContact, false)));
|
||||
this.groupSender.setText(context.getString(R.string.forwarded_by, messageRecord.getSenderName(dcContact)));
|
||||
} else {
|
||||
this.groupSender.setText(context.getString(R.string.forwarded_message));
|
||||
}
|
||||
this.groupSender.setTextColor(context.getResources().getColor(R.color.unknown_sender));
|
||||
}
|
||||
else if (showSender && dcContact !=null) {
|
||||
this.groupSender.setText(messageRecord.getSenderName(dcContact, true));
|
||||
this.groupSender.setText(messageRecord.getSenderName(dcContact));
|
||||
int color = messageRecord.getSenderColor();
|
||||
this.groupSender.setTextColor(Util.rgbToArgbColor(color!=0? color : dcContact.getColor()));
|
||||
}
|
||||
|
||||
@@ -174,9 +174,7 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
|
||||
vibrate(viewHolder.itemView.getContext());
|
||||
}
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
recyclerView.cancelPendingInputEvents();
|
||||
}
|
||||
recyclerView.cancelPendingInputEvents();
|
||||
}
|
||||
|
||||
private static void resetProgress(RecyclerView.ViewHolder viewHolder) {
|
||||
@@ -199,11 +197,7 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
|
||||
}
|
||||
|
||||
private static float getSignFromDirection(@NonNull View view) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
return view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL ? -1f : 1f;
|
||||
} else {
|
||||
return 1f;
|
||||
}
|
||||
return view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL ? -1f : 1f;
|
||||
}
|
||||
|
||||
private static boolean sameSign(float dX, float sign) {
|
||||
|
||||
@@ -564,7 +564,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
private void shareInvite() {
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
String inviteURL = Util.QrDataToInviteURL(DcHelper.getContext(this).getSecurejoinQr(0));
|
||||
String inviteURL = DcHelper.getContext(this).getSecurejoinQr(0);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, getString(R.string.invite_friends_text, inviteURL));
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.chat_share_with_title)));
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ public class ConversationListItemInboxZero extends LinearLayout implements Binda
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
public ConversationListItemInboxZero(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
@@ -35,8 +34,6 @@ import com.soundcloud.android.crop.Crop;
|
||||
|
||||
import org.thoughtcrime.securesms.components.AvatarSelector;
|
||||
import org.thoughtcrime.securesms.components.InputAwareLayout;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.mms.AttachmentManager;
|
||||
@@ -54,7 +51,7 @@ import java.security.SecureRandom;
|
||||
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public class CreateProfileActivity extends BaseActionBarActivity implements EmojiKeyboardProvider.EmojiEventListener {
|
||||
public class CreateProfileActivity extends BaseActionBarActivity {
|
||||
|
||||
private static final String TAG = CreateProfileActivity.class.getSimpleName();
|
||||
|
||||
@@ -66,7 +63,6 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Emoj
|
||||
private ImageView avatar;
|
||||
private EditText name;
|
||||
private EditText overridenName;
|
||||
private MediaKeyboard emojiDrawer;
|
||||
private EditText statusView;
|
||||
|
||||
private boolean fromWelcome;
|
||||
@@ -91,7 +87,6 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Emoj
|
||||
attachmentManager = new AttachmentManager(this, () -> {});
|
||||
avatarChanged = false;
|
||||
initializeResources();
|
||||
initializeEmojiInput();
|
||||
initializeProfileName();
|
||||
initializeProfileAvatar();
|
||||
initializeStatusText();
|
||||
@@ -130,15 +125,6 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Emoj
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
||||
if (container.getCurrentInput() == emojiDrawer) {
|
||||
container.hideAttachedInput(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
@@ -206,7 +192,6 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Emoj
|
||||
this.avatar = ViewUtil.findById(this, R.id.avatar);
|
||||
this.name = ViewUtil.findById(this, R.id.name_text);
|
||||
this.overridenName = ViewUtil.findById(this, R.id.overriden_name);
|
||||
this.emojiDrawer = ViewUtil.findById(this, R.id.emoji_drawer);
|
||||
this.container = ViewUtil.findById(this, R.id.container);
|
||||
this.statusView = ViewUtil.findById(this, R.id.status_text);
|
||||
|
||||
@@ -261,33 +246,6 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Emoj
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onEmojiSelected(String emoji) {
|
||||
final int start = name.getSelectionStart();
|
||||
final int end = name.getSelectionEnd();
|
||||
|
||||
name.getText().replace(Math.min(start, end), Math.max(start, end), emoji);
|
||||
name.setSelection(start + emoji.length());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKeyEvent(KeyEvent keyEvent) {
|
||||
name.dispatchKeyEvent(keyEvent);
|
||||
}
|
||||
|
||||
private void initializeMediaKeyboardProviders(@NonNull MediaKeyboard mediaKeyboard) {
|
||||
boolean isSystemEmojiPreferred = Prefs.isSystemEmojiPreferred(this);
|
||||
if (!isSystemEmojiPreferred) {
|
||||
mediaKeyboard.setProviders(0, new EmojiKeyboardProvider(this, this));
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeEmojiInput() {
|
||||
initializeMediaKeyboardProviders(emojiDrawer);
|
||||
this.name.setOnClickListener(v -> container.showSoftkey(name));
|
||||
}
|
||||
|
||||
private void initializeStatusText() {
|
||||
String status = DcHelper.get(this, DcHelper.CONFIG_SELF_STATUS);
|
||||
statusView.setText(status);
|
||||
|
||||
@@ -246,16 +246,22 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
|
||||
final String displayName = name.getText().toString();
|
||||
DcHelper.set(this, DcHelper.CONFIG_DISPLAY_NAME, TextUtils.isEmpty(displayName)? null : displayName);
|
||||
// Save display name and avatar in the unconfigured profile.
|
||||
// If the currently selected profile is configured, then this means that rollbackAccountCreation()
|
||||
// was called (see handleOnBackPressed() above), i.e. the newly created profile was removed already
|
||||
// and we can't save the display name & avatar.
|
||||
if (DcHelper.getContext(this).isConfigured() == 0) {
|
||||
final String displayName = name.getText().toString();
|
||||
DcHelper.set(this, DcHelper.CONFIG_DISPLAY_NAME, TextUtils.isEmpty(displayName) ? null : displayName);
|
||||
|
||||
if (avatarChanged) {
|
||||
try {
|
||||
AvatarHelper.setSelfAvatar(InstantOnboardingActivity.this, avatarBmp);
|
||||
Prefs.setProfileAvatarId(InstantOnboardingActivity.this, new SecureRandom().nextInt());
|
||||
avatarChanged = false;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to save avatar", e);
|
||||
if (avatarChanged) {
|
||||
try {
|
||||
AvatarHelper.setSelfAvatar(InstantOnboardingActivity.this, avatarBmp);
|
||||
Prefs.setProfileAvatarId(InstantOnboardingActivity.this, new SecureRandom().nextInt());
|
||||
avatarChanged = false;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to save avatar", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,12 +197,11 @@ public class LogViewFragment extends Fragment {
|
||||
asMegs(info.maxMemory()));
|
||||
}
|
||||
|
||||
@TargetApi(VERSION_CODES.KITKAT)
|
||||
public static String getMemoryClass(Context context) {
|
||||
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
String lowMem = "";
|
||||
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.KITKAT && activityManager.isLowRamDevice()) {
|
||||
if (activityManager.isLowRamDevice()) {
|
||||
lowMem = ", low-mem device";
|
||||
}
|
||||
return activityManager.getMemoryClass() + lowMem;
|
||||
@@ -252,10 +251,8 @@ public class LogViewFragment extends Fragment {
|
||||
|
||||
Locale locale = Util.getLocale();
|
||||
builder.append("lang=").append(locale.toString()).append("\n");
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
boolean isRtl = Util.getLayoutDirection(context) == View.LAYOUT_DIRECTION_RTL;
|
||||
builder.append("rtl=").append(isRtl).append("\n");
|
||||
}
|
||||
boolean isRtl = Util.getLayoutDirection(context) == View.LAYOUT_DIRECTION_RTL;
|
||||
builder.append("rtl=").append(isRtl).append("\n");
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
boolean notifPermGranted = PermissionChecker.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PermissionChecker.PERMISSION_GRANTED;
|
||||
|
||||
@@ -229,11 +229,9 @@ public class ProfileDocumentsFragment
|
||||
mode.getMenuInflater().inflate(R.menu.profile_context, menu);
|
||||
mode.setTitle("1");
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
Window window = getActivity().getWindow();
|
||||
originalStatusBarColor = window.getStatusBarColor();
|
||||
window.setStatusBarColor(getResources().getColor(R.color.action_mode_status_bar));
|
||||
}
|
||||
Window window = getActivity().getWindow();
|
||||
originalStatusBarColor = window.getStatusBarColor();
|
||||
window.setStatusBarColor(getResources().getColor(R.color.action_mode_status_bar));
|
||||
setCorrectMenuVisibility(menu);
|
||||
return true;
|
||||
}
|
||||
@@ -283,9 +281,7 @@ public class ProfileDocumentsFragment
|
||||
actionMode = null;
|
||||
getListAdapter().clearSelection();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
getActivity().getWindow().setStatusBarColor(originalStatusBarColor);
|
||||
}
|
||||
getActivity().getWindow().setStatusBarColor(originalStatusBarColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,11 +219,9 @@ public class ProfileGalleryFragment
|
||||
mode.getMenuInflater().inflate(R.menu.profile_context, menu);
|
||||
mode.setTitle("1");
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
Window window = getActivity().getWindow();
|
||||
originalStatusBarColor = window.getStatusBarColor();
|
||||
window.setStatusBarColor(getResources().getColor(R.color.action_mode_status_bar));
|
||||
}
|
||||
Window window = getActivity().getWindow();
|
||||
originalStatusBarColor = window.getStatusBarColor();
|
||||
window.setStatusBarColor(getResources().getColor(R.color.action_mode_status_bar));
|
||||
setCorrectMenuVisibility(menu);
|
||||
return true;
|
||||
}
|
||||
@@ -269,9 +267,7 @@ public class ProfileGalleryFragment
|
||||
actionMode = null;
|
||||
getListAdapter().clearSelection();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
getActivity().getWindow().setStatusBarColor(originalStatusBarColor);
|
||||
}
|
||||
getActivity().getWindow().setStatusBarColor(originalStatusBarColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,11 +268,9 @@ public class ProfileSettingsFragment extends Fragment
|
||||
menu.findItem(R.id.menu_select_all).setVisible(false);
|
||||
mode.setTitle("1");
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
Window window = getActivity().getWindow();
|
||||
originalStatusBarColor = window.getStatusBarColor();
|
||||
window.setStatusBarColor(getResources().getColor(R.color.action_mode_status_bar));
|
||||
}
|
||||
Window window = getActivity().getWindow();
|
||||
originalStatusBarColor = window.getStatusBarColor();
|
||||
window.setStatusBarColor(getResources().getColor(R.color.action_mode_status_bar));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -314,9 +312,7 @@ public class ProfileSettingsFragment extends Fragment
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
actionMode = null;
|
||||
adapter.clearSelection();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
getActivity().getWindow().setStatusBarColor(originalStatusBarColor);
|
||||
}
|
||||
getActivity().getWindow().setStatusBarColor(originalStatusBarColor);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,14 @@ import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Linkifier;
|
||||
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
|
||||
|
||||
public class ProfileStatusItem extends LinearLayout {
|
||||
|
||||
private EmojiTextView statusTextView;
|
||||
private AppCompatTextView statusTextView;
|
||||
private final PassthroughClickListener passthroughClickListener = new PassthroughClickListener();
|
||||
|
||||
public ProfileStatusItem(Context context) {
|
||||
@@ -32,7 +34,7 @@ public class ProfileStatusItem extends LinearLayout {
|
||||
}
|
||||
|
||||
public void set(String status) {
|
||||
statusTextView.setText(EmojiTextView.linkify(new SpannableString(status)));
|
||||
statusTextView.setText(Linkifier.linkify(new SpannableString(status)));
|
||||
}
|
||||
|
||||
private class PassthroughClickListener implements View.OnLongClickListener, View.OnClickListener {
|
||||
|
||||
@@ -31,7 +31,6 @@ import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
@@ -59,6 +58,7 @@ import org.thoughtcrime.securesms.proxy.ProxySettingsActivity;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.IntentUtils;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.views.ProgressDialog;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
@@ -185,7 +185,7 @@ public class RegistrationActivity extends BaseActionBarActivity implements DcEve
|
||||
expandAdvanced = expandAdvanced || !TextUtils.isEmpty(strVal);
|
||||
|
||||
intVal = DcHelper.getInt(this, CONFIG_MAIL_SECURITY);
|
||||
imapSecurity.setSelection(intVal);
|
||||
imapSecurity.setSelection(ViewUtil.checkBounds(intVal, imapSecurity));
|
||||
expandAdvanced = expandAdvanced || intVal != 0;
|
||||
|
||||
TextInputEditText smtpLoginInput = findViewById(R.id.smtp_login_text);
|
||||
@@ -207,7 +207,7 @@ public class RegistrationActivity extends BaseActionBarActivity implements DcEve
|
||||
expandAdvanced = expandAdvanced || !TextUtils.isEmpty(strVal);
|
||||
|
||||
intVal = DcHelper.getInt(this, CONFIG_SEND_SECURITY);
|
||||
smtpSecurity.setSelection(intVal);
|
||||
smtpSecurity.setSelection(ViewUtil.checkBounds(intVal, smtpSecurity));
|
||||
expandAdvanced = expandAdvanced || intVal != 0;
|
||||
|
||||
int serverFlags = DcHelper.getInt(this, CONFIG_SERVER_FLAGS);
|
||||
@@ -222,12 +222,15 @@ public class RegistrationActivity extends BaseActionBarActivity implements DcEve
|
||||
}
|
||||
// /remove gmail oauth2
|
||||
}
|
||||
authMethod.setSelection(sel);
|
||||
authMethod.setSelection(ViewUtil.checkBounds(sel, authMethod));
|
||||
expandAdvanced = expandAdvanced || sel != 0;
|
||||
|
||||
int certCheckFlags = DcHelper.getInt(this, "imap_certificate_checks");
|
||||
certCheck.setSelection(certCheckFlags);
|
||||
expandAdvanced = expandAdvanced || certCheckFlags != 0;
|
||||
int imapCertificateChecks = DcHelper.getInt(this, "imap_certificate_checks");
|
||||
if (imapCertificateChecks == 3) {
|
||||
imapCertificateChecks = 2; // 3 is a deprecated alias for 2
|
||||
}
|
||||
certCheck.setSelection(ViewUtil.checkBounds(imapCertificateChecks, certCheck));
|
||||
expandAdvanced = expandAdvanced || imapCertificateChecks != 0;
|
||||
} else if (getIntent() != null && getIntent().getBundleExtra(ACCOUNT_DATA) != null) {
|
||||
// Companion app might have sent account data
|
||||
Bundle b = getIntent().getBundleExtra(ACCOUNT_DATA);
|
||||
|
||||
@@ -40,6 +40,7 @@ import com.b44t.messenger.DcEvent;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
import com.b44t.messenger.rpc.Rpc;
|
||||
import com.b44t.messenger.rpc.RpcException;
|
||||
import com.google.common.base.Charsets;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.thoughtcrime.securesms.connect.AccountManager;
|
||||
@@ -54,6 +55,8 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -63,6 +66,7 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
private static final String EXTRA_ACCOUNT_ID = "accountId";
|
||||
private static final String EXTRA_APP_MSG_ID = "appMessageId";
|
||||
private static final String EXTRA_HIDE_ACTION_BAR = "hideActionBar";
|
||||
private static final String EXTRA_HREF = "href";
|
||||
private static final int REQUEST_CODE_FILE_PICKER = 51426;
|
||||
|
||||
private ValueCallback<Uri[]> filePathCallback;
|
||||
@@ -71,6 +75,7 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
private DcMsg dcAppMsg;
|
||||
private String baseURL;
|
||||
private String sourceCodeUrl = "";
|
||||
private String selfAddr = "";
|
||||
private boolean internetAccess = false;
|
||||
private boolean hideActionBar = false;
|
||||
|
||||
@@ -92,33 +97,34 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
return;
|
||||
}
|
||||
}
|
||||
openWebxdcActivity(context, msgId, true);
|
||||
openWebxdcActivity(context, msgId, true, "");
|
||||
}
|
||||
|
||||
public static void openWebxdcActivity(Context context, DcMsg instance) {
|
||||
openWebxdcActivity(context, instance.getId(), false);
|
||||
openWebxdcActivity(context, instance, "");
|
||||
}
|
||||
|
||||
public static void openWebxdcActivity(Context context, int msgId, boolean hideActionBar) {
|
||||
public static void openWebxdcActivity(Context context, DcMsg instance, String href) {
|
||||
openWebxdcActivity(context, instance.getId(), false, href);
|
||||
}
|
||||
|
||||
public static void openWebxdcActivity(Context context, int msgId, boolean hideActionBar, String href) {
|
||||
if (!Util.isClickedRecently()) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (Prefs.isDeveloperModeEnabled(context)) {
|
||||
WebView.setWebContentsDebuggingEnabled(true);
|
||||
}
|
||||
context.startActivity(getWebxdcIntent(context, msgId, hideActionBar));
|
||||
} else {
|
||||
Toast.makeText(context, "At least Android 5.0 (Lollipop) required for Webxdc.", Toast.LENGTH_LONG).show();
|
||||
if (Prefs.isDeveloperModeEnabled(context)) {
|
||||
WebView.setWebContentsDebuggingEnabled(true);
|
||||
}
|
||||
context.startActivity(getWebxdcIntent(context, msgId, hideActionBar, href));
|
||||
}
|
||||
}
|
||||
|
||||
private static Intent getWebxdcIntent(Context context, int msgId, boolean hideActionBar) {
|
||||
private static Intent getWebxdcIntent(Context context, int msgId, boolean hideActionBar, String href) {
|
||||
DcContext dcContext = DcHelper.getContext(context);
|
||||
Intent intent = new Intent(context, WebxdcActivity.class);
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.putExtra(EXTRA_ACCOUNT_ID, dcContext.getAccountId());
|
||||
intent.putExtra(EXTRA_APP_MSG_ID, msgId);
|
||||
intent.putExtra(EXTRA_HIDE_ACTION_BAR, hideActionBar);
|
||||
intent.putExtra(EXTRA_HREF, href);
|
||||
return intent;
|
||||
}
|
||||
|
||||
@@ -129,7 +135,7 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
.putExtra(ConversationActivity.CHAT_ID_EXTRA, dcContext.getMsg(msgId).getChatId())
|
||||
.setAction(Intent.ACTION_VIEW);
|
||||
|
||||
final Intent webxdcIntent = getWebxdcIntent(context, msgId, false);
|
||||
final Intent webxdcIntent = getWebxdcIntent(context, msgId, false, "");
|
||||
|
||||
return TaskStackBuilder.create(context)
|
||||
.addNextIntentWithParentStack(chatIntent)
|
||||
@@ -197,6 +203,7 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
// this is needed here because if the app is opened while already in landscape mode, onConfigurationChanged() is not triggered
|
||||
setScreenMode(getResources().getConfiguration());
|
||||
}
|
||||
selfAddr = JsonUtils.optString(info, "self_addr");
|
||||
|
||||
toggleFakeProxy(!internetAccess);
|
||||
|
||||
@@ -213,7 +220,15 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
webView.setNetworkAvailable(internetAccess); // this does not block network but sets `window.navigator.isOnline` in js land
|
||||
webView.addJavascriptInterface(new InternalJSApi(), "InternalJSApi");
|
||||
|
||||
webView.loadUrl(this.baseURL + "/index.html");
|
||||
String href = b.getString(EXTRA_HREF, "");
|
||||
String encodedHref = "";
|
||||
try {
|
||||
encodedHref = URLEncoder.encode(href, Charsets.UTF_8.name());
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
webView.loadUrl(this.baseURL + "/webxdc_bootstrap324567869.html?i=1&href=" + encodedHref);
|
||||
|
||||
Util.runOnAnyBackgroundThread(() -> {
|
||||
final DcChat chat = dcContext.getChat(dcAppMsg.getChatId());
|
||||
@@ -223,6 +238,18 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
DcHelper.getNotificationCenter(this).updateVisibleWebxdc(dcContext.getAccountId(), dcAppMsg.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
DcHelper.getNotificationCenter(this).clearVisibleWebxdc();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
DcHelper.getEventCenter(this.getApplicationContext()).removeObservers(this);
|
||||
@@ -249,6 +276,9 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
case R.id.source_code:
|
||||
openUrlInBrowser(this, sourceCodeUrl);
|
||||
return true;
|
||||
case R.id.show_in_chat:
|
||||
showInChat();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -334,7 +364,7 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
res = new WebResourceResponse("text/plain", "UTF-8", targetStream);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !internetAccess) {
|
||||
if (!internetAccess) {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("Content-Security-Policy",
|
||||
"default-src 'self'; "
|
||||
@@ -353,9 +383,7 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
}
|
||||
|
||||
private void callJavaScriptFunction(String func) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
webView.evaluateJavascript("window." + func + ";", null);
|
||||
}
|
||||
webView.evaluateJavascript("document.getElementById('frame').contentWindow." + func + ";", null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -394,6 +422,13 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
}
|
||||
}
|
||||
|
||||
private void showInChat() {
|
||||
Intent intent = new Intent(this, ConversationActivity.class);
|
||||
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, dcAppMsg.getChatId());
|
||||
intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, DcMsg.getMessagePosition(dcAppMsg, dcContext));
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
public static void addToHomeScreen(Activity activity, int msgId) {
|
||||
Context context = activity.getApplicationContext();
|
||||
try {
|
||||
@@ -475,7 +510,7 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
int flags = Base64.NO_WRAP | Base64.NO_PADDING | Base64.URL_SAFE;
|
||||
return Base64.encodeToString(dcContext.getCommunityUser().getBytes(), flags);
|
||||
}
|
||||
return dcContext.getConfig("configured_addr");
|
||||
return WebxdcActivity.this.selfAddr;
|
||||
}
|
||||
|
||||
/** @noinspection unused*/
|
||||
@@ -493,9 +528,9 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
|
||||
/** @noinspection unused*/
|
||||
@JavascriptInterface
|
||||
public boolean sendStatusUpdate(String payload, String descr) {
|
||||
public boolean sendStatusUpdate(String payload) {
|
||||
Log.i(TAG, "sendStatusUpdate");
|
||||
if (!WebxdcActivity.this.dcContext.sendWebxdcStatusUpdate(WebxdcActivity.this.dcAppMsg.getId(), payload, descr)) {
|
||||
if (!WebxdcActivity.this.dcContext.sendWebxdcStatusUpdate(WebxdcActivity.this.dcAppMsg.getId(), payload)) {
|
||||
DcChat dcChat = WebxdcActivity.this.dcContext.getChat(WebxdcActivity.this.dcAppMsg.getChatId());
|
||||
Toast.makeText(WebxdcActivity.this,
|
||||
dcChat.isContactRequest() ?
|
||||
|
||||
@@ -166,7 +166,7 @@ public class WelcomeActivity extends BaseActionBarActivity implements DcEventCen
|
||||
.withPermanentDenialDialog(getString(R.string.perm_explain_access_to_storage_denied))
|
||||
.onAllGranted(() -> {
|
||||
File imexDir = DcHelper.getImexDir();
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
AttachmentManager.selectMediaType(this, "application/x-tar", null, PICK_BACKUP, StorageUtil.getDownloadUri());
|
||||
} else {
|
||||
final String backupFile = dcContext.imexHasBackup(imexDir.getAbsolutePath());
|
||||
|
||||
+10
-5
@@ -189,11 +189,16 @@ public class AccountSelectionListFragment extends DialogFragment implements DcEv
|
||||
avatar.setAvatar(GlideApp.with(activity), recipient, false);
|
||||
nameView.setText(name);
|
||||
addrView.setText(contact.getAddr());
|
||||
try {
|
||||
sizeView.setText(Util.getPrettyFileSize(rpc.getAccountFileSize(accountId)));
|
||||
} catch (RpcException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
Util.runOnAnyBackgroundThread(() -> {
|
||||
try {
|
||||
final int sizeBytes = rpc.getAccountFileSize(accountId);
|
||||
Util.runOnMain(() -> {
|
||||
sizeView.setText(Util.getPrettyFileSize(sizeBytes));
|
||||
});
|
||||
} catch (RpcException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
description.setText(activity.getString(R.string.delete_account_explain_with_name, name));
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(activity)
|
||||
|
||||
@@ -130,33 +130,23 @@ public class AttachmentTypeSelector extends PopupWindow {
|
||||
public void onGlobalLayout() {
|
||||
getContentView().getViewTreeObserver().removeGlobalOnLayoutListener(this);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
animateWindowInCircular(anchor, getContentView());
|
||||
} else {
|
||||
animateWindowInTranslate(getContentView());
|
||||
}
|
||||
animateWindowInCircular(anchor, getContentView());
|
||||
}
|
||||
});
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
//animateButtonIn(cameraButton, ANIMATION_DURATION / 2);
|
||||
animateButtonIn(videoButton, ANIMATION_DURATION / 2);
|
||||
animateButtonIn(imageButton, ANIMATION_DURATION / 3);
|
||||
animateButtonIn(contactButton, ANIMATION_DURATION / 3);
|
||||
animateButtonIn(locationButton, ANIMATION_DURATION / 4);
|
||||
animateButtonIn(documentButton, ANIMATION_DURATION / 4);
|
||||
animateButtonIn(videoChatButton, 0);
|
||||
animateButtonIn(closeButton, 0);
|
||||
}
|
||||
//animateButtonIn(cameraButton, ANIMATION_DURATION / 2);
|
||||
animateButtonIn(videoButton, ANIMATION_DURATION / 2);
|
||||
animateButtonIn(imageButton, ANIMATION_DURATION / 3);
|
||||
animateButtonIn(contactButton, ANIMATION_DURATION / 3);
|
||||
animateButtonIn(locationButton, ANIMATION_DURATION / 4);
|
||||
animateButtonIn(documentButton, ANIMATION_DURATION / 4);
|
||||
animateButtonIn(videoChatButton, 0);
|
||||
animateButtonIn(closeButton, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dismiss() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
animateWindowOutCircular(currentAnchor, getContentView());
|
||||
} else {
|
||||
animateWindowOutTranslate(getContentView());
|
||||
}
|
||||
animateWindowOutCircular(currentAnchor, getContentView());
|
||||
}
|
||||
|
||||
public void setListener(@Nullable AttachmentClickedListener listener) {
|
||||
@@ -186,7 +176,6 @@ public class AttachmentTypeSelector extends PopupWindow {
|
||||
button.startAnimation(animation);
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
private void animateWindowInCircular(@Nullable View anchor, @NonNull View contentView) {
|
||||
Pair<Integer, Integer> coordinates = getClickOrigin(anchor, contentView);
|
||||
Animator animator = ViewAnimationUtils.createCircularReveal(contentView,
|
||||
@@ -205,7 +194,6 @@ public class AttachmentTypeSelector extends PopupWindow {
|
||||
getContentView().startAnimation(animation);
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
private void animateWindowOutCircular(@Nullable View anchor, @NonNull View contentView) {
|
||||
Pair<Integer, Integer> coordinates = getClickOrigin(anchor, contentView);
|
||||
Animator animator = ViewAnimationUtils.createCircularReveal(getContentView(),
|
||||
|
||||
@@ -71,12 +71,10 @@ public class AudioView extends FrameLayout implements AudioSlidePlayer.Listener
|
||||
this.pauseButton.setOnClickListener(new PauseClickedListener());
|
||||
this.seekBar.setOnSeekBarChangeListener(new SeekBarModifiedListener());
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
this.playButton.setImageDrawable(context.getDrawable(R.drawable.play_icon));
|
||||
this.pauseButton.setImageDrawable(context.getDrawable(R.drawable.pause_icon));
|
||||
this.playButton.setBackground(context.getDrawable(R.drawable.ic_circle_fill_white_48dp));
|
||||
this.pauseButton.setBackground(context.getDrawable(R.drawable.ic_circle_fill_white_48dp));
|
||||
}
|
||||
this.playButton.setImageDrawable(context.getDrawable(R.drawable.play_icon));
|
||||
this.pauseButton.setImageDrawable(context.getDrawable(R.drawable.pause_icon));
|
||||
this.playButton.setBackground(context.getDrawable(R.drawable.ic_circle_fill_white_48dp));
|
||||
this.pauseButton.setBackground(context.getDrawable(R.drawable.ic_circle_fill_white_48dp));
|
||||
|
||||
setTint(getContext().getResources().getColor(R.color.audio_icon));
|
||||
}
|
||||
@@ -192,13 +190,8 @@ public class AudioView extends FrameLayout implements AudioSlidePlayer.Listener
|
||||
}
|
||||
|
||||
public void setTint(int foregroundTint) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
this.playButton.setBackgroundTintList(ColorStateList.valueOf(foregroundTint));
|
||||
this.pauseButton.setBackgroundTintList(ColorStateList.valueOf(foregroundTint));
|
||||
} else {
|
||||
this.playButton.setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
|
||||
this.pauseButton.setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
this.playButton.setBackgroundTintList(ColorStateList.valueOf(foregroundTint));
|
||||
this.pauseButton.setBackgroundTintList(ColorStateList.valueOf(foregroundTint));
|
||||
|
||||
this.seekBar.getProgressDrawable().setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
|
||||
|
||||
@@ -220,25 +213,20 @@ public class AudioView extends FrameLayout implements AudioSlidePlayer.Listener
|
||||
private void togglePlayToPause() {
|
||||
controlToggle.displayQuick(pauseButton);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
AnimatedVectorDrawable playToPauseDrawable = (AnimatedVectorDrawable)getContext().getDrawable(R.drawable.play_to_pause_animation);
|
||||
pauseButton.setImageDrawable(playToPauseDrawable);
|
||||
playToPauseDrawable.start();
|
||||
}
|
||||
AnimatedVectorDrawable playToPauseDrawable = (AnimatedVectorDrawable) getContext().getDrawable(R.drawable.play_to_pause_animation);
|
||||
pauseButton.setImageDrawable(playToPauseDrawable);
|
||||
playToPauseDrawable.start();
|
||||
}
|
||||
|
||||
private void togglePauseToPlay() {
|
||||
controlToggle.displayQuick(playButton);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
AnimatedVectorDrawable pauseToPlayDrawable = (AnimatedVectorDrawable)getContext().getDrawable(R.drawable.pause_to_play_animation);
|
||||
playButton.setImageDrawable(pauseToPlayDrawable);
|
||||
pauseToPlayDrawable.start();
|
||||
}
|
||||
AnimatedVectorDrawable pauseToPlayDrawable = (AnimatedVectorDrawable) getContext().getDrawable(R.drawable.pause_to_play_animation);
|
||||
playButton.setImageDrawable(pauseToPlayDrawable);
|
||||
pauseToPlayDrawable.start();
|
||||
}
|
||||
|
||||
private class PlayClickedListener implements View.OnClickListener {
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
try {
|
||||
@@ -279,7 +267,6 @@ public class AudioView extends FrameLayout implements AudioSlidePlayer.Listener
|
||||
}
|
||||
|
||||
private class PauseClickedListener implements View.OnClickListener {
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Log.w(TAG, "pausebutton onClick");
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.widget.AppCompatEditText;
|
||||
import androidx.core.view.inputmethod.EditorInfoCompat;
|
||||
import androidx.core.view.inputmethod.InputConnectionCompat;
|
||||
import androidx.core.view.inputmethod.InputContentInfoCompat;
|
||||
@@ -25,10 +26,9 @@ import android.view.inputmethod.InputConnection;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiEditText;
|
||||
import org.thoughtcrime.securesms.util.Prefs;
|
||||
|
||||
public class ComposeText extends EmojiEditText {
|
||||
public class ComposeText extends AppCompatEditText {
|
||||
|
||||
private CharSequence hint;
|
||||
private SpannableString subHint;
|
||||
@@ -122,7 +122,6 @@ public class ComposeText extends EmojiEditText {
|
||||
editorInfo.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT < 21) return inputConnection;
|
||||
if (mediaListener == null) return inputConnection;
|
||||
if (inputConnection == null) return null;
|
||||
|
||||
|
||||
@@ -120,15 +120,10 @@ public class ConversationItemThumbnail extends FrameLayout {
|
||||
@SuppressWarnings("SuspiciousNameCombination")
|
||||
@Override
|
||||
protected void dispatchDraw(Canvas canvas) {
|
||||
if (cornerMask.isLegacy()) {
|
||||
cornerMask.mask(canvas);
|
||||
}
|
||||
|
||||
super.dispatchDraw(canvas);
|
||||
|
||||
if (!cornerMask.isLegacy()) {
|
||||
cornerMask.mask(canvas);
|
||||
}
|
||||
cornerMask.mask(canvas);
|
||||
|
||||
final float halfStrokeWidth = outlinePaint.getStrokeWidth() / 2;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import android.graphics.Path;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffXfermode;
|
||||
import android.graphics.RectF;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import android.view.View;
|
||||
|
||||
@@ -20,11 +20,7 @@ public class CornerMask {
|
||||
private final RectF bounds = new RectF();
|
||||
|
||||
public CornerMask(@NonNull View view) {
|
||||
if (isLegacy()) {
|
||||
view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
|
||||
} else {
|
||||
view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
|
||||
}
|
||||
view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
|
||||
|
||||
clearPaint.setColor(Color.BLACK);
|
||||
clearPaint.setStyle(Paint.Style.FILL);
|
||||
@@ -44,19 +40,10 @@ public class CornerMask {
|
||||
// Note: There's a bug in the P beta where most PorterDuff modes aren't working. But CLEAR does.
|
||||
// So we find and inverse path and use Mode.CLEAR for versions that support Path.op().
|
||||
// See issue https://issuetracker.google.com/issues/111394085.
|
||||
if (!isLegacy()) {
|
||||
outline.reset();
|
||||
outline.addRect(bounds, Path.Direction.CW);
|
||||
outline.op(corners, Path.Op.DIFFERENCE);
|
||||
canvas.drawPath(outline, clearPaint);
|
||||
} else {
|
||||
corners.addRoundRect(bounds, radii, Path.Direction.CW);
|
||||
canvas.clipPath(corners);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isLegacy() {
|
||||
return Build.VERSION.SDK_INT < 19;
|
||||
outline.reset();
|
||||
outline.addRect(bounds, Path.Direction.CW);
|
||||
outline.op(corners, Path.Op.DIFFERENCE);
|
||||
canvas.drawPath(outline, clearPaint);
|
||||
}
|
||||
|
||||
public void setRadius(int radius) {
|
||||
|
||||
@@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Typeface;
|
||||
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
@@ -14,12 +16,11 @@ import android.text.style.TypefaceSpan;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.ResUtil;
|
||||
import org.thoughtcrime.securesms.util.spans.CenterAlignedRelativeSizeSpan;
|
||||
|
||||
public class FromTextView extends EmojiTextView {
|
||||
public class FromTextView extends AppCompatTextView {
|
||||
|
||||
private static final String TAG = FromTextView.class.getSimpleName();
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import android.os.Build;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.AlphaAnimation;
|
||||
@@ -29,14 +28,12 @@ import com.b44t.messenger.util.concurrent.SettableFuture;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.Prefs;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
|
||||
@@ -46,9 +43,9 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
public class InputPanel extends ConstraintLayout
|
||||
implements MicrophoneRecorderView.Listener,
|
||||
KeyboardAwareLinearLayout.OnKeyboardShownListener,
|
||||
EmojiKeyboardProvider.EmojiEventListener
|
||||
implements MicrophoneRecorderView.Listener,
|
||||
KeyboardAwareLinearLayout.OnKeyboardShownListener,
|
||||
MediaKeyboard.MediaKeyboardListener
|
||||
{
|
||||
|
||||
private static final String TAG = InputPanel.class.getSimpleName();
|
||||
@@ -56,7 +53,7 @@ public class InputPanel extends ConstraintLayout
|
||||
private static final int FADE_TIME = 150;
|
||||
|
||||
private QuoteView quoteView;
|
||||
private EmojiToggle mediaKeyboard;
|
||||
private EmojiToggle emojiToggle;
|
||||
private ComposeText composeText;
|
||||
private View quickCameraToggle;
|
||||
private View quickAudioToggle;
|
||||
@@ -70,7 +67,6 @@ public class InputPanel extends ConstraintLayout
|
||||
private ValueAnimator quoteAnimator;
|
||||
|
||||
private @Nullable Listener listener;
|
||||
private boolean emojiVisible;
|
||||
|
||||
public InputPanel(Context context) {
|
||||
super(context);
|
||||
@@ -92,7 +88,7 @@ public class InputPanel extends ConstraintLayout
|
||||
View quoteDismiss = findViewById(R.id.quote_dismiss);
|
||||
|
||||
this.quoteView = findViewById(R.id.quote_view);
|
||||
this.mediaKeyboard = findViewById(R.id.emoji_toggle);
|
||||
this.emojiToggle = findViewById(R.id.emoji_toggle);
|
||||
this.composeText = findViewById(R.id.embedded_text_editor);
|
||||
this.quickCameraToggle = findViewById(R.id.quick_camera_toggle);
|
||||
this.quickAudioToggle = findViewById(R.id.quick_audio_toggle);
|
||||
@@ -106,21 +102,13 @@ public class InputPanel extends ConstraintLayout
|
||||
|
||||
this.recordLockCancel.setOnClickListener(v -> microphoneRecorderView.cancelAction());
|
||||
|
||||
if (Prefs.isSystemEmojiPreferred(getContext())) {
|
||||
mediaKeyboard.setVisibility(View.GONE);
|
||||
emojiVisible = false;
|
||||
} else {
|
||||
mediaKeyboard.setVisibility(View.VISIBLE);
|
||||
emojiVisible = true;
|
||||
}
|
||||
|
||||
quoteDismiss.setOnClickListener(v -> clearQuote());
|
||||
}
|
||||
|
||||
public void setListener(final @NonNull Listener listener) {
|
||||
this.listener = listener;
|
||||
|
||||
mediaKeyboard.setOnClickListener(v -> listener.onEmojiToggle());
|
||||
emojiToggle.setOnClickListener(v -> listener.onEmojiToggle());
|
||||
}
|
||||
|
||||
public void setMediaListener(@NonNull MediaListener listener) {
|
||||
@@ -207,7 +195,7 @@ public class InputPanel extends ConstraintLayout
|
||||
}
|
||||
|
||||
public void setMediaKeyboard(@NonNull MediaKeyboard mediaKeyboard) {
|
||||
this.mediaKeyboard.attach(mediaKeyboard);
|
||||
mediaKeyboard.setKeyboardListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -221,7 +209,7 @@ public class InputPanel extends ConstraintLayout
|
||||
recordTime.display();
|
||||
slideToCancel.display();
|
||||
|
||||
if (emojiVisible) ViewUtil.fadeOut(mediaKeyboard, FADE_TIME, View.INVISIBLE);
|
||||
ViewUtil.fadeOut(emojiToggle, FADE_TIME, View.INVISIBLE);
|
||||
ViewUtil.fadeOut(composeText, FADE_TIME, View.INVISIBLE);
|
||||
ViewUtil.fadeOut(quickCameraToggle, FADE_TIME, View.INVISIBLE);
|
||||
ViewUtil.fadeOut(quickAudioToggle, FADE_TIME, View.INVISIBLE);
|
||||
@@ -276,7 +264,7 @@ public class InputPanel extends ConstraintLayout
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
composeText.setEnabled(enabled);
|
||||
mediaKeyboard.setEnabled(enabled);
|
||||
emojiToggle.setEnabled(enabled);
|
||||
quickAudioToggle.setEnabled(enabled);
|
||||
quickCameraToggle.setEnabled(enabled);
|
||||
}
|
||||
@@ -290,7 +278,7 @@ public class InputPanel extends ConstraintLayout
|
||||
future.addListener(new AssertedSuccessListener<Void>() {
|
||||
@Override
|
||||
public void onSuccess(Void result) {
|
||||
if (emojiVisible) ViewUtil.fadeIn(mediaKeyboard, FADE_TIME);
|
||||
ViewUtil.fadeIn(emojiToggle, FADE_TIME);
|
||||
ViewUtil.fadeIn(composeText, FADE_TIME);
|
||||
ViewUtil.fadeIn(quickCameraToggle, FADE_TIME);
|
||||
ViewUtil.fadeIn(quickAudioToggle, FADE_TIME);
|
||||
@@ -304,17 +292,7 @@ public class InputPanel extends ConstraintLayout
|
||||
|
||||
@Override
|
||||
public void onKeyboardShown() {
|
||||
mediaKeyboard.setToMedia();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKeyEvent(KeyEvent keyEvent) {
|
||||
composeText.dispatchKeyEvent(keyEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEmojiSelected(String emoji) {
|
||||
composeText.insertEmoji(emoji);
|
||||
emojiToggle.setToMedia();
|
||||
}
|
||||
|
||||
public boolean isRecordingInLockedMode() {
|
||||
@@ -325,6 +303,25 @@ public class InputPanel extends ConstraintLayout
|
||||
microphoneRecorderView.unlockAction();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShown() {
|
||||
emojiToggle.setToIme();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHidden() {
|
||||
emojiToggle.setToMedia();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEmojiPicked(String emoji) {
|
||||
final int start = composeText.getSelectionStart();
|
||||
final int end = composeText.getSelectionEnd();
|
||||
|
||||
composeText.getText().replace(Math.min(start, end), Math.max(start, end), emoji);
|
||||
composeText.setSelection(start + emoji.length());
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onRecorderStarted();
|
||||
void onRecorderLocked();
|
||||
|
||||
@@ -104,7 +104,7 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
|
||||
}
|
||||
|
||||
private void updateKeyboardState() {
|
||||
if (viewInset == 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) viewInset = getViewInset();
|
||||
if (viewInset == 0) viewInset = getViewInset();
|
||||
|
||||
getWindowVisibleDisplayFrame(rect);
|
||||
|
||||
@@ -148,7 +148,6 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
|
||||
private int getViewInset() {
|
||||
try {
|
||||
Field attachInfoField = View.class.getDeclaredField("mAttachInfo");
|
||||
@@ -219,11 +218,7 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
|
||||
return Surface.ROTATION_0;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
return ServiceUtil.getWindowManager(getContext()).getDefaultDisplay().getRotation();
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
getContext().getDisplay().getRealMetrics(displayMetrics);
|
||||
} else {
|
||||
ServiceUtil.getWindowManager(getContext()).getDefaultDisplay().getRealMetrics(displayMetrics);
|
||||
|
||||
@@ -40,7 +40,6 @@ public class MediaView extends FrameLayout {
|
||||
initialize();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
public MediaView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
initialize();
|
||||
|
||||
@@ -4,7 +4,6 @@ package org.thoughtcrime.securesms.components;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.text.Spannable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
@@ -17,7 +16,6 @@ import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.b44t.messenger.DcContact;
|
||||
@@ -80,7 +78,6 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
initialize(attrs);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
public QuoteView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
initialize(attrs);
|
||||
@@ -158,7 +155,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
if (contact == null) {
|
||||
authorView.setText(getContext().getString(R.string.forwarded_message));
|
||||
} else {
|
||||
authorView.setText(getContext().getString(R.string.forwarded_by, quotedMsg.getSenderName(contact, false)));
|
||||
authorView.setText(getContext().getString(R.string.forwarded_by, quotedMsg.getSenderName(contact)));
|
||||
}
|
||||
authorView.setTextColor(getForwardedColor());
|
||||
quoteBarView.setBackgroundColor(getForwardedColor());
|
||||
@@ -169,7 +166,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
quoteBarView.setBackgroundColor(getForwardedColor());
|
||||
} else {
|
||||
authorView.setVisibility(VISIBLE);
|
||||
authorView.setText(quotedMsg.getSenderName(contact, true));
|
||||
authorView.setText(quotedMsg.getSenderName(contact));
|
||||
if (hasSticker) {
|
||||
authorView.setTextColor(getResources().getColor(R.color.core_dark_05));
|
||||
quoteBarView.setBackgroundColor(getResources().getColor(R.color.core_dark_05));
|
||||
|
||||
@@ -29,7 +29,6 @@ public class RepeatableImageKey extends ImageButton {
|
||||
init();
|
||||
}
|
||||
|
||||
@TargetApi(VERSION_CODES.LOLLIPOP)
|
||||
public RepeatableImageKey(Context context, AttributeSet attrs, int defStyleAttr,
|
||||
int defStyleRes)
|
||||
{
|
||||
|
||||
@@ -130,15 +130,11 @@ public class SearchToolbar extends LinearLayout {
|
||||
|
||||
searchItem.expandActionView();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
Animator animator = ViewAnimationUtils.createCircularReveal(this, (int)x, (int)y, 0, getWidth());
|
||||
animator.setDuration(400);
|
||||
Animator animator = ViewAnimationUtils.createCircularReveal(this, (int) x, (int) y, 0, getWidth());
|
||||
animator.setDuration(400);
|
||||
|
||||
setVisibility(View.VISIBLE);
|
||||
animator.start();
|
||||
} else {
|
||||
setVisibility(View.VISIBLE);
|
||||
}
|
||||
setVisibility(View.VISIBLE);
|
||||
animator.start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,19 +149,15 @@ public class SearchToolbar extends LinearLayout {
|
||||
|
||||
if (listener != null) listener.onSearchClosed();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
Animator animator = ViewAnimationUtils.createCircularReveal(this, (int)x, (int)y, getWidth(), 0);
|
||||
animator.setDuration(400);
|
||||
animator.addListener(new AnimationCompleteListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
setVisibility(View.INVISIBLE);
|
||||
}
|
||||
});
|
||||
animator.start();
|
||||
} else {
|
||||
setVisibility(View.INVISIBLE);
|
||||
}
|
||||
Animator animator = ViewAnimationUtils.createCircularReveal(this, (int) x, (int) y, getWidth(), 0);
|
||||
animator.setDuration(400);
|
||||
animator.addListener(new AnimationCompleteListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
setVisibility(View.INVISIBLE);
|
||||
}
|
||||
});
|
||||
animator.start();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ public class SwitchPreferenceCompat extends CheckBoxPreference {
|
||||
setLayoutRes();
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
public SwitchPreferenceCompat(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
setLayoutRes();
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.Drawable.Callback;
|
||||
import android.text.style.ImageSpan;
|
||||
|
||||
public class AnimatingImageSpan extends ImageSpan {
|
||||
public AnimatingImageSpan(Drawable drawable, Callback callback) {
|
||||
super(drawable, ALIGN_BOTTOM);
|
||||
drawable.setCallback(callback);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ResUtil;
|
||||
|
||||
public class AsciiEmojiView extends View {
|
||||
|
||||
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
|
||||
|
||||
private String emoji;
|
||||
|
||||
public AsciiEmojiView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public AsciiEmojiView(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public void setEmoji(String emoji) {
|
||||
this.emoji = emoji;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
if (TextUtils.isEmpty(emoji)) {
|
||||
return;
|
||||
}
|
||||
|
||||
float targetFontSize = 0.75f * getHeight() - getPaddingTop() - getPaddingBottom();
|
||||
|
||||
paint.setTextSize(targetFontSize);
|
||||
paint.setColor(ResUtil.getColor(getContext(), R.attr.emoji_text_color));
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
|
||||
int xPos = (getWidth() / 2);
|
||||
int yPos = (int) ((getHeight() / 2) - ((paint.descent() + paint.ascent()) / 2));
|
||||
|
||||
float overflow = paint.measureText(emoji) / (getWidth() - getPaddingLeft() - getPaddingRight());
|
||||
if (overflow > 1f) {
|
||||
paint.setTextSize(targetFontSize / overflow);
|
||||
yPos = (int) ((getHeight() / 2) - ((paint.descent() + paint.ascent()) / 2));
|
||||
}
|
||||
canvas.drawText(emoji, xPos, yPos, paint);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
//noinspection SuspiciousNameCombination
|
||||
super.onMeasure(widthMeasureSpec, widthMeasureSpec);
|
||||
}
|
||||
}
|
||||
+109
File diff suppressed because one or more lines are too long
-55
@@ -1,55 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class CompositeEmojiPageModel implements EmojiPageModel {
|
||||
@AttrRes private final int iconAttr;
|
||||
@NonNull private final EmojiPageModel[] models;
|
||||
|
||||
public CompositeEmojiPageModel(@AttrRes int iconAttr, @NonNull EmojiPageModel... models) {
|
||||
this.iconAttr = iconAttr;
|
||||
this.models = models;
|
||||
}
|
||||
|
||||
public int getIconAttr() {
|
||||
return iconAttr;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<String> getEmoji() {
|
||||
List<String> emojis = new LinkedList<>();
|
||||
for (EmojiPageModel model : models) {
|
||||
emojis.addAll(model.getEmoji());
|
||||
}
|
||||
return emojis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<Emoji> getDisplayEmoji() {
|
||||
List<Emoji> emojis = new LinkedList<>();
|
||||
for (EmojiPageModel model : models) {
|
||||
emojis.addAll(model.getDisplayEmoji());
|
||||
}
|
||||
return emojis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSpriteMap() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getSprite() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDynamic() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class Emoji {
|
||||
|
||||
private final List<String> variations;
|
||||
|
||||
public Emoji(String... variations) {
|
||||
this.variations = Arrays.asList(variations);
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return variations.get(0);
|
||||
}
|
||||
|
||||
public List<String> getVariations() {
|
||||
return variations;
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.AppCompatEditText;
|
||||
import android.text.InputFilter;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
|
||||
import org.thoughtcrime.securesms.util.Prefs;
|
||||
|
||||
|
||||
public class EmojiEditText extends AppCompatEditText {
|
||||
private static final String TAG = EmojiEditText.class.getSimpleName();
|
||||
|
||||
public EmojiEditText(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public EmojiEditText(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, R.attr.editTextStyle);
|
||||
}
|
||||
|
||||
public EmojiEditText(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
if (!Prefs.isSystemEmojiPreferred(getContext())) {
|
||||
setFilters(appendEmojiFilter(this.getFilters()));
|
||||
}
|
||||
}
|
||||
|
||||
public void insertEmoji(String emoji) {
|
||||
final int start = getSelectionStart();
|
||||
final int end = getSelectionEnd();
|
||||
|
||||
getText().replace(Math.min(start, end), Math.max(start, end), emoji);
|
||||
setSelection(start + emoji.length());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateDrawable(@NonNull Drawable drawable) {
|
||||
if (drawable instanceof EmojiDrawable) invalidate();
|
||||
else super.invalidateDrawable(drawable);
|
||||
}
|
||||
|
||||
private InputFilter[] appendEmojiFilter(@Nullable InputFilter[] originalFilters) {
|
||||
InputFilter[] result;
|
||||
|
||||
if (originalFilters != null) {
|
||||
result = new InputFilter[originalFilters.length + 1];
|
||||
System.arraycopy(originalFilters, 0, result, 1, originalFilters.length);
|
||||
} else {
|
||||
result = new InputFilter[1];
|
||||
}
|
||||
|
||||
result[0] = new EmojiFilter(this);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.text.InputFilter;
|
||||
import android.text.Spannable;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.TextView;
|
||||
|
||||
public class EmojiFilter implements InputFilter {
|
||||
private final TextView view;
|
||||
|
||||
public EmojiFilter(TextView view) {
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend)
|
||||
{
|
||||
char[] v = new char[end - start];
|
||||
TextUtils.getChars(source, start, end, v, 0);
|
||||
|
||||
Spannable emojified = EmojiProvider.getInstance(view.getContext()).emojify(new String(v), view);
|
||||
|
||||
if (source instanceof Spanned && emojified != null) {
|
||||
TextUtils.copySpansFrom((Spanned) source, start, end, null, emojified, 0);
|
||||
}
|
||||
|
||||
return emojified;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.appcompat.widget.AppCompatImageView;
|
||||
|
||||
public class EmojiImageView extends AppCompatImageView {
|
||||
public EmojiImageView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public EmojiImageView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public void setImageEmoji(CharSequence emoji) {
|
||||
setImageDrawable(EmojiProvider.getInstance(getContext()).getEmojiDrawable(emoji));
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.VariationSelectorListener;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.util.ResUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A provider to select emoji in the {@link org.thoughtcrime.securesms.components.emoji.MediaKeyboard}.
|
||||
*/
|
||||
public class EmojiKeyboardProvider implements MediaKeyboardProvider,
|
||||
MediaKeyboardProvider.TabIconProvider,
|
||||
MediaKeyboardProvider.BackspaceObserver,
|
||||
VariationSelectorListener
|
||||
{
|
||||
private static final KeyEvent DELETE_KEY_EVENT = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL);
|
||||
|
||||
private static final String RECENT_STORAGE_KEY = "pref_recent_emoji2";
|
||||
|
||||
private final Context context;
|
||||
private final List<EmojiPageModel> models;
|
||||
private final RecentEmojiPageModel recentModel;
|
||||
private final EmojiPagerAdapter emojiPagerAdapter;
|
||||
private final EmojiEventListener emojiEventListener;
|
||||
|
||||
private Controller controller;
|
||||
|
||||
public EmojiKeyboardProvider(@NonNull Context context, @Nullable EmojiEventListener emojiEventListener) {
|
||||
this.context = context;
|
||||
this.emojiEventListener = emojiEventListener;
|
||||
this.models = new LinkedList<>();
|
||||
this.recentModel = new RecentEmojiPageModel(context, RECENT_STORAGE_KEY);
|
||||
this.emojiPagerAdapter = new EmojiPagerAdapter(context, models, new EmojiEventListener() {
|
||||
@Override
|
||||
public void onEmojiSelected(String emoji) {
|
||||
recentModel.onCodePointSelected(emoji);
|
||||
|
||||
if (emojiEventListener != null) {
|
||||
emojiEventListener.onEmojiSelected(emoji);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKeyEvent(KeyEvent keyEvent) {
|
||||
if (emojiEventListener != null) {
|
||||
emojiEventListener.onKeyEvent(keyEvent);
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
|
||||
models.add(recentModel);
|
||||
models.addAll(EmojiPages.DISPLAY_PAGES);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestPresentation(@NonNull Presenter presenter, boolean isSoloProvider) {
|
||||
presenter.present(this, emojiPagerAdapter, this, this, null, null, recentModel.getEmoji().size() > 0 ? 0 : 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setController(@Nullable Controller controller) {
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getProviderIconView(boolean selected) {
|
||||
if (selected) {
|
||||
return ThemeUtil.isDarkTheme(context) ? R.layout.emoji_keyboard_icon_dark_selected : R.layout.emoji_keyboard_icon_light_selected;
|
||||
} else {
|
||||
return ThemeUtil.isDarkTheme(context) ? R.layout.emoji_keyboard_icon_dark : R.layout.emoji_keyboard_icon_light;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadCategoryTabIcon(@NonNull GlideRequests glideRequests, @NonNull ImageView imageView, int index) {
|
||||
Drawable drawable = ResUtil.getDrawable(context, models.get(index).getIconAttr());
|
||||
imageView.setImageDrawable(drawable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackspaceClicked() {
|
||||
if (emojiEventListener != null) {
|
||||
emojiEventListener.onKeyEvent(DELETE_KEY_EVENT);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVariationSelectorStateChanged(boolean open) {
|
||||
if (controller != null) {
|
||||
controller.setViewPagerEnabled(!open);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
return obj instanceof EmojiKeyboardProvider;
|
||||
}
|
||||
|
||||
private static class EmojiPagerAdapter extends PagerAdapter {
|
||||
private final Context context;
|
||||
private final List<EmojiPageModel> pages;
|
||||
private final EmojiEventListener emojiSelectionListener;
|
||||
private final VariationSelectorListener variationSelectorListener;
|
||||
|
||||
public EmojiPagerAdapter(@NonNull Context context,
|
||||
@NonNull List<EmojiPageModel> pages,
|
||||
@NonNull EmojiEventListener emojiSelectionListener,
|
||||
@NonNull VariationSelectorListener variationSelectorListener)
|
||||
{
|
||||
super();
|
||||
this.context = context;
|
||||
this.pages = pages;
|
||||
this.emojiSelectionListener = emojiSelectionListener;
|
||||
this.variationSelectorListener = variationSelectorListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return pages.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Object instantiateItem(@NonNull ViewGroup container, int position) {
|
||||
EmojiPageView page = new EmojiPageView(context, emojiSelectionListener, variationSelectorListener, true);
|
||||
page.setModel(pages.get(position));
|
||||
container.addView(page);
|
||||
return page;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(ViewGroup container, int position, Object object) {
|
||||
container.removeView((View)object);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPrimaryItem(ViewGroup container, int position, Object object) {
|
||||
EmojiPageView current = (EmojiPageView) object;
|
||||
current.onSelected();
|
||||
super.setPrimaryItem(container, position, object);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isViewFromObject(View view, Object object) {
|
||||
return view == object;
|
||||
}
|
||||
}
|
||||
|
||||
public interface EmojiEventListener {
|
||||
void onEmojiSelected(String emoji);
|
||||
void onKeyEvent(KeyEvent keyEvent);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface EmojiPageModel {
|
||||
int getIconAttr();
|
||||
List<String> getEmoji();
|
||||
List<Emoji> getDisplayEmoji();
|
||||
boolean hasSpriteMap();
|
||||
String getSprite();
|
||||
boolean isDynamic();
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider.EmojiEventListener;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.VariationSelectorListener;
|
||||
|
||||
public class EmojiPageView extends FrameLayout implements VariationSelectorListener {
|
||||
private static final String TAG = EmojiPageView.class.getSimpleName();
|
||||
|
||||
private EmojiPageModel model;
|
||||
private final EmojiPageViewGridAdapter adapter;
|
||||
private final RecyclerView recyclerView;
|
||||
private final GridLayoutManager layoutManager;
|
||||
private final RecyclerView.OnItemTouchListener scrollDisabler;
|
||||
private final VariationSelectorListener variationSelectorListener;
|
||||
private final EmojiVariationSelectorPopup popup;
|
||||
|
||||
public EmojiPageView(@NonNull Context context,
|
||||
@NonNull EmojiEventListener emojiSelectionListener,
|
||||
@NonNull VariationSelectorListener variationSelectorListener,
|
||||
boolean allowVariations)
|
||||
{
|
||||
super(context);
|
||||
final View view = LayoutInflater.from(getContext()).inflate(R.layout.emoji_grid_layout, this, true);
|
||||
|
||||
this.variationSelectorListener = variationSelectorListener;
|
||||
|
||||
recyclerView = view.findViewById(R.id.emoji);
|
||||
layoutManager = new GridLayoutManager(context, 8);
|
||||
scrollDisabler = new ScrollDisabler();
|
||||
popup = new EmojiVariationSelectorPopup(context, emojiSelectionListener);
|
||||
adapter = new EmojiPageViewGridAdapter(EmojiProvider.getInstance(context),
|
||||
popup,
|
||||
emojiSelectionListener,
|
||||
this,
|
||||
allowVariations);
|
||||
|
||||
recyclerView.setLayoutManager(layoutManager);
|
||||
recyclerView.setAdapter(adapter);
|
||||
}
|
||||
|
||||
public void onSelected() {
|
||||
if (model.isDynamic() && adapter != null) {
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void setModel(EmojiPageModel model) {
|
||||
this.model = model;
|
||||
adapter.setEmoji(model.getDisplayEmoji());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
|
||||
if (visibility != VISIBLE) {
|
||||
popup.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
int idealWidth = getContext().getResources().getDimensionPixelOffset(R.dimen.emoji_drawer_item_width);
|
||||
layoutManager.setSpanCount(Math.max(w / idealWidth, 1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVariationSelectorStateChanged(boolean open) {
|
||||
if (open) {
|
||||
recyclerView.addOnItemTouchListener(scrollDisabler);
|
||||
} else {
|
||||
post(() -> recyclerView.removeOnItemTouchListener(scrollDisabler));
|
||||
}
|
||||
|
||||
if (variationSelectorListener != null) {
|
||||
variationSelectorListener.onVariationSelectorStateChanged(open);
|
||||
}
|
||||
}
|
||||
|
||||
public void setRecyclerNestedScrollingEnabled(boolean enabled) {
|
||||
recyclerView.setNestedScrollingEnabled(enabled);
|
||||
}
|
||||
|
||||
private static class ScrollDisabler implements RecyclerView.OnItemTouchListener {
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) { }
|
||||
|
||||
@Override
|
||||
public void onRequestDisallowInterceptTouchEvent(boolean b) { }
|
||||
}
|
||||
}
|
||||
-120
@@ -1,120 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupWindow;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider.EmojiEventListener;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class EmojiPageViewGridAdapter extends RecyclerView.Adapter<EmojiPageViewGridAdapter.EmojiViewHolder> implements PopupWindow.OnDismissListener {
|
||||
|
||||
private final List<Emoji> emojiList;
|
||||
private final EmojiProvider emojiProvider;
|
||||
private final EmojiVariationSelectorPopup popup;
|
||||
private final VariationSelectorListener variationSelectorListener;
|
||||
private final EmojiEventListener emojiEventListener;
|
||||
private final boolean allowVariations;
|
||||
|
||||
public EmojiPageViewGridAdapter(@NonNull EmojiProvider emojiProvider,
|
||||
@NonNull EmojiVariationSelectorPopup popup,
|
||||
@NonNull EmojiEventListener emojiEventListener,
|
||||
@NonNull VariationSelectorListener variationSelectorListener,
|
||||
boolean allowVariations)
|
||||
{
|
||||
this.emojiList = new ArrayList<>();
|
||||
this.emojiProvider = emojiProvider;
|
||||
this.popup = popup;
|
||||
this.emojiEventListener = emojiEventListener;
|
||||
this.variationSelectorListener = variationSelectorListener;
|
||||
this.allowVariations = allowVariations;
|
||||
|
||||
popup.setOnDismissListener(this);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public EmojiViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
|
||||
return new EmojiViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.emoji_display_item, viewGroup, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull EmojiViewHolder viewHolder, int i) {
|
||||
Emoji emoji = emojiList.get(i);
|
||||
|
||||
Drawable drawable = emojiProvider.getEmojiDrawable(emoji.getValue());
|
||||
|
||||
if (drawable != null) {
|
||||
viewHolder.textView.setVisibility(View.GONE);
|
||||
viewHolder.imageView.setVisibility(View.VISIBLE);
|
||||
viewHolder.imageView.setContentDescription(emoji.getValue());
|
||||
|
||||
viewHolder.imageView.setImageDrawable(drawable);
|
||||
} else {
|
||||
viewHolder.textView.setVisibility(View.VISIBLE);
|
||||
viewHolder.imageView.setVisibility(View.GONE);
|
||||
|
||||
viewHolder.textView.setEmoji(emoji.getValue());
|
||||
}
|
||||
|
||||
viewHolder.itemView.setOnClickListener(v -> {
|
||||
emojiEventListener.onEmojiSelected(emoji.getValue());
|
||||
});
|
||||
|
||||
if (allowVariations && emoji.getVariations().size() > 1) {
|
||||
viewHolder.itemView.setOnLongClickListener(v -> {
|
||||
popup.dismiss();
|
||||
popup.setVariations(emoji.getVariations());
|
||||
popup.showAsDropDown(viewHolder.itemView, 0, -(2 * viewHolder.itemView.getHeight()));
|
||||
variationSelectorListener.onVariationSelectorStateChanged(true);
|
||||
return true;
|
||||
});
|
||||
viewHolder.hintCorner.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
viewHolder.itemView.setOnLongClickListener(null);
|
||||
viewHolder.hintCorner.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return emojiList.size();
|
||||
}
|
||||
|
||||
public void setEmoji(@NonNull List<Emoji> emojiList) {
|
||||
this.emojiList.clear();
|
||||
this.emojiList.addAll(emojiList);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismiss() {
|
||||
variationSelectorListener.onVariationSelectorStateChanged(false);
|
||||
}
|
||||
|
||||
static class EmojiViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final ImageView imageView;
|
||||
private final AsciiEmojiView textView;
|
||||
private final ImageView hintCorner;
|
||||
|
||||
public EmojiViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
this.imageView = itemView.findViewById(R.id.emoji_image);
|
||||
this.textView = itemView.findViewById(R.id.emoji_text);
|
||||
this.hintCorner = itemView.findViewById(R.id.emoji_variation_hint);
|
||||
}
|
||||
}
|
||||
|
||||
public interface VariationSelectorListener {
|
||||
void onVariationSelectorStateChanged(boolean open);
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,258 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.util.Log;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiDrawInfo;
|
||||
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiPageBitmap;
|
||||
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
|
||||
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiTree;
|
||||
import org.thoughtcrime.securesms.util.FutureTaskListener;
|
||||
import org.thoughtcrime.securesms.util.Pair;
|
||||
import org.thoughtcrime.securesms.util.StringUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class EmojiProvider {
|
||||
|
||||
private static final String TAG = EmojiProvider.class.getSimpleName();
|
||||
private static volatile EmojiProvider instance = null;
|
||||
private static final Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG);
|
||||
private static final Pattern EMOJI_PATTERN = Pattern.compile("^(?:(?:[\u00a9\u00ae\u203c\u2049\u2122\u2139\u2194-\u2199\u21a9-\u21aa\u231a-\u231b\u2328\u23cf\u23e9-\u23f3\u23f8-\u23fa\u24c2\u25aa-\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614-\u2615\u2618\u261d\u2620\u2622-\u2623\u2626\u262a\u262e-\u262f\u2638-\u263a\u2648-\u2653\u2660\u2663\u2665-\u2666\u2668\u267b\u267f\u2692-\u2694\u2696-\u2697\u2699\u269b-\u269c\u26a0-\u26a1\u26aa-\u26ab\u26b0-\u26b1\u26bd-\u26be\u26c4-\u26c5\u26c8\u26ce-\u26cf\u26d1\u26d3-\u26d4\u26e9-\u26ea\u26f0-\u26f5\u26f7-\u26fa\u26fd\u2702\u2705\u2708-\u270d\u270f\u2712\u2714\u2716\u271d\u2721\u2728\u2733-\u2734\u2744\u2747\u274c\u274e\u2753-\u2755\u2757\u2763-\u2764\u2795-\u2797\u27a1\u27b0\u27bf\u2934-\u2935\u2b05-\u2b07\u2b1b-\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299\ud83c\udc04\ud83c\udccf\ud83c\udd70-\ud83c\udd71\ud83c\udd7e-\ud83c\udd7f\ud83c\udd8e\ud83c\udd91-\ud83c\udd9a\ud83c\ude01-\ud83c\ude02\ud83c\ude1a\ud83c\ude2f\ud83c\ude32-\ud83c\ude3a\ud83c\ude50-\ud83c\ude51\u200d\ud83c\udf00-\ud83d\uddff\ud83d\ude00-\ud83d\ude4f\ud83d\ude80-\ud83d\udeff\ud83e\udd00-\ud83e\uddff\udb40\udc20-\udb40\udc7f]|\u200d[\u2640\u2642]|[\ud83c\udde6-\ud83c\uddff]{2}|.[\u20e0\u20e3\ufe0f]+)+)+$");
|
||||
|
||||
private final EmojiTree emojiTree = new EmojiTree();
|
||||
|
||||
private static final int EMOJI_RAW_HEIGHT = 64;
|
||||
private static final int EMOJI_RAW_WIDTH = 64;
|
||||
private static final int EMOJI_VERT_PAD = 0;
|
||||
private static final int EMOJI_PER_ROW = 16;
|
||||
|
||||
private final float decodeScale;
|
||||
private final float verticalPad;
|
||||
|
||||
public static EmojiProvider getInstance(Context context) {
|
||||
if (instance == null) {
|
||||
synchronized (EmojiProvider.class) {
|
||||
if (instance == null) {
|
||||
instance = new EmojiProvider(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
private EmojiProvider(Context context) {
|
||||
this.decodeScale = Math.min(1f, context.getResources().getDimension(R.dimen.emoji_drawer_size) / EMOJI_RAW_HEIGHT);
|
||||
this.verticalPad = EMOJI_VERT_PAD * this.decodeScale;
|
||||
|
||||
for (EmojiPageModel page : EmojiPages.DATA_PAGES) {
|
||||
if (page.hasSpriteMap()) {
|
||||
EmojiPageBitmap pageBitmap = new EmojiPageBitmap(context, page, decodeScale);
|
||||
|
||||
List<String> emojis = page.getEmoji();
|
||||
for (int i = 0; i < emojis.size(); i++) {
|
||||
emojiTree.add(emojis.get(i), new EmojiDrawInfo(pageBitmap, i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (Pair<String,String> obsolete : EmojiPages.OBSOLETE) {
|
||||
emojiTree.add(obsolete.first(), emojiTree.getEmoji(obsolete.second(), 0, obsolete.second().length()));
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable EmojiParser.CandidateList getCandidates(@Nullable CharSequence text) {
|
||||
if (text == null) return null;
|
||||
return new EmojiParser(emojiTree).findCandidates(text);
|
||||
}
|
||||
|
||||
@Nullable Spannable emojify(@Nullable CharSequence text, @NonNull TextView tv) {
|
||||
return emojify(getCandidates(text), text, tv, false);
|
||||
}
|
||||
|
||||
@Nullable Spannable emojify(@Nullable EmojiParser.CandidateList matches,
|
||||
@Nullable CharSequence text,
|
||||
@NonNull TextView tv,
|
||||
boolean background) {
|
||||
if (matches == null || text == null) return null;
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder(text);
|
||||
|
||||
for (EmojiParser.Candidate candidate : matches) {
|
||||
Drawable drawable = getEmojiDrawable(candidate.getDrawInfo(), background);
|
||||
|
||||
if (drawable != null) {
|
||||
builder.setSpan(new EmojiSpan(drawable, tv), candidate.getStartIndex(), candidate.getEndIndex(),
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
@Nullable Drawable getEmojiDrawable(CharSequence emoji) {
|
||||
EmojiDrawInfo drawInfo = emojiTree.getEmoji(emoji, 0, emoji.length());
|
||||
return getEmojiDrawable(drawInfo, false);
|
||||
}
|
||||
|
||||
public boolean isEmoji(CharSequence emoji) {
|
||||
return emojiTree.getEmoji(emoji, 0, emoji.length()) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the text is likely a single, valid emoji. Otherwise false.
|
||||
*
|
||||
* We do a two-tier check: first using our own knowledge of emojis (which could be incomplete),
|
||||
* followed by a more wide check for all of the valid emoji unicode ranges (which could lead to
|
||||
* some false positives). YMMV.
|
||||
*/
|
||||
public boolean maybeEmoji(CharSequence emoji) {
|
||||
if (Util.isEmpty(emoji)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (StringUtil.getGraphemeCount(emoji) != 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isEmoji(emoji) || EMOJI_PATTERN.matcher(emoji).matches();
|
||||
}
|
||||
|
||||
public @Nullable Bitmap getEmojiBitmap(CharSequence emoji, float scale, boolean background) {
|
||||
EmojiDrawInfo drawInfo = emojiTree.getEmoji(emoji, 0, emoji.length());
|
||||
EmojiDrawable drawable = ((EmojiDrawable) getEmojiDrawable(drawInfo, background));
|
||||
if (drawable != null) {
|
||||
return drawable.getEmojiBitmap(scale);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected @Nullable Drawable getEmojiDrawable(@Nullable EmojiDrawInfo drawInfo, boolean background) {
|
||||
if (drawInfo == null) {
|
||||
return null;
|
||||
}
|
||||
final EmojiDrawable drawable = new EmojiDrawable(drawInfo, decodeScale);
|
||||
if (background) {
|
||||
try {
|
||||
drawable.setBitmap(drawInfo.getPage().loadPage(), background);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
drawInfo.getPage().get().addListener(new FutureTaskListener<Bitmap>() {
|
||||
@Override public void onSuccess(final Bitmap result) {
|
||||
Util.runOnMain(() -> drawable.setBitmap(result));
|
||||
}
|
||||
|
||||
@Override public void onFailure(ExecutionException error) {
|
||||
Log.w(TAG, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
return drawable;
|
||||
}
|
||||
|
||||
class EmojiDrawable extends Drawable {
|
||||
private final EmojiDrawInfo info;
|
||||
private Bitmap bmp;
|
||||
private final float intrinsicWidth;
|
||||
private final float intrinsicHeight;
|
||||
|
||||
@Override
|
||||
public int getIntrinsicWidth() {
|
||||
return (int)intrinsicWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicHeight() {
|
||||
return (int)intrinsicHeight;
|
||||
}
|
||||
|
||||
EmojiDrawable(EmojiDrawInfo info, float decodeScale) {
|
||||
this.info = info;
|
||||
this.intrinsicWidth = EMOJI_RAW_WIDTH * decodeScale;
|
||||
this.intrinsicHeight = EMOJI_RAW_HEIGHT * decodeScale;
|
||||
}
|
||||
|
||||
private Bitmap getEmojiBitmap(float scale) {
|
||||
Bitmap singleEmoji = Bitmap.createBitmap((int) (intrinsicWidth * scale), (int) (intrinsicHeight*scale), Bitmap.Config.ARGB_8888);
|
||||
|
||||
final int row = info.getIndex() / EMOJI_PER_ROW;
|
||||
final int rowIndex = info.getIndex() % EMOJI_PER_ROW;
|
||||
|
||||
Rect desRect = new Rect(0, 0, (int) intrinsicWidth, (int) intrinsicWidth);
|
||||
Rect srcRect = new Rect((int)(rowIndex * intrinsicWidth),
|
||||
(int)(row * intrinsicHeight + row * verticalPad)+1,
|
||||
(int)(((rowIndex + 1) * intrinsicWidth)-1),
|
||||
(int)((row + 1) * intrinsicHeight + row * verticalPad)-1);
|
||||
|
||||
Canvas canvas = new Canvas(singleEmoji);
|
||||
canvas.scale(scale, scale);
|
||||
canvas.drawBitmap(bmp, srcRect, desRect, paint);
|
||||
|
||||
return singleEmoji;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas) {
|
||||
if (bmp == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int row = info.getIndex() / EMOJI_PER_ROW;
|
||||
final int row_index = info.getIndex() % EMOJI_PER_ROW;
|
||||
|
||||
canvas.drawBitmap(bmp,
|
||||
new Rect((int)(row_index * intrinsicWidth),
|
||||
(int)(row * intrinsicHeight + row * verticalPad)+1,
|
||||
(int)(((row_index + 1) * intrinsicWidth)-1),
|
||||
(int)((row + 1) * intrinsicHeight + row * verticalPad)-1),
|
||||
getBounds(),
|
||||
paint);
|
||||
}
|
||||
|
||||
public void setBitmap(Bitmap bitmap) {
|
||||
setBitmap(bitmap, false);
|
||||
}
|
||||
|
||||
public void setBitmap(Bitmap bitmap, boolean background) {
|
||||
if (!background) {
|
||||
Util.assertMainThread();
|
||||
}
|
||||
if (bmp == null || !bmp.sameAs(bitmap)) {
|
||||
bmp = bitmap;
|
||||
invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return PixelFormat.TRANSLUCENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha) { }
|
||||
|
||||
@Override
|
||||
public void setColorFilter(ColorFilter cf) { }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Paint.FontMetricsInt;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public class EmojiSpan extends AnimatingImageSpan {
|
||||
|
||||
private final float SHIFT_FACTOR = 1.5f;
|
||||
|
||||
private final int size;
|
||||
private final FontMetricsInt fm;
|
||||
|
||||
public EmojiSpan(@NonNull Drawable drawable, @NonNull TextView tv) {
|
||||
super(drawable, tv);
|
||||
fm = tv.getPaint().getFontMetricsInt();
|
||||
size = fm != null ? Math.abs(fm.descent) + Math.abs(fm.ascent)
|
||||
: tv.getResources().getDimensionPixelSize(R.dimen.conversation_item_body_text_size);
|
||||
getDrawable().setBounds(0, 0, size, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, FontMetricsInt fm) {
|
||||
if (fm != null && this.fm != null) {
|
||||
fm.ascent = this.fm.ascent;
|
||||
fm.descent = this.fm.descent;
|
||||
fm.top = this.fm.top;
|
||||
fm.bottom = this.fm.bottom;
|
||||
fm.leading = this.fm.leading;
|
||||
return size;
|
||||
} else {
|
||||
return super.getSize(paint, text, start, end, fm);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) {
|
||||
int height = bottom - top;
|
||||
int centeringMargin = (height - size) / 2;
|
||||
int adjustedMargin = (int) (centeringMargin * SHIFT_FACTOR);
|
||||
super.draw(canvas, text, start, end, x, top, y, bottom - adjustedMargin, paint);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
public final class EmojiStrings {
|
||||
public static final String BUST_IN_SILHOUETTE = "\uD83D\uDC64";
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.URLSpan;
|
||||
import android.text.util.Linkify;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
import androidx.core.widget.TextViewCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
|
||||
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
|
||||
import org.thoughtcrime.securesms.util.LongClickCopySpan;
|
||||
import org.thoughtcrime.securesms.util.Prefs;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.guava.Optional;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
public class EmojiTextView extends AppCompatTextView {
|
||||
|
||||
private final boolean scaleEmojis;
|
||||
private final boolean forceCustom;
|
||||
private final boolean createInBackground;
|
||||
|
||||
private static final char ELLIPSIS = '…';
|
||||
|
||||
private CharSequence previousText;
|
||||
private BufferType previousBufferType;
|
||||
private float originalFontSize;
|
||||
private boolean useSystemEmoji;
|
||||
private boolean sizeChangeInProgress;
|
||||
private final int maxLength;
|
||||
private CharSequence overflowText;
|
||||
private CharSequence previousOverflowText;
|
||||
|
||||
public EmojiTextView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public EmojiTextView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiTextView, 0, 0);
|
||||
scaleEmojis = a.getBoolean(R.styleable.EmojiTextView_scaleEmojis, false);
|
||||
maxLength = a.getInteger(R.styleable.EmojiTextView_emoji_maxLength, -1);
|
||||
forceCustom = a.getBoolean(R.styleable.EmojiTextView_emoji_forceCustom, false);
|
||||
createInBackground = a.getBoolean(R.styleable.EmojiTextView_createInBackground, false);
|
||||
a.recycle();
|
||||
|
||||
a = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.textSize});
|
||||
originalFontSize = a.getDimensionPixelSize(0, 0);
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
@Override public void setText(@Nullable CharSequence text, BufferType type) {
|
||||
EmojiProvider provider = EmojiProvider.getInstance(getContext());
|
||||
EmojiParser.CandidateList candidates = provider.getCandidates(text);
|
||||
|
||||
if (scaleEmojis && candidates != null && candidates.allEmojis) {
|
||||
int emojis = candidates.size();
|
||||
float scale = 1.0f;
|
||||
|
||||
if (emojis <= 8) scale += 0.25f;
|
||||
if (emojis <= 6) scale += 0.25f;
|
||||
if (emojis <= 4) scale += 0.25f;
|
||||
if (emojis <= 2) scale += 0.25f;
|
||||
|
||||
super.setTextSize(TypedValue.COMPLEX_UNIT_PX, originalFontSize * scale);
|
||||
} else if (scaleEmojis) {
|
||||
super.setTextSize(TypedValue.COMPLEX_UNIT_PX, originalFontSize);
|
||||
}
|
||||
|
||||
if (unchanged(text, overflowText, type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
previousText = text;
|
||||
previousOverflowText = overflowText;
|
||||
previousBufferType = type;
|
||||
useSystemEmoji = useSystemEmoji();
|
||||
|
||||
if (useSystemEmoji || candidates == null || candidates.size() == 0) {
|
||||
super.setText(new SpannableStringBuilder(Optional.fromNullable(text).or("")).append(Optional.fromNullable(overflowText).or("")), BufferType.NORMAL);
|
||||
|
||||
if (getEllipsize() == TextUtils.TruncateAt.END && maxLength > 0) {
|
||||
ellipsizeAnyTextForMaxLength();
|
||||
}
|
||||
} else {
|
||||
CharSequence emojified = provider.emojify(candidates, text, this, createInBackground);
|
||||
super.setText(new SpannableStringBuilder(emojified).append(Optional.fromNullable(overflowText).or("")), BufferType.SPANNABLE);
|
||||
|
||||
// Android fails to ellipsize spannable strings. (https://issuetracker.google.com/issues/36991688)
|
||||
// We ellipsize them ourselves by manually truncating the appropriate section.
|
||||
if (getEllipsize() == TextUtils.TruncateAt.END) {
|
||||
if (maxLength > 0) {
|
||||
ellipsizeAnyTextForMaxLength();
|
||||
} else {
|
||||
ellipsizeEmojiTextForMaxLines();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setOverflowText(@Nullable CharSequence overflowText) {
|
||||
this.overflowText = overflowText;
|
||||
setText(previousText, BufferType.SPANNABLE);
|
||||
}
|
||||
|
||||
private void ellipsizeAnyTextForMaxLength() {
|
||||
if (maxLength > 0 && getText().length() > maxLength + 1) {
|
||||
SpannableStringBuilder newContent = new SpannableStringBuilder();
|
||||
newContent.append(getText().subSequence(0, maxLength)).append(ELLIPSIS).append(Optional.fromNullable(overflowText).or(""));
|
||||
|
||||
EmojiParser.CandidateList newCandidates = EmojiProvider.getInstance(getContext()).getCandidates(newContent);
|
||||
|
||||
if (useSystemEmoji || newCandidates == null || newCandidates.size() == 0) {
|
||||
super.setText(newContent, BufferType.NORMAL);
|
||||
} else {
|
||||
CharSequence emojified = EmojiProvider.getInstance(getContext()).emojify(newCandidates, newContent, this, createInBackground);
|
||||
super.setText(emojified, BufferType.SPANNABLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ellipsizeEmojiTextForMaxLines() {
|
||||
post(() -> {
|
||||
if (getLayout() == null) {
|
||||
ellipsizeEmojiTextForMaxLines();
|
||||
return;
|
||||
}
|
||||
|
||||
int maxLines = TextViewCompat.getMaxLines(EmojiTextView.this);
|
||||
if (maxLines <= 0 && maxLength < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int lineCount = getLineCount();
|
||||
if (lineCount > maxLines) {
|
||||
int overflowStart = getLayout().getLineStart(maxLines - 1);
|
||||
CharSequence overflow = getText().subSequence(overflowStart, getText().length());
|
||||
CharSequence ellipsized = TextUtils.ellipsize(overflow, getPaint(), getWidth(), TextUtils.TruncateAt.END);
|
||||
|
||||
SpannableStringBuilder newContent = new SpannableStringBuilder();
|
||||
newContent.append(getText().subSequence(0, overflowStart))
|
||||
.append(ellipsized.subSequence(0, ellipsized.length()))
|
||||
.append(Optional.fromNullable(overflowText).or(""));
|
||||
|
||||
EmojiParser.CandidateList newCandidates = EmojiProvider.getInstance(getContext()).getCandidates(newContent);
|
||||
CharSequence emojified = EmojiProvider.getInstance(getContext()).emojify(newCandidates, newContent, this, createInBackground);
|
||||
|
||||
super.setText(emojified, BufferType.SPANNABLE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean unchanged(CharSequence text, CharSequence overflowText, BufferType bufferType) {
|
||||
return Util.equals(previousText, text) &&
|
||||
Util.equals(previousOverflowText, overflowText) &&
|
||||
Util.equals(previousBufferType, bufferType) &&
|
||||
useSystemEmoji == useSystemEmoji() &&
|
||||
!sizeChangeInProgress;
|
||||
}
|
||||
|
||||
private boolean useSystemEmoji() {
|
||||
return !forceCustom && Prefs.isSystemEmojiPreferred(getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
|
||||
if (!sizeChangeInProgress) {
|
||||
sizeChangeInProgress = true;
|
||||
setText(previousText, previousBufferType);
|
||||
sizeChangeInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateDrawable(@NonNull Drawable drawable) {
|
||||
if (drawable instanceof EmojiDrawable) invalidate();
|
||||
else super.invalidateDrawable(drawable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTextSize(float size) {
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTextSize(int unit, float size) {
|
||||
this.originalFontSize = TypedValue.applyDimension(unit, size, getResources().getDisplayMetrics());
|
||||
super.setTextSize(unit, size);
|
||||
}
|
||||
|
||||
|
||||
// tools for linkify
|
||||
|
||||
private static final Pattern CMD_PATTERN = Pattern.compile("(?<=^|\\s)/[a-zA-Z][a-zA-Z@\\d_/.-]{0,254}");
|
||||
private static final Pattern CUSTOM_PATTERN = Pattern.compile("(?<=^|\\s)(OPENPGP4FPR|openpgp4fpr|mumble):[^ \\n]+");
|
||||
private static final Pattern PROXY_PATTERN = Pattern.compile("(?<=^|\\s)(SOCKS5|socks5|ss|SS):[^ \\n]+");
|
||||
|
||||
private static void replaceURLSpan(Spannable messageBody) {
|
||||
URLSpan[] urlSpans = messageBody.getSpans(0, messageBody.length(), URLSpan.class);
|
||||
for (URLSpan urlSpan : urlSpans) {
|
||||
int start = messageBody.getSpanStart(urlSpan);
|
||||
int end = messageBody.getSpanEnd(urlSpan);
|
||||
// LongClickCopySpan must not be derived from URLSpan, otherwise links will be removed on the next addLinks() call
|
||||
messageBody.setSpan(new LongClickCopySpan(urlSpan.getURL()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
|
||||
public static Spannable linkify(Spannable messageBody) {
|
||||
// linkify commands such as `/echo` -
|
||||
// do this first to avoid `/xkcd_123456` to be treated partly as a phone number
|
||||
Linkify.addLinks(messageBody, CMD_PATTERN, "cmd:", null, null);
|
||||
EmojiTextView.replaceURLSpan(messageBody); // replace URLSpan so that it is not removed on the next addLinks() call
|
||||
|
||||
Linkify.addLinks(messageBody, CUSTOM_PATTERN, null, null, null);
|
||||
EmojiTextView.replaceURLSpan(messageBody);
|
||||
|
||||
if (Linkify.addLinks(messageBody, PROXY_PATTERN, null, null, null)) {
|
||||
EmojiTextView.replaceURLSpan(messageBody); // replace URLSpan so that it is not removed on the next addLinks() call
|
||||
}
|
||||
|
||||
// linkyfiy urls etc., this removes all existing URLSpan
|
||||
if (Linkify.addLinks(messageBody, Linkify.EMAIL_ADDRESSES|Linkify.WEB_URLS|Linkify.PHONE_NUMBERS)) {
|
||||
EmojiTextView.replaceURLSpan(messageBody);
|
||||
}
|
||||
|
||||
return messageBody;
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,14 @@ package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.AppCompatImageButton;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.appcompat.widget.AppCompatImageButton;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ResUtil;
|
||||
|
||||
public class EmojiToggle extends AppCompatImageButton implements MediaKeyboard.MediaKeyboardListener {
|
||||
public class EmojiToggle extends AppCompatImageButton {
|
||||
|
||||
private Drawable emojiToggle;
|
||||
// private Drawable stickerToggle;
|
||||
@@ -50,10 +50,6 @@ public class EmojiToggle extends AppCompatImageButton implements MediaKeyboard.M
|
||||
setToMedia();
|
||||
}
|
||||
|
||||
public void attach(MediaKeyboard drawer) {
|
||||
drawer.setKeyboardListener(this);
|
||||
}
|
||||
|
||||
public void setStickerMode(boolean stickerMode) {
|
||||
this.mediaToggle = /*stickerMode ? stickerToggle :*/ emojiToggle;
|
||||
|
||||
@@ -66,20 +62,4 @@ public class EmojiToggle extends AppCompatImageButton implements MediaKeyboard.M
|
||||
//return this.mediaToggle == stickerToggle;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override public void onShown() {
|
||||
setToIme();
|
||||
}
|
||||
|
||||
@Override public void onHidden() {
|
||||
setToMedia();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKeyboardProviderChanged(@NonNull MediaKeyboardProvider provider) {
|
||||
setStickerMode(false);
|
||||
//setStickerMode(provider instanceof StickerKeyboardProvider);
|
||||
/*TextSecurePreferences.setMediaKeyboardMode(getContext(), (provider instanceof StickerKeyboardProvider) ? TextSecurePreferences.MediaKeyboardMode.STICKER
|
||||
: TextSecurePreferences.MediaKeyboardMode.EMOJI);*/
|
||||
}
|
||||
}
|
||||
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupWindow;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider.EmojiEventListener;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class EmojiVariationSelectorPopup extends PopupWindow {
|
||||
|
||||
private final Context context;
|
||||
private final ViewGroup list;
|
||||
private final EmojiEventListener listener;
|
||||
|
||||
public EmojiVariationSelectorPopup(@NonNull Context context, @NonNull EmojiEventListener listener) {
|
||||
super(LayoutInflater.from(context).inflate(R.layout.emoji_variation_selector, null),
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
this.context = context;
|
||||
this.listener = listener;
|
||||
this.list = (ViewGroup) getContentView().findViewById(R.id.emoji_variation_container);
|
||||
|
||||
setBackgroundDrawable(null);
|
||||
setOutsideTouchable(true);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
setElevation(20);
|
||||
}
|
||||
}
|
||||
|
||||
public void setVariations(List<String> variations) {
|
||||
list.removeAllViews();
|
||||
|
||||
for (String variation : variations) {
|
||||
ImageView imageView = (ImageView) LayoutInflater.from(context).inflate(R.layout.emoji_variation_selector_item, list, false);
|
||||
imageView.setImageDrawable(EmojiProvider.getInstance(context).getEmojiDrawable(variation));
|
||||
imageView.setOnClickListener(v -> {
|
||||
listener.onEmojiSelected(variation);
|
||||
dismiss();
|
||||
});
|
||||
list.addView(imageView);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,74 +1,33 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.emoji2.emojipicker.EmojiPickerView;
|
||||
import androidx.emoji2.emojipicker.EmojiViewItem;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.InputAwareLayout.InputView;
|
||||
import org.thoughtcrime.securesms.components.RepeatableImageKey;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class MediaKeyboard extends FrameLayout implements InputView,
|
||||
MediaKeyboardProvider.Presenter,
|
||||
MediaKeyboardProvider.Controller,
|
||||
MediaKeyboardBottomTabAdapter.EventListener
|
||||
{
|
||||
public class MediaKeyboard extends FrameLayout implements InputView, Consumer<EmojiViewItem> {
|
||||
|
||||
private static final String TAG = MediaKeyboard.class.getSimpleName();
|
||||
|
||||
private RecyclerView categoryTabs;
|
||||
private ViewPager categoryPager;
|
||||
private ViewGroup providerTabs;
|
||||
private RepeatableImageKey backspaceButton;
|
||||
private RepeatableImageKey backspaceButtonBackup;
|
||||
private View searchButton;
|
||||
private View addButton;
|
||||
@Nullable private MediaKeyboardListener keyboardListener;
|
||||
private MediaKeyboardProvider[] providers;
|
||||
private int providerIndex;
|
||||
private EmojiPickerView emojiPicker;
|
||||
|
||||
private final boolean tabsAtBottom;
|
||||
|
||||
private MediaKeyboardBottomTabAdapter categoryTabAdapter;
|
||||
|
||||
public MediaKeyboard(Context context) {
|
||||
this(context, null);
|
||||
public MediaKeyboard(@NonNull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public MediaKeyboard(Context context, AttributeSet attrs) {
|
||||
public MediaKeyboard(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MediaKeyboard, 0, 0);
|
||||
|
||||
try {
|
||||
tabsAtBottom = typedArray.getInt(R.styleable.MediaKeyboard_tabs_gravity, 0) == 0;
|
||||
} finally {
|
||||
typedArray.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
public void setProviders(int startIndex, MediaKeyboardProvider... providers) {
|
||||
if (!Arrays.equals(this.providers, providers)) {
|
||||
this.providers = providers;
|
||||
this.providerIndex = startIndex;
|
||||
|
||||
requestPresent(providers, providerIndex);
|
||||
}
|
||||
}
|
||||
|
||||
public void setKeyboardListener(@Nullable MediaKeyboardListener listener) {
|
||||
@@ -82,8 +41,6 @@ public class MediaKeyboard extends FrameLayout implements InputView,
|
||||
|
||||
@Override
|
||||
public void show(int height, boolean immediate) {
|
||||
if (this.categoryPager == null) initView();
|
||||
|
||||
ViewGroup.LayoutParams params = getLayoutParams();
|
||||
params.height = height;
|
||||
Log.i(TAG, "showing emoji drawer with height " + params.height);
|
||||
@@ -93,12 +50,12 @@ public class MediaKeyboard extends FrameLayout implements InputView,
|
||||
}
|
||||
|
||||
public void show() {
|
||||
if (this.categoryPager == null) initView();
|
||||
|
||||
if (emojiPicker == null) {
|
||||
emojiPicker = findViewById(R.id.emoji_picker);
|
||||
emojiPicker.setOnEmojiPickedListener(this);
|
||||
};
|
||||
setVisibility(VISIBLE);
|
||||
if (keyboardListener != null) keyboardListener.onShown();
|
||||
|
||||
requestPresent(providers, providerIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -109,188 +66,13 @@ public class MediaKeyboard extends FrameLayout implements InputView,
|
||||
}
|
||||
|
||||
@Override
|
||||
public void present(@NonNull MediaKeyboardProvider provider,
|
||||
@NonNull PagerAdapter pagerAdapter,
|
||||
@NonNull MediaKeyboardProvider.TabIconProvider tabIconProvider,
|
||||
@Nullable MediaKeyboardProvider.BackspaceObserver backspaceObserver,
|
||||
@Nullable MediaKeyboardProvider.AddObserver addObserver,
|
||||
@Nullable MediaKeyboardProvider.SearchObserver searchObserver,
|
||||
int startingIndex)
|
||||
{
|
||||
if (categoryPager == null) return;
|
||||
if (!provider.equals(providers[providerIndex])) return;
|
||||
if (keyboardListener != null) keyboardListener.onKeyboardProviderChanged(provider);
|
||||
|
||||
boolean isSolo = providers.length == 1;
|
||||
|
||||
presentProviderStrip(isSolo);
|
||||
presentCategoryPager(pagerAdapter, tabIconProvider, startingIndex);
|
||||
presentProviderTabs(providers, providerIndex);
|
||||
presentSearchButton(searchObserver);
|
||||
presentBackspaceButton(backspaceObserver, isSolo);
|
||||
presentAddButton(addObserver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCurrentPosition() {
|
||||
return categoryPager != null ? categoryPager.getCurrentItem() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestDismissal() {
|
||||
hide(true);
|
||||
providerIndex = 0;
|
||||
if (keyboardListener != null) keyboardListener.onKeyboardProviderChanged(providers[providerIndex]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVisible() {
|
||||
return getVisibility() == View.VISIBLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabSelected(int index) {
|
||||
if (categoryPager != null) {
|
||||
categoryPager.setCurrentItem(index);
|
||||
categoryTabs.smoothScrollToPosition(index);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setViewPagerEnabled(boolean enabled) {
|
||||
if (categoryPager != null) {
|
||||
categoryPager.setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
private void initView() {
|
||||
final View view = LayoutInflater.from(getContext()).inflate(R.layout.media_keyboard, this, true);
|
||||
|
||||
RecyclerView categoryTabsTop = view.findViewById(R.id.media_keyboard_tabs_top);
|
||||
RecyclerView categoryTabsBottom = view.findViewById(R.id.media_keyboard_tabs);
|
||||
|
||||
this.categoryTabs = tabsAtBottom ? categoryTabsBottom : categoryTabsTop;
|
||||
this.categoryPager = view.findViewById(R.id.media_keyboard_pager);
|
||||
this.providerTabs = view.findViewById(R.id.media_keyboard_provider_tabs);
|
||||
this.backspaceButton = view.findViewById(R.id.media_keyboard_backspace);
|
||||
this.backspaceButtonBackup = view.findViewById(R.id.media_keyboard_backspace_backup);
|
||||
this.searchButton = view.findViewById(R.id.media_keyboard_search);
|
||||
this.addButton = view.findViewById(R.id.media_keyboard_add);
|
||||
|
||||
this.categoryTabAdapter = new MediaKeyboardBottomTabAdapter(GlideApp.with(this), this, tabsAtBottom);
|
||||
|
||||
categoryTabs.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false));
|
||||
categoryTabs.setAdapter(categoryTabAdapter);
|
||||
categoryTabs.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
private void requestPresent(@NonNull MediaKeyboardProvider[] providers, int newIndex) {
|
||||
providers[providerIndex].setController(null);
|
||||
providerIndex = newIndex;
|
||||
|
||||
providers[providerIndex].setController(this);
|
||||
providers[providerIndex].requestPresentation(this, providers.length == 1);
|
||||
}
|
||||
|
||||
|
||||
private void presentCategoryPager(@NonNull PagerAdapter pagerAdapter,
|
||||
@NonNull MediaKeyboardProvider.TabIconProvider iconProvider,
|
||||
int startingIndex) {
|
||||
if (categoryPager.getAdapter() != pagerAdapter) {
|
||||
categoryPager.setAdapter(pagerAdapter);
|
||||
}
|
||||
|
||||
categoryPager.setCurrentItem(startingIndex);
|
||||
|
||||
categoryPager.clearOnPageChangeListeners();
|
||||
categoryPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
|
||||
@Override
|
||||
public void onPageScrolled(int i, float v, int i1) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageSelected(int i) {
|
||||
categoryTabAdapter.setActivePosition(i);
|
||||
categoryTabs.smoothScrollToPosition(i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int i) {
|
||||
}
|
||||
});
|
||||
|
||||
categoryTabAdapter.setTabIconProvider(iconProvider, pagerAdapter.getCount());
|
||||
categoryTabAdapter.setActivePosition(startingIndex);
|
||||
}
|
||||
|
||||
private void presentProviderTabs(@NonNull MediaKeyboardProvider[] providers, int selected) {
|
||||
providerTabs.removeAllViews();
|
||||
|
||||
LayoutInflater inflater = LayoutInflater.from(getContext());
|
||||
|
||||
for (int i = 0; i < providers.length; i++) {
|
||||
MediaKeyboardProvider provider = providers[i];
|
||||
View view = inflater.inflate(provider.getProviderIconView(i == selected), providerTabs, false);
|
||||
|
||||
view.setTag(provider);
|
||||
|
||||
final int index = i;
|
||||
view.setOnClickListener(v -> {
|
||||
requestPresent(providers, index);
|
||||
});
|
||||
|
||||
providerTabs.addView(view);
|
||||
}
|
||||
}
|
||||
|
||||
private void presentBackspaceButton(@Nullable MediaKeyboardProvider.BackspaceObserver backspaceObserver,
|
||||
boolean useBackupPosition)
|
||||
{
|
||||
if (backspaceObserver != null) {
|
||||
if (useBackupPosition) {
|
||||
backspaceButton.setVisibility(INVISIBLE);
|
||||
backspaceButton.setOnKeyEventListener(null);
|
||||
backspaceButtonBackup.setVisibility(VISIBLE);
|
||||
backspaceButtonBackup.setOnKeyEventListener(backspaceObserver::onBackspaceClicked);
|
||||
} else {
|
||||
backspaceButton.setVisibility(VISIBLE);
|
||||
backspaceButton.setOnKeyEventListener(backspaceObserver::onBackspaceClicked);
|
||||
backspaceButtonBackup.setVisibility(GONE);
|
||||
backspaceButtonBackup.setOnKeyEventListener(null);
|
||||
}
|
||||
} else {
|
||||
backspaceButton.setVisibility(INVISIBLE);
|
||||
backspaceButton.setOnKeyEventListener(null);
|
||||
backspaceButtonBackup.setVisibility(GONE);
|
||||
backspaceButton.setOnKeyEventListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void presentAddButton(@Nullable MediaKeyboardProvider.AddObserver addObserver) {
|
||||
if (addObserver != null) {
|
||||
addButton.setVisibility(VISIBLE);
|
||||
addButton.setOnClickListener(v -> addObserver.onAddClicked());
|
||||
} else {
|
||||
addButton.setVisibility(GONE);
|
||||
addButton.setOnClickListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void presentSearchButton(@Nullable MediaKeyboardProvider.SearchObserver searchObserver) {
|
||||
searchButton.setVisibility(searchObserver != null ? VISIBLE : INVISIBLE);
|
||||
}
|
||||
|
||||
private void presentProviderStrip(boolean isSolo) {
|
||||
int visibility = isSolo ? View.GONE : View.VISIBLE;
|
||||
|
||||
searchButton.setVisibility(visibility);
|
||||
backspaceButton.setVisibility(visibility);
|
||||
providerTabs.setVisibility(visibility);
|
||||
public void accept(EmojiViewItem emojiViewItem) {
|
||||
if (keyboardListener != null) keyboardListener.onEmojiPicked(emojiViewItem.getEmoji());
|
||||
}
|
||||
|
||||
public interface MediaKeyboardListener {
|
||||
void onShown();
|
||||
void onHidden();
|
||||
void onKeyboardProviderChanged(@NonNull MediaKeyboardProvider provider);
|
||||
void onEmojiPicked(String emoji);
|
||||
}
|
||||
}
|
||||
|
||||
-103
@@ -1,103 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboardProvider.TabIconProvider;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
public class MediaKeyboardBottomTabAdapter extends RecyclerView.Adapter<MediaKeyboardBottomTabAdapter.MediaKeyboardBottomTabViewHolder> {
|
||||
|
||||
private final GlideRequests glideRequests;
|
||||
private final EventListener eventListener;
|
||||
private final boolean highlightTop;
|
||||
|
||||
private TabIconProvider tabIconProvider;
|
||||
private int activePosition;
|
||||
private int count;
|
||||
|
||||
public MediaKeyboardBottomTabAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener, boolean highlightTop) {
|
||||
this.glideRequests = glideRequests;
|
||||
this.eventListener = eventListener;
|
||||
this.highlightTop = highlightTop;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MediaKeyboardBottomTabViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
|
||||
return new MediaKeyboardBottomTabViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.media_keyboard_bottom_tab_item, viewGroup, false),
|
||||
highlightTop);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull MediaKeyboardBottomTabViewHolder viewHolder, int i) {
|
||||
viewHolder.bind(glideRequests, eventListener, tabIconProvider, i, i == activePosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull MediaKeyboardBottomTabViewHolder holder) {
|
||||
holder.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return count;
|
||||
}
|
||||
|
||||
public void setTabIconProvider(@NonNull TabIconProvider iconProvider, int count) {
|
||||
this.tabIconProvider = iconProvider;
|
||||
this.count = count;
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setActivePosition(int position) {
|
||||
this.activePosition = position;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
static class MediaKeyboardBottomTabViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final ImageView image;
|
||||
private final View indicator;
|
||||
|
||||
public MediaKeyboardBottomTabViewHolder(@NonNull View itemView, boolean highlightTop) {
|
||||
super(itemView);
|
||||
|
||||
View indicatorTop = itemView.findViewById(R.id.media_keyboard_top_tab_indicator);
|
||||
View indicatorBottom = itemView.findViewById(R.id.media_keyboard_bottom_tab_indicator);
|
||||
|
||||
this.image = itemView.findViewById(R.id.media_keyboard_bottom_tab_image);
|
||||
this.indicator = highlightTop ? indicatorTop : indicatorBottom;
|
||||
|
||||
this.indicator.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
void bind(@NonNull GlideRequests glideRequests,
|
||||
@NonNull EventListener eventListener,
|
||||
@NonNull TabIconProvider tabIconProvider,
|
||||
int index,
|
||||
boolean selected)
|
||||
{
|
||||
tabIconProvider.loadCategoryTabIcon(glideRequests, image, index);
|
||||
image.setAlpha(selected ? 1 : 0.5f);
|
||||
image.setSelected(selected);
|
||||
|
||||
indicator.setVisibility(selected ? View.VISIBLE : View.INVISIBLE);
|
||||
|
||||
itemView.setOnClickListener(v -> eventListener.onTabSelected(index));
|
||||
}
|
||||
|
||||
void recycle() {
|
||||
itemView.setOnClickListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
interface EventListener {
|
||||
void onTabSelected(int index);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
public interface MediaKeyboardProvider {
|
||||
@LayoutRes int getProviderIconView(boolean selected);
|
||||
/** @return True if the click was handled with provider-specific logic, otherwise false */
|
||||
void requestPresentation(@NonNull Presenter presenter, boolean isSoloProvider);
|
||||
void setController(@Nullable Controller controller);
|
||||
|
||||
interface BackspaceObserver {
|
||||
void onBackspaceClicked();
|
||||
}
|
||||
|
||||
interface AddObserver {
|
||||
void onAddClicked();
|
||||
}
|
||||
|
||||
interface SearchObserver {
|
||||
void onSearchOpened();
|
||||
void onSearchClosed();
|
||||
void onSearchChanged(@NonNull String query);
|
||||
}
|
||||
|
||||
interface Controller {
|
||||
void setViewPagerEnabled(boolean enabled);
|
||||
}
|
||||
|
||||
interface Presenter {
|
||||
void present(@NonNull MediaKeyboardProvider provider,
|
||||
@NonNull PagerAdapter pagerAdapter,
|
||||
@NonNull TabIconProvider iconProvider,
|
||||
@Nullable BackspaceObserver backspaceObserver,
|
||||
@Nullable AddObserver addObserver,
|
||||
@Nullable SearchObserver searchObserver,
|
||||
int startingIndex);
|
||||
int getCurrentPosition();
|
||||
void requestDismissal();
|
||||
boolean isVisible();
|
||||
}
|
||||
|
||||
interface TabIconProvider {
|
||||
void loadCategoryTabIcon(@NonNull GlideRequests glideRequests, @NonNull ImageView imageView, int index);
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.AsyncTask;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.fasterxml.jackson.databind.type.CollectionType;
|
||||
import com.fasterxml.jackson.databind.type.TypeFactory;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
|
||||
public class RecentEmojiPageModel implements EmojiPageModel {
|
||||
private static final String TAG = RecentEmojiPageModel.class.getSimpleName();
|
||||
private static final int EMOJI_LRU_SIZE = 50;
|
||||
|
||||
private final SharedPreferences prefs;
|
||||
private final String preferenceName;
|
||||
private final LinkedHashSet<String> recentlyUsed;
|
||||
|
||||
public RecentEmojiPageModel(Context context, @NonNull String preferenceName) {
|
||||
this.prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
this.preferenceName = preferenceName;
|
||||
this.recentlyUsed = getPersistedCache();
|
||||
}
|
||||
|
||||
private LinkedHashSet<String> getPersistedCache() {
|
||||
String serialized = prefs.getString(preferenceName, "[]");
|
||||
try {
|
||||
CollectionType collectionType = TypeFactory.defaultInstance()
|
||||
.constructCollectionType(LinkedHashSet.class, String.class);
|
||||
return JsonUtils.getMapper().readValue(serialized, collectionType);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return new LinkedHashSet<>();
|
||||
}
|
||||
}
|
||||
|
||||
@Override public int getIconAttr() {
|
||||
return R.attr.emoji_category_recent;
|
||||
}
|
||||
|
||||
@Override public List<String> getEmoji() {
|
||||
List<String> emoji = new ArrayList<>(recentlyUsed);
|
||||
Collections.reverse(emoji);
|
||||
return emoji;
|
||||
}
|
||||
|
||||
@Override public List<Emoji> getDisplayEmoji() {
|
||||
return Stream.of(getEmoji()).map(Emoji::new).toList();
|
||||
}
|
||||
|
||||
@Override public boolean hasSpriteMap() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override public String getSprite() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override public boolean isDynamic() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void onCodePointSelected(String emoji) {
|
||||
recentlyUsed.remove(emoji);
|
||||
recentlyUsed.add(emoji);
|
||||
|
||||
if (recentlyUsed.size() > EMOJI_LRU_SIZE) {
|
||||
Iterator<String> iterator = recentlyUsed.iterator();
|
||||
iterator.next();
|
||||
iterator.remove();
|
||||
}
|
||||
|
||||
final LinkedHashSet<String> latestRecentlyUsed = new LinkedHashSet<>(recentlyUsed);
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
try {
|
||||
String serialized = JsonUtils.toJson(latestRecentlyUsed);
|
||||
prefs.edit()
|
||||
.putString(preferenceName, serialized)
|
||||
.apply();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class StaticEmojiPageModel implements EmojiPageModel {
|
||||
@AttrRes private final int iconAttr;
|
||||
@NonNull private final List<Emoji> emoji;
|
||||
@Nullable private final String sprite;
|
||||
|
||||
public StaticEmojiPageModel(@AttrRes int iconAttr, @NonNull String[] strings, @Nullable String sprite) {
|
||||
List<Emoji> emoji = new ArrayList<>(strings.length);
|
||||
for (String s : strings) {
|
||||
emoji.add(new Emoji(s));
|
||||
}
|
||||
|
||||
this.iconAttr = iconAttr;
|
||||
this.emoji = emoji;
|
||||
this.sprite = sprite;
|
||||
}
|
||||
|
||||
public StaticEmojiPageModel(@AttrRes int iconAttr, @NonNull Emoji[] emoji, @Nullable String sprite) {
|
||||
this.iconAttr = iconAttr;
|
||||
this.emoji = Arrays.asList(emoji);
|
||||
this.sprite = sprite;
|
||||
}
|
||||
|
||||
public int getIconAttr() {
|
||||
return iconAttr;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<String> getEmoji() {
|
||||
List<String> emojis = new LinkedList<>();
|
||||
for (Emoji e : emoji) {
|
||||
emojis.addAll(e.getVariations());
|
||||
}
|
||||
return emojis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<Emoji> getDisplayEmoji() {
|
||||
return emoji;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSpriteMap() {
|
||||
return sprite != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getSprite() {
|
||||
return sprite;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDynamic() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji.parsing;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class EmojiDrawInfo {
|
||||
|
||||
private final EmojiPageBitmap page;
|
||||
private final int index;
|
||||
|
||||
public EmojiDrawInfo(final @NonNull EmojiPageBitmap page, final int index) {
|
||||
this.page = page;
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public @NonNull EmojiPageBitmap getPage() {
|
||||
return page;
|
||||
}
|
||||
|
||||
public int getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return "DrawInfo{" +
|
||||
"page=" + page +
|
||||
", index=" + index +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji.parsing;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.os.AsyncTask;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel;
|
||||
import org.thoughtcrime.securesms.util.ListenableFutureTask;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
public class EmojiPageBitmap {
|
||||
|
||||
private static final String TAG = EmojiPageBitmap.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
private final EmojiPageModel model;
|
||||
private final float decodeScale;
|
||||
|
||||
private SoftReference<Bitmap> bitmapReference;
|
||||
private ListenableFutureTask<Bitmap> task;
|
||||
|
||||
public EmojiPageBitmap(@NonNull Context context, @NonNull EmojiPageModel model, float decodeScale) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.model = model;
|
||||
this.decodeScale = decodeScale;
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public ListenableFutureTask<Bitmap> get() {
|
||||
Util.assertMainThread();
|
||||
if (bitmapReference != null && bitmapReference.get() != null) {
|
||||
return new ListenableFutureTask<>(bitmapReference.get());
|
||||
} else if (task != null) {
|
||||
return task;
|
||||
} else {
|
||||
Callable<Bitmap> callable = () -> {
|
||||
try {
|
||||
Log.i(TAG, "loading page " + model.getSprite());
|
||||
return loadPage();
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, ioe);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
task = new ListenableFutureTask<>(callable);
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override protected Void doInBackground(Void... params) {
|
||||
task.run();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override protected void onPostExecute(Void aVoid) {
|
||||
task = null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
public Bitmap loadPage() throws IOException {
|
||||
if (bitmapReference != null && bitmapReference.get() != null) return bitmapReference.get();
|
||||
|
||||
|
||||
float scale = decodeScale;
|
||||
AssetManager assetManager = context.getAssets();
|
||||
InputStream assetStream = assetManager.open(model.getSprite());
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
|
||||
if (Util.isLowMemory(context)) {
|
||||
Log.i(TAG, "Low memory detected. Changing sample size.");
|
||||
options.inSampleSize = 2;
|
||||
scale = decodeScale * 2;
|
||||
}
|
||||
|
||||
Stopwatch stopwatch = new Stopwatch(model.getSprite());
|
||||
Bitmap bitmap = BitmapFactory.decodeStream(assetStream, null, options);
|
||||
stopwatch.split("decode");
|
||||
|
||||
Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, (int)(bitmap.getWidth() * scale), (int)(bitmap.getHeight() * scale), true);
|
||||
stopwatch.split("scale");
|
||||
stopwatch.stop(TAG);
|
||||
|
||||
bitmapReference = new SoftReference<>(scaledBitmap);
|
||||
Log.i(TAG, "onPageLoaded(" + model.getSprite() + ") originalByteCount: " + bitmap.getByteCount()
|
||||
+ " scaledByteCount: " + scaledBitmap.getByteCount()
|
||||
+ " scaledSize: " + scaledBitmap.getWidth() + "x" + scaledBitmap.getHeight());
|
||||
return scaledBitmap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return model.getSprite();
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2014-present Vincent DURMONT vdurmont@gmail.com
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and associated documentation files (the "Software"),
|
||||
* to deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.emoji.parsing;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Based in part on code from emoji-java
|
||||
*/
|
||||
public class EmojiParser {
|
||||
|
||||
private final EmojiTree emojiTree;
|
||||
|
||||
public EmojiParser(EmojiTree emojiTree) {
|
||||
this.emojiTree = emojiTree;
|
||||
}
|
||||
|
||||
public @NonNull CandidateList findCandidates(@Nullable CharSequence text) {
|
||||
List<Candidate> results = new LinkedList<>();
|
||||
|
||||
if (text == null) {
|
||||
return new CandidateList(results, false);
|
||||
}
|
||||
|
||||
boolean allEmojis = text.length() > 0;
|
||||
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
int emojiEnd = getEmojiEndPos(text, i);
|
||||
|
||||
if (emojiEnd != -1) {
|
||||
EmojiDrawInfo drawInfo = emojiTree.getEmoji(text, i, emojiEnd);
|
||||
|
||||
if (emojiEnd + 2 <= text.length()) {
|
||||
if (Fitzpatrick.fitzpatrickFromUnicode(text, emojiEnd) != null) {
|
||||
emojiEnd += 2;
|
||||
}
|
||||
}
|
||||
|
||||
results.add(new Candidate(i, emojiEnd, drawInfo));
|
||||
|
||||
i = emojiEnd - 1;
|
||||
} else if (text.charAt(i) != ' '){
|
||||
allEmojis = false;
|
||||
}
|
||||
}
|
||||
|
||||
allEmojis &= !results.isEmpty();
|
||||
|
||||
return new CandidateList(results, allEmojis);
|
||||
}
|
||||
|
||||
private int getEmojiEndPos(CharSequence text, int startPos) {
|
||||
int best = -1;
|
||||
|
||||
for (int j = startPos + 1; j <= text.length(); j++) {
|
||||
EmojiTree.Matches status = emojiTree.isEmoji(text, startPos, j);
|
||||
|
||||
if (status.exactMatch()) {
|
||||
best = j;
|
||||
} else if (status.impossibleMatch()) {
|
||||
return best;
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
public static class Candidate {
|
||||
|
||||
private final int startIndex;
|
||||
private final int endIndex;
|
||||
private final EmojiDrawInfo drawInfo;
|
||||
|
||||
Candidate(int startIndex, int endIndex, EmojiDrawInfo drawInfo) {
|
||||
this.startIndex = startIndex;
|
||||
this.endIndex = endIndex;
|
||||
this.drawInfo = drawInfo;
|
||||
}
|
||||
|
||||
public EmojiDrawInfo getDrawInfo() {
|
||||
return drawInfo;
|
||||
}
|
||||
|
||||
public int getEndIndex() {
|
||||
return endIndex;
|
||||
}
|
||||
|
||||
public int getStartIndex() {
|
||||
return startIndex;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CandidateList implements Iterable<Candidate> {
|
||||
public final List<EmojiParser.Candidate> list;
|
||||
public final boolean allEmojis;
|
||||
|
||||
public CandidateList(List<EmojiParser.Candidate> candidates, boolean allEmojis) {
|
||||
this.list = candidates;
|
||||
this.allEmojis = allEmojis;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return list.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Iterator<Candidate> iterator() {
|
||||
return list.iterator();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2014-present Vincent DURMONT vdurmont@gmail.com
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and associated documentation files (the "Software"),
|
||||
* to deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.emoji.parsing;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Based in part on code from emoji-java
|
||||
*/
|
||||
public class EmojiTree {
|
||||
|
||||
private final EmojiTreeNode root = new EmojiTreeNode();
|
||||
|
||||
private static final char TERMINATOR = '\ufe0f';
|
||||
|
||||
public void add(String emojiEncoding, EmojiDrawInfo emoji) {
|
||||
EmojiTreeNode tree = root;
|
||||
|
||||
for (char c: emojiEncoding.toCharArray()) {
|
||||
if (!tree.hasChild(c)) {
|
||||
tree.addChild(c);
|
||||
}
|
||||
|
||||
tree = tree.getChild(c);
|
||||
}
|
||||
|
||||
tree.setEmoji(emoji);
|
||||
}
|
||||
|
||||
public Matches isEmoji(CharSequence sequence, int startPosition, int endPosition) {
|
||||
if (sequence == null) {
|
||||
return Matches.POSSIBLY;
|
||||
}
|
||||
|
||||
EmojiTreeNode tree = root;
|
||||
|
||||
for (int i=startPosition; i<endPosition; i++) {
|
||||
char character = sequence.charAt(i);
|
||||
|
||||
if (!tree.hasChild(character)) {
|
||||
return Matches.IMPOSSIBLE;
|
||||
}
|
||||
|
||||
tree = tree.getChild(character);
|
||||
}
|
||||
|
||||
if (tree.isEndOfEmoji()) {
|
||||
return Matches.EXACTLY;
|
||||
} else if (sequence.charAt(endPosition-1) != TERMINATOR && tree.hasChild(TERMINATOR) && tree.getChild(TERMINATOR).isEndOfEmoji()) {
|
||||
return Matches.EXACTLY;
|
||||
} else {
|
||||
return Matches.POSSIBLY;
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable EmojiDrawInfo getEmoji(CharSequence unicode, int startPosition, int endPostiion) {
|
||||
EmojiTreeNode tree = root;
|
||||
|
||||
for (int i=startPosition; i<endPostiion; i++) {
|
||||
char character = unicode.charAt(i);
|
||||
|
||||
if (!tree.hasChild(character)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
tree = tree.getChild(character);
|
||||
}
|
||||
|
||||
if (tree.getEmoji() != null) return tree.getEmoji();
|
||||
else if (unicode.charAt(endPostiion-1) != TERMINATOR && tree.hasChild(TERMINATOR)) return tree.getChild(TERMINATOR).getEmoji();
|
||||
else return null;
|
||||
}
|
||||
|
||||
|
||||
private static class EmojiTreeNode {
|
||||
|
||||
private final Map<Character, EmojiTreeNode> children = new HashMap<>();
|
||||
private EmojiDrawInfo emoji;
|
||||
|
||||
public void setEmoji(EmojiDrawInfo emoji) {
|
||||
this.emoji = emoji;
|
||||
}
|
||||
|
||||
public @Nullable EmojiDrawInfo getEmoji() {
|
||||
return emoji;
|
||||
}
|
||||
|
||||
boolean hasChild(char child) {
|
||||
return children.containsKey(child);
|
||||
}
|
||||
|
||||
void addChild(char child) {
|
||||
children.put(child, new EmojiTreeNode());
|
||||
}
|
||||
|
||||
EmojiTreeNode getChild(char child) {
|
||||
return children.get(child);
|
||||
}
|
||||
|
||||
boolean isEndOfEmoji() {
|
||||
return emoji != null;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Matches {
|
||||
EXACTLY, POSSIBLY, IMPOSSIBLE;
|
||||
|
||||
public boolean exactMatch() {
|
||||
return this == EXACTLY;
|
||||
}
|
||||
|
||||
public boolean impossibleMatch() {
|
||||
return this == IMPOSSIBLE;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji.parsing;
|
||||
|
||||
|
||||
public enum Fitzpatrick {
|
||||
/**
|
||||
* Fitzpatrick modifier of type 1/2 (pale white/white)
|
||||
*/
|
||||
TYPE_1_2("\uD83C\uDFFB"),
|
||||
|
||||
/**
|
||||
* Fitzpatrick modifier of type 3 (cream white)
|
||||
*/
|
||||
TYPE_3("\uD83C\uDFFC"),
|
||||
|
||||
/**
|
||||
* Fitzpatrick modifier of type 4 (moderate brown)
|
||||
*/
|
||||
TYPE_4("\uD83C\uDFFD"),
|
||||
|
||||
/**
|
||||
* Fitzpatrick modifier of type 5 (dark brown)
|
||||
*/
|
||||
TYPE_5("\uD83C\uDFFE"),
|
||||
|
||||
/**
|
||||
* Fitzpatrick modifier of type 6 (black)
|
||||
*/
|
||||
TYPE_6("\uD83C\uDFFF");
|
||||
|
||||
/**
|
||||
* The unicode representation of the Fitzpatrick modifier
|
||||
*/
|
||||
public final String unicode;
|
||||
|
||||
Fitzpatrick(String unicode) {
|
||||
this.unicode = unicode;
|
||||
}
|
||||
|
||||
|
||||
public static Fitzpatrick fitzpatrickFromUnicode(CharSequence unicode, int index) {
|
||||
for (Fitzpatrick v : values()) {
|
||||
boolean match = true;
|
||||
|
||||
for (int i=0;i<v.unicode.toCharArray().length;i++) {
|
||||
if (v.unicode.toCharArray()[i] != unicode.charAt(index + i)) {
|
||||
match = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (match) return v;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Fitzpatrick fitzpatrickFromType(String type) {
|
||||
try {
|
||||
return Fitzpatrick.valueOf(type.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +171,15 @@ public class DcEventCenter {
|
||||
|
||||
switch (id) {
|
||||
case DcContext.DC_EVENT_INCOMING_MSG:
|
||||
DcHelper.getNotificationCenter(context).addNotification(accountId, event.getData1Int(), event.getData2Int());
|
||||
DcHelper.getNotificationCenter(context).notifyMessage(accountId, event.getData1Int(), event.getData2Int());
|
||||
break;
|
||||
|
||||
case DcContext.DC_EVENT_INCOMING_REACTION:
|
||||
DcHelper.getNotificationCenter(context).notifyReaction(accountId, event.getData1Int(), event.getData2Int(), event.getData2Str());
|
||||
break;
|
||||
|
||||
case DcContext.DC_EVENT_INCOMING_WEBXDC_NOTIFY:
|
||||
DcHelper.getNotificationCenter(context).notifyWebxdc(accountId, event.getData1Int(), event.getData2Int(), event.getData2Str());
|
||||
break;
|
||||
|
||||
case DcContext.DC_EVENT_MSGS_NOTICED:
|
||||
@@ -187,19 +195,26 @@ public class DcEventCenter {
|
||||
return 0;
|
||||
}
|
||||
|
||||
final String logPrefix = "[accId="+accountId + "] ";
|
||||
switch (id) {
|
||||
case DcContext.DC_EVENT_INFO:
|
||||
Log.i("DeltaChat", logPrefix + event.getData2Str());
|
||||
break;
|
||||
|
||||
case DcContext.DC_EVENT_WARNING:
|
||||
Log.w("DeltaChat", logPrefix + event.getData2Str());
|
||||
break;
|
||||
|
||||
case DcContext.DC_EVENT_ERROR:
|
||||
Log.e("DeltaChat", logPrefix + event.getData2Str());
|
||||
break;
|
||||
}
|
||||
|
||||
if (accountId != context.dcContext.getAccountId()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
switch (id) {
|
||||
case DcContext.DC_EVENT_INFO:
|
||||
Log.i("DeltaChat", event.getData2Str());
|
||||
break;
|
||||
|
||||
case DcContext.DC_EVENT_WARNING:
|
||||
Log.w("DeltaChat", event.getData2Str());
|
||||
break;
|
||||
|
||||
case DcContext.DC_EVENT_ERROR:
|
||||
handleError(id, event.getData2Str());
|
||||
break;
|
||||
|
||||
@@ -263,9 +263,7 @@ public class DcHelper {
|
||||
}
|
||||
|
||||
public static File getImexDir() {
|
||||
// DIRECTORY_DOCUMENTS is only available since KitKat;
|
||||
// as we also support Ice Cream Sandwich and Jellybean (2017: 11% in total), this is no option.
|
||||
// Moreover, DIRECTORY_DOWNLOADS seems to be easier accessible by the user,
|
||||
// DIRECTORY_DOCUMENTS could be used but DIRECTORY_DOWNLOADS seems to be easier accessible by the user,
|
||||
// eg. "Download Managers" are nearly always installed.
|
||||
// CAVE: do not use DownloadManager to add the file as it is deleted on uninstall then ...
|
||||
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
||||
|
||||
@@ -1,345 +0,0 @@
|
||||
/*
|
||||
* This software is provided 'as-is', without any express or implied
|
||||
* warranty. In no event will Google be held liable for any damages
|
||||
* arising from the use of this software.
|
||||
*
|
||||
* Permission is granted to anyone to use this software for any purpose,
|
||||
* including commercial applications, and to alter it and redistribute it
|
||||
* freely, as long as the origin is not misrepresented.
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Process;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.Provider;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.SecureRandomSpi;
|
||||
import java.security.Security;
|
||||
|
||||
/**
|
||||
* This class is taken directly from the Android blog post announcing this bug:
|
||||
* http://android-developers.blogspot.com/2013/08/some-securerandom-thoughts.html
|
||||
*
|
||||
* Since I still don't know exactly what the source of this bug was, I'm using
|
||||
* this class verbatim under the assumption that the Android team knows what
|
||||
* they're doing. Although, at this point, that is perhaps a foolish assumption.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fixes for the output of the default PRNG having low entropy.
|
||||
*
|
||||
* The fixes need to be applied via {@link #apply()} before any use of Java
|
||||
* Cryptography Architecture primitives. A good place to invoke them is in the
|
||||
* application's {@code onCreate}.
|
||||
*/
|
||||
public final class PRNGFixes {
|
||||
|
||||
private static final int VERSION_CODE_JELLY_BEAN = 16;
|
||||
private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18;
|
||||
private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL =
|
||||
getBuildFingerprintAndDeviceSerial();
|
||||
|
||||
/** Hidden constructor to prevent instantiation. */
|
||||
private PRNGFixes() {}
|
||||
|
||||
/**
|
||||
* Applies all fixes.
|
||||
*
|
||||
* @throws SecurityException if a fix is needed but could not be applied.
|
||||
*/
|
||||
public static void apply() {
|
||||
applyOpenSSLFix();
|
||||
installLinuxPRNGSecureRandom();
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the
|
||||
* fix is not needed.
|
||||
*
|
||||
* @throws SecurityException if the fix is needed but could not be applied.
|
||||
*/
|
||||
private static void applyOpenSSLFix() throws SecurityException {
|
||||
if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN)
|
||||
|| (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) {
|
||||
// No need to apply the fix
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Mix in the device- and invocation-specific seed.
|
||||
Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto")
|
||||
.getMethod("RAND_seed", byte[].class)
|
||||
.invoke(null, generateSeed());
|
||||
|
||||
// Mix output of Linux PRNG into OpenSSL's PRNG
|
||||
int bytesRead = (Integer) Class.forName(
|
||||
"org.apache.harmony.xnet.provider.jsse.NativeCrypto")
|
||||
.getMethod("RAND_load_file", String.class, long.class)
|
||||
.invoke(null, "/dev/urandom", 1024);
|
||||
if (bytesRead != 1024) {
|
||||
throw new IOException(
|
||||
"Unexpected number of bytes read from Linux PRNG: "
|
||||
+ bytesRead);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new SecurityException("Failed to seed OpenSSL PRNG", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs a Linux PRNG-backed {@code SecureRandom} implementation as the
|
||||
* default. Does nothing if the implementation is already the default or if
|
||||
* there is not need to install the implementation.
|
||||
*
|
||||
* @throws SecurityException if the fix is needed but could not be applied.
|
||||
*/
|
||||
private static void installLinuxPRNGSecureRandom()
|
||||
throws SecurityException {
|
||||
if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) {
|
||||
// No need to apply the fix
|
||||
return;
|
||||
}
|
||||
|
||||
// Install a Linux PRNG-based SecureRandom implementation as the
|
||||
// default, if not yet installed.
|
||||
Provider[] secureRandomProviders =
|
||||
Security.getProviders("SecureRandom.SHA1PRNG");
|
||||
if ((secureRandomProviders == null)
|
||||
|| (secureRandomProviders.length < 1)
|
||||
|| (!LinuxPRNGSecureRandomProvider.class.equals(
|
||||
secureRandomProviders[0].getClass()))) {
|
||||
Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1);
|
||||
}
|
||||
|
||||
// Assert that new SecureRandom() and
|
||||
// SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed
|
||||
// by the Linux PRNG-based SecureRandom implementation.
|
||||
SecureRandom rng1 = new SecureRandom();
|
||||
if (!LinuxPRNGSecureRandomProvider.class.equals(
|
||||
rng1.getProvider().getClass())) {
|
||||
throw new SecurityException(
|
||||
"new SecureRandom() backed by wrong Provider: "
|
||||
+ rng1.getProvider().getClass());
|
||||
}
|
||||
|
||||
SecureRandom rng2;
|
||||
try {
|
||||
rng2 = SecureRandom.getInstance("SHA1PRNG");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new SecurityException("SHA1PRNG not available", e);
|
||||
}
|
||||
if (!LinuxPRNGSecureRandomProvider.class.equals(
|
||||
rng2.getProvider().getClass())) {
|
||||
throw new SecurityException(
|
||||
"SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong"
|
||||
+ " Provider: " + rng2.getProvider().getClass());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code Provider} of {@code SecureRandom} engines which pass through
|
||||
* all requests to the Linux PRNG.
|
||||
*/
|
||||
private static class LinuxPRNGSecureRandomProvider extends Provider {
|
||||
|
||||
public LinuxPRNGSecureRandomProvider() {
|
||||
super("LinuxPRNG",
|
||||
1.0,
|
||||
"A Linux-specific random number provider that uses"
|
||||
+ " /dev/urandom");
|
||||
// Although /dev/urandom is not a SHA-1 PRNG, some apps
|
||||
// explicitly request a SHA1PRNG SecureRandom and we thus need to
|
||||
// prevent them from getting the default implementation whose output
|
||||
// may have low entropy.
|
||||
put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName());
|
||||
put("SecureRandom.SHA1PRNG ImplementedIn", "Software");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link SecureRandomSpi} which passes all requests to the Linux PRNG
|
||||
* ({@code /dev/urandom}).
|
||||
*/
|
||||
public static class LinuxPRNGSecureRandom extends SecureRandomSpi {
|
||||
|
||||
/*
|
||||
* IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed
|
||||
* are passed through to the Linux PRNG (/dev/urandom). Instances of
|
||||
* this class seed themselves by mixing in the current time, PID, UID,
|
||||
* build fingerprint, and hardware serial number (where available) into
|
||||
* Linux PRNG.
|
||||
*
|
||||
* Concurrency: Read requests to the underlying Linux PRNG are
|
||||
* serialized (on sLock) to ensure that multiple threads do not get
|
||||
* duplicated PRNG output.
|
||||
*/
|
||||
|
||||
private static final File URANDOM_FILE = new File("/dev/urandom");
|
||||
|
||||
private static final Object sLock = new Object();
|
||||
|
||||
/**
|
||||
* Input stream for reading from Linux PRNG or {@code null} if not yet
|
||||
* opened.
|
||||
*
|
||||
* @GuardedBy("sLock")
|
||||
*/
|
||||
private static DataInputStream sUrandomIn;
|
||||
|
||||
/**
|
||||
* Output stream for writing to Linux PRNG or {@code null} if not yet
|
||||
* opened.
|
||||
*
|
||||
* @GuardedBy("sLock")
|
||||
*/
|
||||
private static OutputStream sUrandomOut;
|
||||
|
||||
/**
|
||||
* Whether this engine instance has been seeded. This is needed because
|
||||
* each instance needs to seed itself if the client does not explicitly
|
||||
* seed it.
|
||||
*/
|
||||
private boolean mSeeded;
|
||||
|
||||
@Override
|
||||
protected void engineSetSeed(byte[] bytes) {
|
||||
try {
|
||||
OutputStream out;
|
||||
synchronized (sLock) {
|
||||
out = getUrandomOutputStream();
|
||||
}
|
||||
out.write(bytes);
|
||||
out.flush();
|
||||
} catch (IOException e) {
|
||||
// On a small fraction of devices /dev/urandom is not writable.
|
||||
// Log and ignore.
|
||||
Log.w(PRNGFixes.class.getSimpleName(),
|
||||
"Failed to mix seed into " + URANDOM_FILE);
|
||||
} finally {
|
||||
mSeeded = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void engineNextBytes(byte[] bytes) {
|
||||
if (!mSeeded) {
|
||||
// Mix in the device- and invocation-specific seed.
|
||||
engineSetSeed(generateSeed());
|
||||
}
|
||||
|
||||
try {
|
||||
DataInputStream in;
|
||||
synchronized (sLock) {
|
||||
in = getUrandomInputStream();
|
||||
}
|
||||
synchronized (in) {
|
||||
in.readFully(bytes);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new SecurityException(
|
||||
"Failed to read from " + URANDOM_FILE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] engineGenerateSeed(int size) {
|
||||
byte[] seed = new byte[size];
|
||||
engineNextBytes(seed);
|
||||
return seed;
|
||||
}
|
||||
|
||||
private DataInputStream getUrandomInputStream() {
|
||||
synchronized (sLock) {
|
||||
if (sUrandomIn == null) {
|
||||
// NOTE: Consider inserting a BufferedInputStream between
|
||||
// DataInputStream and FileInputStream if you need higher
|
||||
// PRNG output performance and can live with future PRNG
|
||||
// output being pulled into this process prematurely.
|
||||
try {
|
||||
sUrandomIn = new DataInputStream(
|
||||
new FileInputStream(URANDOM_FILE));
|
||||
} catch (IOException e) {
|
||||
throw new SecurityException("Failed to open "
|
||||
+ URANDOM_FILE + " for reading", e);
|
||||
}
|
||||
}
|
||||
return sUrandomIn;
|
||||
}
|
||||
}
|
||||
|
||||
private OutputStream getUrandomOutputStream() throws IOException {
|
||||
synchronized (sLock) {
|
||||
if (sUrandomOut == null) {
|
||||
sUrandomOut = new FileOutputStream(URANDOM_FILE);
|
||||
}
|
||||
return sUrandomOut;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a device- and invocation-specific seed to be mixed into the
|
||||
* Linux PRNG.
|
||||
*/
|
||||
private static byte[] generateSeed() {
|
||||
try {
|
||||
ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream();
|
||||
DataOutputStream seedBufferOut =
|
||||
new DataOutputStream(seedBuffer);
|
||||
seedBufferOut.writeLong(System.currentTimeMillis());
|
||||
seedBufferOut.writeLong(System.nanoTime());
|
||||
seedBufferOut.writeInt(Process.myPid());
|
||||
seedBufferOut.writeInt(Process.myUid());
|
||||
seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL);
|
||||
seedBufferOut.close();
|
||||
return seedBuffer.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new SecurityException("Failed to generate seed", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the hardware serial number of this device.
|
||||
*
|
||||
* @return serial number or {@code null} if not available.
|
||||
*/
|
||||
private static String getDeviceSerialNumber() {
|
||||
// We're using the Reflection API because Build.SERIAL is only available
|
||||
// since API Level 9 (Gingerbread, Android 2.3).
|
||||
try {
|
||||
return (String) Build.class.getField("SERIAL").get(null);
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] getBuildFingerprintAndDeviceSerial() {
|
||||
StringBuilder result = new StringBuilder();
|
||||
String fingerprint = Build.FINGERPRINT;
|
||||
if (fingerprint != null) {
|
||||
result.append(fingerprint);
|
||||
}
|
||||
String serial = getDeviceSerialNumber();
|
||||
if (serial != null) {
|
||||
result.append(serial);
|
||||
}
|
||||
try {
|
||||
return result.toString().getBytes("UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException("UTF-8 encoding not supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user