mirror of
https://github.com/oxen-io/session-android.git
synced 2025-04-05 11:15:43 +00:00
Strings work
Squashed commit of the following: commit 86cab0e11e4871ec2258c2099d8634a91a2f9bea Author: ThomasSession <thomas.r@getsession.org> Date: Fri Aug 30 10:17:04 2024 +1000 Bringing my xml dialog styling from my 'Standardise message deletion' branch commit 706d1aadd833f6fa60de8ac308c62919adf45dc4 Author: ThomasSession <thomas.r@getsession.org> Date: Fri Aug 30 09:49:48 2024 +1000 fixing up clear data dialog Removing unused code commit f90599451f9660e4a64964481eacdf65070dc092 Author: Al Lansley <al@oxen.io> Date: Fri Aug 30 09:13:51 2024 +1000 Replaced 'now' with 12/24 hour time commit 16b8ad46c09515de949f0f47a0ef16f799a7e878 Author: alansley <aclansley@gmail.com> Date: Thu Aug 29 17:34:03 2024 +1000 Fix two one-liner issues commit 4c6c450b3218a0c3663ede1773b6dc32989024fc Merge: 052f910d69 beb89d5b74 Author: ThomasSession <thomas.r@getsession.org> Date: Thu Aug 29 17:07:16 2024 +1000 Merge branch 'strings-squashed' of https://github.com/oxen-io/session-android into strings-squashed commit 052f910d69c453f847e5dbad9132a40f3e00126b Author: ThomasSession <thomas.r@getsession.org> Date: Thu Aug 29 17:06:53 2024 +1000 More bold fixing commit beb89d5b74b8a64ffcf9c7ce3d7507a9b83dac9e Author: fanchao <git@fanchao.dev> Date: Thu Aug 29 17:00:37 2024 +1000 Fix incorrect group member left message commit 5773f05a5c461fba8c91bb804be17f0245e6ee79 Merge: d35482daba 1cec477020 Author: ThomasSession <thomas.r@getsession.org> Date: Thu Aug 29 15:21:44 2024 +1000 Merge branch 'strings-squashed' of https://github.com/oxen-io/session-android into strings-squashed commit d35482dabaac8ae2da97fb920903a984cec525ca Author: ThomasSession <thomas.r@getsession.org> Date: Thu Aug 29 15:20:13 2024 +1000 More bold fixes and UI tweaks commit 78a9ab7159218005f5bca91b35583e4daa608e2d Author: ThomasSession <thomas.r@getsession.org> Date: Thu Aug 29 14:03:41 2024 +1000 Making sure we bold appropriately commit 1cec4770203a61547356009e42bf80e65fe17410 Author: alansley <aclansley@gmail.com> Date: Thu Aug 29 13:33:50 2024 +1000 Made call to 'getQuantityString' pass the count twice because otherwise it doesn't work correctly commit 8e80ab08a926c772f620089aeb8c7710a203af2d Author: ThomasSession <thomas.r@getsession.org> Date: Thu Aug 29 13:28:54 2024 +1000 Using the existing implementation commit cb9554ab386af1d01177940cac10283be2944ce2 Author: alansley <aclansley@gmail.com> Date: Thu Aug 29 12:32:30 2024 +1000 Merge CrowdIn strings circa 2024-08-29 commit dd57da70f64eb622482eea4f3c4a78e233d96d28 Author: alansley <aclansley@gmail.com> Date: Thu Aug 29 09:06:22 2024 +1000 Updated Phrase usage in ConversationAdapter commit 34b15d78656a9c82d7952da9d003df686e231f81 Author: alansley <aclansley@gmail.com> Date: Thu Aug 29 09:03:55 2024 +1000 Converted TransferControlView into Kotlin and updated Phrase usage commit a35a7a6a96cf68f7b44749f1f3482adac5b1d17e Author: alansley <aclansley@gmail.com> Date: Thu Aug 29 08:55:16 2024 +1000 Converted MessageReceipientNotificationBuilder to Kotlin & updated Phrase usage commit 6dd93b33f222c0818073ff3fff02c312b3b9d2e9 Author: alansley <aclansley@gmail.com> Date: Thu Aug 29 08:25:24 2024 +1000 Update MuteDialog, LinkPreviewDialog, and PathActivity commit e7dd1c582d1ceb4bdf132ca068264badc70bccb4 Author: alansley <aclansley@gmail.com> Date: Thu Aug 29 08:16:09 2024 +1000 Updated DisappearingMessages.kt and HelpSettingsActivity.kt commit 5bd55ea99320941b8f9b40f0680d6980f8e09dc4 Author: alansley <aclansley@gmail.com> Date: Thu Aug 29 08:01:30 2024 +1000 Converted SwitchPreferenceCompat to Kotlin and fixed the BlockedDialog using the joinCommunity string for some bizarre reason commit d3fb440d05b90b6eb30d28dc9cf0524be3275160 Author: alansley <aclansley@gmail.com> Date: Thu Aug 29 07:15:03 2024 +1000 Removed R.string.gif and replaced with a string constant commit ace58e3493ec3a5991274ec8d2554ff1eea6cf8e Author: alansley <aclansley@gmail.com> Date: Thu Aug 29 07:11:53 2024 +1000 getSubbedString correction commit 2a8f010369424ff5a7138c9294283478e31c424e Merge: ce8efd7def 116bef3c71 Author: alansley <aclansley@gmail.com> Date: Wed Aug 28 16:31:43 2024 +1000 Merge branch 'compose-open-url-dialog' into strings-squashed commit ce8efd7def0a25515a06fea3b1dabf90cc4909c2 Author: alansley <aclansley@gmail.com> Date: Wed Aug 28 16:31:11 2024 +1000 WIP commit 114066ad5f841dfc0e8e68adc29f61abfc804f21 Author: alansley <aclansley@gmail.com> Date: Wed Aug 28 15:30:02 2024 +1000 Push before changing over all the Phrase.from to extension method calls commit 116bef3c7110a38b9f8198dbdb85e8bc7eafffed Author: ThomasSession <thomas.r@getsession.org> Date: Wed Aug 28 15:25:03 2024 +1000 For safety commit 0b1a71a5820901a010b633525f56988e8b5095cd Author: ThomasSession <thomas.r@getsession.org> Date: Wed Aug 28 15:23:02 2024 +1000 Cleaning other use of old url dialog commit 20abbebf4ac8bc3a8fd46463f3be621b991c15d1 Author: ThomasSession <thomas.r@getsession.org> Date: Wed Aug 28 15:19:46 2024 +1000 Forgot !! commit 25132c6342f11613083b9cd3f7413b775faefc00 Author: ThomasSession <thomas.r@getsession.org> Date: Wed Aug 28 15:13:58 2024 +1000 Proper set up for the Open URL dialog commit 1f68791da92287e52c7b1e95256ccb771f77a31b Author: alansley <aclansley@gmail.com> Date: Wed Aug 28 14:35:05 2024 +1000 Replaced placeholder text with new string commit 8d97f31b4d5bf79d7f69887ca871210e431c40e5 Author: alansley <aclansley@gmail.com> Date: Wed Aug 28 14:31:52 2024 +1000 Adjusted comment commit dfebe6f3f97c6ea96d2b143291ae5991d7242104 Author: alansley <aclansley@gmail.com> Date: Wed Aug 28 14:25:23 2024 +1000 Moved block/unblock string selection logic into ViewModel and fixed a comment commit 736b5313e634c17e1446c0f42f1962ba1fdb0664 Author: alansley <aclansley@gmail.com> Date: Wed Aug 28 14:02:54 2024 +1000 Changed toast to warning - although condition to trigger should not be possible commit 413bc0be4b1464efcbe9cda92e47a139a87f6610 Author: alansley <aclansley@gmail.com> Date: Wed Aug 28 13:55:04 2024 +1000 Adjusted EditGroupMembers to match iOS and fixed up save attachment commentary / logic commit ae7164ecbb78d2045cb4df9fafdf5ad07eba5365 Merge: 5df981bc7a d1c4283f42 Author: alansley <aclansley@gmail.com> Date: Wed Aug 28 09:51:58 2024 +1000 Merge branch 'dev' into strings-squashed commit 2aa58f4dd6c62ec712715a24cf86272c0990a7af Author: alansley <aclansley@gmail.com> Date: Wed Aug 28 08:27:03 2024 +1000 WIP compose openURL dialog commit 5df981bc7ab4736e1a96ef4f585f063189f95740 Author: alansley <aclansley@gmail.com> Date: Tue Aug 27 15:51:38 2024 +1000 Adjusted NotificationRadioButton that takes string IDs to act as a pass-through commit 96453f1f1ee9af9b8ddf20c83d52070d65b3d184 Author: alansley <aclansley@gmail.com> Date: Tue Aug 27 15:42:33 2024 +1000 Added some TODO markers for tomorrow commit a402a1be79a5e6ddebb65cc5bca2e41310f4f94e Author: alansley <aclansley@gmail.com> Date: Tue Aug 27 15:33:55 2024 +1000 Adjusted Landing page string substitutions to cater for emojis commit 4809b5444b7b3488e58e6b6e96e5d17f77d545b0 Author: alansley <aclansley@gmail.com> Date: Tue Aug 27 15:12:39 2024 +1000 Removed unused 'isEmpty' utility methods commit b52048a080ac5c9cf6217d5f519c79aa48c873b6 Author: alansley <aclansley@gmail.com> Date: Tue Aug 27 14:42:57 2024 +1000 Addressed many aspects of PR feedback + misc. strings issues commit 9cdbc4b80b80368d42635144bc37b3ee2689f06b Author: alansley <aclansley@gmail.com> Date: Tue Aug 27 09:50:51 2024 +1000 Adjusted strings as per Rebecca's 'String Changes' spreadsheet commit 4d7e4b9e2c6a91e3c362ce5694b31a30ef4f00f8 Merge: 3c576053a3 1393335121 Author: alansley <aclansley@gmail.com> Date: Tue Aug 27 08:19:53 2024 +1000 Merge branch 'dev' into strings-squashed commit 3c576053a3e5717b7beeef2286837be4951e355f Author: alansley <aclansley@gmail.com> Date: Mon Aug 26 17:11:45 2024 +1000 Moved into libsession for ease of access to control message view creation commit b908a54a44aa7713a8a51e34d2432b54d6590758 Merge: 404fb8001c bfbe4a8fd2 Author: alansley <aclansley@gmail.com> Date: Mon Aug 26 11:54:09 2024 +1000 Merge branch 'dev' into strings-squashed commit 404fb8001cfe84b44bd76decb43dd0fa93040c25 Author: alansley <aclansley@gmail.com> Date: Mon Aug 26 11:52:41 2024 +1000 Performed a PR pass to fix up anything obvious - there's still a few things left TODO commit 53978f818dedf9d8b3aea063b7803a3152f9cae7 Author: Al Lansley <al@oxen.io> Date: Fri Aug 23 14:13:11 2024 +1000 Cleaned up HomeActivityTests.kt commit 5f82571befba7ec830c60064fefe553aac307cd6 Merge: 69b8bd7396 8deb21c0c6 Author: Al Lansley <al@oxen.io> Date: Fri Aug 23 08:59:21 2024 +1000 Merge branch 'dev' into strings-squashed commit 69b8bd739690f51540490d943b06f92ccb0a323a Author: alansley <aclansley@gmail.com> Date: Thu Aug 22 16:20:17 2024 +1000 Added back app_name string so app names properly, fixed API 28 save issue, made some buttons display as red if they should commit e3cab9c0d9aad3c98ead66d8df70b68a0afef56a Author: alansley <aclansley@gmail.com> Date: Thu Aug 22 14:26:48 2024 +1000 SS-75 Prevented ScrollView vertical scroll bar from fading out commit b0b835092dffab3a112f61f203dd9138a9a1c9b1 Author: alansley <aclansley@gmail.com> Date: Thu Aug 22 14:07:49 2024 +1000 SS-64 Removed all 'Unblocked {name}' toasts as per instructions commit c3c35de4089ddb16203b69e6391f4a936092c701 Merge: efc2ee2824 8e10e1abf4 Author: alansley <aclansley@gmail.com> Date: Thu Aug 22 13:43:00 2024 +1000 Merge branch 'dev' into strings-squashed commit efc2ee2824494169e383978819501d2edaa061d4 Author: alansley <aclansley@gmail.com> Date: Thu Aug 22 13:40:59 2024 +1000 Added some comments about the new CrowdIn strings commit 7a03fb37ef34726d268e467d51bfc83905609483 Author: alansley <aclansley@gmail.com> Date: Thu Aug 22 13:08:03 2024 +1000 Initial integration of CrowdIn strings (English only) commit 9766c3fd0b9200323584f15fbc004d9bc1b0987f Author: alansley <aclansley@gmail.com> Date: Thu Aug 22 09:55:14 2024 +1000 SS-75 Added 'Copied' toast when the user copies a URL in the Open URL dialog commit 59b4805b8b5420adc64e23c49e381598226022cb Author: alansley <aclansley@gmail.com> Date: Thu Aug 22 09:51:01 2024 +1000 SS-75 Prevent 'Are you sure you want to open this URL?' dialog from being excessively tall when given a very long URL commit b7f627f03c5c41fbcb215a78e54a0450b10295b6 Author: alansley <aclansley@gmail.com> Date: Wed Aug 21 14:54:17 2024 +1000 Made closed group deleting-someone-elses msgs use 'Delete message' or 'Delete Messages' appropriately commit 69f6818f99608f4cb2fae8c7e7a132c66f049a33 Author: alansley <aclansley@gmail.com> Date: Wed Aug 21 13:53:58 2024 +1000 Adjusted SS-64 so that all Block / Unblock buttons now use that text and are displayed in red commit 2192c2c00757cc07306fdd22f000e8061ddc899a Merge: 2338bb47ca eea54d1a17 Author: alansley <aclansley@gmail.com> Date: Wed Aug 21 13:28:16 2024 +1000 Merge branch 'dev' into strings-squashed commit 2338bb47ca1dea1deb232c86978157a9f01fe44c Author: alansley <aclansley@gmail.com> Date: Tue Aug 20 19:11:40 2024 +1000 Converted DefaultMessageNotifier to Kotlin because it needs adjustment & that Java is nasty commit 6b29e4d8ceae7bd24c56a724e67bcd58f90c5b3b Author: alansley <aclansley@gmail.com> Date: Tue Aug 20 17:53:27 2024 +1000 Added a note about the plurals for search results commit f7748a0c05eb1272a7b281d6910691df2130c3b0 Author: alansley <aclansley@gmail.com> Date: Tue Aug 20 16:06:24 2024 +1000 Corrected text on storage permission dialog commit f6b62565989fa78b493c48a8a8efe9b9284c29d9 Author: alansley <aclansley@gmail.com> Date: Tue Aug 20 14:44:25 2024 +1000 Minor cleanup of BlockedContactsActivity commit e3d4870d81bd54f2f2373cc5d969ad2c406ddf89 Author: alansley <aclansley@gmail.com> Date: Tue Aug 20 14:41:14 2024 +1000 Addressed changes to fix SS-64 / QA-146 - unblocking contacts modal & toast adjustments commit e81252735856fb7e4b0ddf36fd107b2f82f2f194 Merge: 5e02e1ef5c 9919f716a7 Author: alansley <aclansley@gmail.com> Date: Tue Aug 20 13:27:35 2024 +1000 Merge branch 'dev' into strings-squashed commit 5e02e1ef5c04056761409c97ba90efc5b447bb6c Author: alansley <aclansley@gmail.com> Date: Tue Aug 20 09:39:16 2024 +1000 Added 'NonTranslatableStringConstants' file commit 816f21bb29e00633285cf084e314f4375eec31dc Author: alansley <aclansley@gmail.com> Date: Tue Aug 20 09:30:30 2024 +1000 Addressed commit feedback & removed desktop string 'attachmentsClickToDownload' as we use 'attachmentsTapToDownload' commit acc8d47c6875893ef9e988440c55f8239fda47d1 Author: Al Lansley <al@oxen.io> Date: Mon Aug 19 16:22:08 2024 +1000 SES-1571 Large messages show warning toast commit 27ca77d5c48b097d8e6b397414a09383a4645fc2 Merge: 27bc90bf1f f379604c54 Author: Al Lansley <al@oxen.io> Date: Mon Aug 19 11:19:27 2024 +1000 Merge branch 'dev' into strings-squashed commit 27bc90bf1f21ad3cba8c11e6318c51a083736f01 Author: Al Lansley <al@oxen.io> Date: Mon Aug 19 08:59:38 2024 +1000 Cleaned up some comments and content description commit 558684a56d9e609030242d411424def9f21b510a Merge: 90d7064c18 93a28906fb Author: Al Lansley <al@oxen.io> Date: Mon Aug 19 08:41:47 2024 +1000 Merge branch 'dev' into strings-squashed commit 90d7064c18d0d95cbeb1f3fd04831fb8d36e2d0c Author: Al Lansley <al@oxen.io> Date: Thu Aug 15 12:13:30 2024 +1000 Fixed issue where new closed groups would display a timestamp instead of the 'groupNoMessages' text commit 51ef0ec81c8810c42379c863d970754ebc0814b8 Author: Al Lansley <al@oxen.io> Date: Thu Aug 15 09:45:28 2024 +1000 Replaced string 'CreateProfileActivity_profile_photo' with the string 'photo' which has the same text ('Photo') commit eecce08c25e560f2d62064f064afabb474c50a16 Merge: 01009cf521 5a248da445 Author: Al Lansley <al@oxen.io> Date: Thu Aug 15 09:38:10 2024 +1000 Merge branch 'dev' into strings-squashed commit 01009cf521e4fe8cb94f25beed48cd6e550a5b4a Author: Al Lansley <al@oxen.io> Date: Thu Aug 15 08:37:19 2024 +1000 Changed allowed emoji reactions per minute from 5 (which I used for testing) to 20 (production) commit 9441d1e08daa11d2dce4168e3a2816acb1180dcb Author: Al Lansley <al@oxen.io> Date: Thu Aug 15 08:34:16 2024 +1000 Refactored emoji rate limiter to use a timestamp mechanism rather than removing queue items after a delay commit 6cd6cc3e26b8f30213bb0570434a49a51c08bd6c Author: alansley <aclansley@gmail.com> Date: Wed Aug 14 16:48:07 2024 +1000 Adjusted emoji rate limit to 20 reactions per minute to match acceptance criteria commit edd154d8e1979fc572250601c3f044ba00a3efe0 Author: alansley <aclansley@gmail.com> Date: Wed Aug 14 16:02:16 2024 +1000 SS-78 / SES-199 Mechanism required to limit emoji reaction rate commit a8ee5c9f3b0b121ca597fee5fc11cc5acb768ba0 Author: alansley <aclansley@gmail.com> Date: Wed Aug 14 14:51:40 2024 +1000 Replaced hard-coded 'Session' with '{app_name}' in 'callsSessionCall' commit 621094ebe4cb8c51ca4595b041eb94e5d4d469aa Author: alansley <aclansley@gmail.com> Date: Wed Aug 14 13:40:01 2024 +1000 SS-72 Update save attachment models + add one-time warning that other apps can access saved attachments commit 0c8360653928f94e3a391ed851a63b309fda7e3d Author: alansley <aclansley@gmail.com> Date: Tue Aug 13 15:50:35 2024 +1000 SS-75 Open URL modal change commit 802cf19598e83709303199a1d361416a557daac2 Author: Al Lansley <al@oxen.io> Date: Mon Aug 12 16:42:15 2024 +1000 Open or copy URL WIP commit ea84aa1478081095df6a0e6c120bc7e29100dd4a Author: Al Lansley <al@oxen.io> Date: Mon Aug 12 14:17:04 2024 +1000 Tied in bandDeleteAll string commit 93b8e74f2d1489ea7c9127cea940300f020b9a11 Author: Al Lansley <al@oxen.io> Date: Mon Aug 12 11:34:03 2024 +1000 Job done! All Accessibility ID strings mapped and/or dealt with appropriately! commit fc3b4ad36723ec10cd136569417e69f68a2e0800 Author: Al Lansley <al@oxen.io> Date: Mon Aug 12 09:49:57 2024 +1000 Further AccessibilityId mapping & fixed group members counts to display correct details commit 558d6741b159a21c4f1a12262a08101a391daab2 Author: alansley <aclansley@gmail.com> Date: Fri Aug 9 17:24:44 2024 +1000 End of day push commit 73fdb16214c8f6c76b3e06c9e804e50a8961c032 Author: alansley <aclansley@gmail.com> Date: Fri Aug 9 15:57:06 2024 +1000 Localised time strings working - even if the unit tests aren't commit 436175d146db7add793fc8ebce31503ce1d6c844 Author: alansley <aclansley@gmail.com> Date: Fri Aug 9 13:54:09 2024 +1000 Relative time string WIP commit f309263e39fe9d68b4665d3ebf60a5debc5bd81d Merge: 45c4118d52 007e705cd9 Author: alansley <aclansley@gmail.com> Date: Fri Aug 9 11:39:13 2024 +1000 Merge dev commit 45c4118d526a54b2aa7b332adef04c49b7a77205 Author: Al Lansley <al@oxen.io> Date: Thu Aug 8 16:43:02 2024 +1000 Further AccessibilityId mapping WIP commit 31bac8e30e0cf37b917fb847d913cd40a0109d0e Author: Al Lansley <al@oxen.io> Date: Thu Aug 8 10:53:30 2024 +1000 Further accessibility ID changes & removed fragment_new_conversation_home.xml commit 9c2111e66e2ac09e5e210876665886bc9acb7d27 Author: alansley <aclansley@gmail.com> Date: Wed Aug 7 13:13:52 2024 +1000 AccessibilityId WIP commit 1e9eeff86adff3af64b515de4682a147f3078a55 Author: alansley <aclansley@gmail.com> Date: Wed Aug 7 11:06:39 2024 +1000 AccessibilityId adjustments & removed some unused XML layouts commit e5fd2c8cc03535bfd02fe7cd28eba51530ec7985 Author: alansley <aclansley@gmail.com> Date: Wed Aug 7 09:22:14 2024 +1000 AccessibilityId refactor WIP commit 399796bac34ca9ebbeb7cd37c030cca05da70dda Author: alansley <aclansley@gmail.com> Date: Tue Aug 6 15:51:53 2024 +1000 AccessibilityId WIP - up to AccessibilityId_reveal_recovery_phrase_button commit a8d72dfcc073530fab923f95d08dc10c15852e03 Author: alansley <aclansley@gmail.com> Date: Tue Aug 6 14:12:10 2024 +1000 Cleaned up a few comments and fixed some plurals logic commit be400d8f4f9289de26d70eafa001f16fc039e7e0 Author: alansley <aclansley@gmail.com> Date: Tue Aug 6 11:32:08 2024 +1000 Removed commented out merge conflict marker commit 5cbe289a8d562f7e187f6e6e494b26e88094c5e0 Merge: 5fe123e7b5 d6c5ab2b18 Author: alansley <aclansley@gmail.com> Date: Tue Aug 6 11:30:50 2024 +1000 Merge dev and cleanup commit 5fe123e7b54dfa4f5056af00e5440f01ec226a4e Author: Al Lansley <al@oxen.io> Date: Mon Aug 5 14:37:47 2024 +1000 Adjusted sending of mms messages to show 'Uploading' rather than 'Sending' as per SES-1721 commit d3f8e928b6799bccf8fd2e9e74dc1eedca80340b Merge: 00552930e6 cd1a0643e3 Author: Al Lansley <al@oxen.io> Date: Mon Aug 5 13:30:03 2024 +1000 Merge branch 'dev' into strings-squashed commit 00552930e604176f2dd679e9c8e956030078d39c Author: Al Lansley <al@oxen.io> Date: Mon Aug 5 13:28:55 2024 +1000 Removed unused helpReportABugDesktop strings commit 6c0450b487b17e99e87805ada59a8bb583b86fac Author: Al Lansley <al@oxen.io> Date: Mon Aug 5 12:59:15 2024 +1000 Renamed 'quitButton' string to just 'quit' commit 284c4859038362b8ef065d08507c43ccd444d27c Author: Al Lansley <al@oxen.io> Date: Mon Aug 5 12:00:35 2024 +1000 Replaced 'screenSecurity' with 'screenshotNotifications' as the title of the notifications toggle commit 6948d64fa88d75a5a8bf6de4c5b8ababfc1a8445 Author: Al Lansley <al@oxen.io> Date: Mon Aug 5 10:45:05 2024 +1000 WIP commit bc94cb78db54b39c7276014809e55378d38056a0 Author: alansley <aclansley@gmail.com> Date: Fri Aug 2 16:21:16 2024 +1000 End of day push commit 1a2df3798ae285c1f61061ecf2fc423065490f98 Merge: c7fdb6aed9 a56e1d0b91 Author: alansley <aclansley@gmail.com> Date: Fri Aug 2 15:20:19 2024 +1000 Merged dev commit c7fdb6aed94544dcbef278d26702f8d093bdd91c Author: alansley <aclansley@gmail.com> Date: Fri Aug 2 14:21:11 2024 +1000 Replaced string 'dialog_disappearing_messages_follow_setting_confirm' with 'confirm' commit 2992d590d9c1a5007941e17a404d692b89aa8899 Author: alansley <aclansley@gmail.com> Date: Fri Aug 2 14:01:00 2024 +1000 Removed string 'attachment_type_selector__gallery' and associated / un-used 'attachment_type_selector.xml' layout commit 4218663c956d1de735c2764bc42c47dc7d93b207 Author: alansley <aclansley@gmail.com> Date: Fri Aug 2 13:39:54 2024 +1000 Removed 'message_details_header__disappears' and the unused 'activity_message_detail.xml' which was the only reference to it commit ba2d0275e448c59c6f60ec6a087eb5ad2f1eff46 Author: alansley <aclansley@gmail.com> Date: Fri Aug 2 12:15:42 2024 +1000 Implemented task SS-79 to only provide a save attachment menu option when the attachment download is complete commit 20662c82222e9f2b3da698129b569be5bfe0f511 Merge: 608c984a6b fbbef4898a Author: alansley <aclansley@gmail.com> Date: Wed Jul 31 13:08:04 2024 +1000 Merge branch 'dev' into strings-squashed commit 608c984a6b550e18423d0159fa746af7fe5be426 Author: alansley <aclansley@gmail.com> Date: Tue Jul 30 16:58:08 2024 +1000 Actually remove the 4 specific time period mute strings commit 006a4e8bad85db54b25e3edfc5ed02c05ce834fe Author: alansley <aclansley@gmail.com> Date: Tue Jul 30 16:43:54 2024 +1000 Cleaned up MuteDialog.kt commit d3177f9f1a85ca772873fd4ec4f14d0d84c58e73 Author: alansley <aclansley@gmail.com> Date: Tue Jul 30 16:27:06 2024 +1000 Added a 1 second kludge to the mute for subtitle so that it initially shows 1 hour not 59 minutes etc. commit d568a86649b1d880b373ca525074de7443fcd8d6 Author: alansley <aclansley@gmail.com> Date: Tue Jul 30 16:20:20 2024 +1000 Removed 'Muted for' strings and fixed it up to use 'Mute for {large_time_unit}' across the board commit 84f6f19cf4f66b0309e07f82e120d83abdea326e Author: alansley <aclansley@gmail.com> Date: Tue Jul 30 11:03:46 2024 +1000 Changed some hard-coded 'Session' text in strings and renamed another commit bc90d18c91e2a2dbb15c090b5d9d7e2fd02a2acf Author: alansley <aclansley@gmail.com> Date: Tue Jul 30 10:27:55 2024 +1000 Cleaned up a leftover plural & changed 'app_name' to use 'sessionMessenger' string commit 79cd87878c18aad828df6142777b944e1d9eb9f2 Merge: 3b62e474b3 dec02cef5a Author: alansley <aclansley@gmail.com> Date: Tue Jul 30 08:16:02 2024 +1000 Merge branch 'dev' into strings-squashed commit 3b62e474b37bef9530ae7e74d28312c902653b1b Author: Al Lansley <al@oxen.io> Date: Mon Jul 29 16:33:21 2024 +1000 Down to just the final few straggler strings commit 13e81f046b7a781d8e8491170f52951f93353fce Author: Al Lansley <al@oxen.io> Date: Mon Jul 29 13:13:54 2024 +1000 WIP commit 2d9961d5c0e27332ab87a37e7e263645e490f51c Author: Al Lansley <al@oxen.io> Date: Mon Jul 29 08:58:01 2024 +1000 Further cleanup of stragglers commit 08b8a84309a8c91fb71cad313d52be565d86d3fd Author: Al Lansley <al@oxen.io> Date: Mon Jul 29 08:29:12 2024 +1000 Cleaning up straggler strings commit d0e87c64b594f34579f1dcd4884801374dc6dbd1 Author: alansley <aclansley@gmail.com> Date: Fri Jul 26 17:07:46 2024 +1000 WIP commit 4bc9d09be2ffd5eecfa0d0fad6a9bc53f019307c Author: alansley <aclansley@gmail.com> Date: Fri Jul 26 16:30:28 2024 +1000 WIP commit 3cee4bc12f778a3b9a5560092430303ba3f12a0b Merge: aa1db13e3a a495ec232a Author: alansley <aclansley@gmail.com> Date: Fri Jul 26 13:57:09 2024 +1000 Removed some legacy strings & substituted others commit aa1db13e3a254c2b2972ba3040db087b16644033 Author: fanchao <git@fanchao.dev> Date: Fri Jul 26 11:34:05 2024 +1000 Initial squash merge for strings
This commit is contained in:
parent
d1c4283f42
commit
67bcc937ce
@ -221,11 +221,13 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation project(':content-descriptions')
|
||||||
|
|
||||||
|
ksp("androidx.hilt:hilt-compiler:$jetpackHiltVersion")
|
||||||
|
ksp("com.google.dagger:hilt-compiler:$daggerHiltVersion")
|
||||||
|
ksp("com.github.bumptech.glide:ksp:$glideVersion")
|
||||||
|
|
||||||
implementation("com.google.dagger:hilt-android:$daggerHiltVersion")
|
implementation("com.google.dagger:hilt-android:$daggerHiltVersion")
|
||||||
ksp("com.google.dagger:hilt-compiler:$daggerHiltVersion")
|
|
||||||
ksp("androidx.hilt:hilt-compiler:$jetpackHiltVersion")
|
|
||||||
|
|
||||||
implementation "androidx.appcompat:appcompat:$appcompatVersion"
|
implementation "androidx.appcompat:appcompat:$appcompatVersion"
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||||
implementation "com.google.android.material:material:$materialVersion"
|
implementation "com.google.android.material:material:$materialVersion"
|
||||||
@ -249,12 +251,15 @@ dependencies {
|
|||||||
implementation 'androidx.fragment:fragment-ktx:1.5.3'
|
implementation 'androidx.fragment:fragment-ktx:1.5.3'
|
||||||
implementation "androidx.core:core-ktx:$coreVersion"
|
implementation "androidx.core:core-ktx:$coreVersion"
|
||||||
implementation "androidx.work:work-runtime-ktx:2.7.1"
|
implementation "androidx.work:work-runtime-ktx:2.7.1"
|
||||||
|
|
||||||
playImplementation ("com.google.firebase:firebase-messaging:24.0.0") {
|
playImplementation ("com.google.firebase:firebase-messaging:24.0.0") {
|
||||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||||
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
||||||
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (project.hasProperty('huawei')) huaweiImplementation 'com.huawei.hms:push:6.7.0.300'
|
if (project.hasProperty('huawei')) huaweiImplementation 'com.huawei.hms:push:6.7.0.300'
|
||||||
|
|
||||||
implementation 'androidx.media3:media3-exoplayer:1.4.0'
|
implementation 'androidx.media3:media3-exoplayer:1.4.0'
|
||||||
implementation 'androidx.media3:media3-ui:1.4.0'
|
implementation 'androidx.media3:media3-ui:1.4.0'
|
||||||
implementation 'org.conscrypt:conscrypt-android:2.5.2'
|
implementation 'org.conscrypt:conscrypt-android:2.5.2'
|
||||||
@ -268,7 +273,6 @@ dependencies {
|
|||||||
implementation 'com.github.chrisbanes:PhotoView:2.1.3'
|
implementation 'com.github.chrisbanes:PhotoView:2.1.3'
|
||||||
implementation "com.github.bumptech.glide:glide:$glideVersion"
|
implementation "com.github.bumptech.glide:glide:$glideVersion"
|
||||||
implementation "com.github.bumptech.glide:compose:1.0.0-beta01"
|
implementation "com.github.bumptech.glide:compose:1.0.0-beta01"
|
||||||
ksp "com.github.bumptech.glide:ksp:$glideVersion"
|
|
||||||
implementation 'com.makeramen:roundedimageview:2.1.0'
|
implementation 'com.makeramen:roundedimageview:2.1.0'
|
||||||
implementation 'com.pnikosis:materialish-progress:1.5'
|
implementation 'com.pnikosis:materialish-progress:1.5'
|
||||||
implementation 'org.greenrobot:eventbus:3.0.0'
|
implementation 'org.greenrobot:eventbus:3.0.0'
|
||||||
|
@ -2,8 +2,6 @@ package network.loki.messenger
|
|||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.app.Instrumentation
|
import android.app.Instrumentation
|
||||||
import android.content.ClipboardManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.test.espresso.Espresso.onView
|
import androidx.test.espresso.Espresso.onView
|
||||||
import androidx.test.espresso.Espresso.pressBack
|
import androidx.test.espresso.Espresso.pressBack
|
||||||
@ -16,8 +14,6 @@ import androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA
|
|||||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.isRoot
|
import androidx.test.espresso.matcher.ViewMatchers.isRoot
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.withSubstring
|
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
|
||||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.LargeTest
|
import androidx.test.filters.LargeTest
|
||||||
@ -25,6 +21,7 @@ import androidx.test.platform.app.InstrumentationRegistry
|
|||||||
import androidx.test.uiautomator.By
|
import androidx.test.uiautomator.By
|
||||||
import androidx.test.uiautomator.UiDevice
|
import androidx.test.uiautomator.UiDevice
|
||||||
import com.adevinta.android.barista.interaction.PermissionGranter
|
import com.adevinta.android.barista.interaction.PermissionGranter
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
import network.loki.messenger.util.InputBarButtonDrawableMatcher.Companion.inputButtonWithDrawable
|
import network.loki.messenger.util.InputBarButtonDrawableMatcher.Companion.inputButtonWithDrawable
|
||||||
import org.hamcrest.Matcher
|
import org.hamcrest.Matcher
|
||||||
import org.hamcrest.Matchers.allOf
|
import org.hamcrest.Matchers.allOf
|
||||||
@ -36,11 +33,9 @@ import org.junit.Test
|
|||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsignal.utilities.guava.Optional
|
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBar
|
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBar
|
||||||
import org.thoughtcrime.securesms.home.HomeActivity
|
import org.thoughtcrime.securesms.home.HomeActivity
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Currently not used as part of our CI/Deployment processes !!!!
|
* Currently not used as part of our CI/Deployment processes !!!!
|
||||||
@ -62,7 +57,6 @@ class HomeActivityTests {
|
|||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
InstrumentationRegistry.getInstrumentation().addMonitor(activityMonitor)
|
InstrumentationRegistry.getInstrumentation().addMonitor(activityMonitor)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
@ -96,10 +90,10 @@ class HomeActivityTests {
|
|||||||
device.pressKeyCode(67)
|
device.pressKeyCode(67)
|
||||||
|
|
||||||
// Continue with display name
|
// Continue with display name
|
||||||
objectFromDesc(R.string.continue_2).click()
|
objectFromDesc(R.string.theContinue).click()
|
||||||
|
|
||||||
// Continue with default push notification setting
|
// Continue with default push notification setting
|
||||||
objectFromDesc(R.string.continue_2).click()
|
objectFromDesc(R.string.theContinue).click()
|
||||||
|
|
||||||
// PN select
|
// PN select
|
||||||
if (hasViewedSeed) {
|
if (hasViewedSeed) {
|
||||||
@ -110,7 +104,6 @@ class HomeActivityTests {
|
|||||||
PermissionGranter.allowPermissionsIfNeeded(Manifest.permission.POST_NOTIFICATIONS)
|
PermissionGranter.allowPermissionsIfNeeded(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* private fun goToMyChat() {
|
/* private fun goToMyChat() {
|
||||||
onView(withId(R.id.newConversationButton)).perform(ViewActions.click())
|
onView(withId(R.id.newConversationButton)).perform(ViewActions.click())
|
||||||
onView(withId(R.id.createPrivateChatButton)).perform(ViewActions.click())
|
onView(withId(R.id.createPrivateChatButton)).perform(ViewActions.click())
|
||||||
@ -131,7 +124,7 @@ class HomeActivityTests {
|
|||||||
@Test
|
@Test
|
||||||
fun testLaunches_dismiss_seedView() {
|
fun testLaunches_dismiss_seedView() {
|
||||||
setupLoggedInState()
|
setupLoggedInState()
|
||||||
objectFromDesc(R.string.continue_2).click()
|
objectFromDesc(R.string.theContinue).click()
|
||||||
objectFromDesc(R.string.copy).click()
|
objectFromDesc(R.string.copy).click()
|
||||||
pressBack()
|
pressBack()
|
||||||
onView(withId(R.id.seedReminderView)).check(matches(not(isDisplayed())))
|
onView(withId(R.id.seedReminderView)).check(matches(not(isDisplayed())))
|
||||||
@ -182,6 +175,7 @@ class HomeActivityTests {
|
|||||||
onView(withText(dialogPromptText)).check(matches(isDisplayed()))
|
onView(withText(dialogPromptText)).check(matches(isDisplayed()))
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform action of waiting for a specific time.
|
* Perform action of waiting for a specific time.
|
||||||
*/
|
*/
|
||||||
@ -198,5 +192,4 @@ class HomeActivityTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -37,7 +37,7 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
|
||||||
<uses-permission android:name="network.loki.messenger.ACCESS_SESSION_SECRETS" />
|
<uses-permission android:name="network.loki.messenger.ACCESS_SESSION_SECRETS" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- Only used on Android API 29 and lower -->
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
|
||||||
@ -79,7 +79,7 @@
|
|||||||
android:networkSecurityConfig="@xml/network_security_configuration"
|
android:networkSecurityConfig="@xml/network_security_configuration"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Session.DayNight"
|
android:theme="@style/Theme.Session.DayNight"
|
||||||
tools:replace="android:allowBackup">
|
tools:replace="android:allowBackup,android:label" >
|
||||||
|
|
||||||
<!-- Disable all analytics -->
|
<!-- Disable all analytics -->
|
||||||
|
|
||||||
@ -130,12 +130,12 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity"
|
android:name="org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/activity_message_requests_title"
|
android:label="@string/sessionMessageRequests"
|
||||||
android:screenOrientation="portrait" />
|
android:screenOrientation="portrait" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.preferences.SettingsActivity"
|
android:name="org.thoughtcrime.securesms.preferences.SettingsActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:label="@string/activity_settings_title" />
|
android:label="@string/sessionSettings" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.debugmenu.DebugActivity"
|
android:name="org.thoughtcrime.securesms.debugmenu.DebugActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
@ -151,11 +151,11 @@
|
|||||||
android:name="org.thoughtcrime.securesms.preferences.BlockedContactsActivity"
|
android:name="org.thoughtcrime.securesms.preferences.BlockedContactsActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar"
|
android:theme="@style/Theme.Session.DayNight.FlatActionBar"
|
||||||
android:label="@string/blocked_contacts_title"
|
android:label="@string/conversationsBlockedContacts"
|
||||||
/>
|
/>
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.groups.EditClosedGroupActivity"
|
android:name="org.thoughtcrime.securesms.groups.EditClosedGroupActivity"
|
||||||
android:label="@string/activity_edit_closed_group_title"
|
android:label="@string/groupEdit"
|
||||||
android:screenOrientation="portrait" />
|
android:screenOrientation="portrait" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.recoverypassword.RecoveryPasswordActivity"
|
android:name="org.thoughtcrime.securesms.recoverypassword.RecoveryPasswordActivity"
|
||||||
@ -165,7 +165,7 @@
|
|||||||
android:screenOrientation="portrait" />
|
android:screenOrientation="portrait" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.preferences.PrivacySettingsActivity"
|
android:name="org.thoughtcrime.securesms.preferences.PrivacySettingsActivity"
|
||||||
android:label="@string/activity_privacy_settings_title"
|
android:label="@string/sessionPrivacy"
|
||||||
android:screenOrientation="portrait" />
|
android:screenOrientation="portrait" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.preferences.NotificationSettingsActivity"
|
android:name="org.thoughtcrime.securesms.preferences.NotificationSettingsActivity"
|
||||||
@ -175,7 +175,7 @@
|
|||||||
android:screenOrientation="portrait" />
|
android:screenOrientation="portrait" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.preferences.HelpSettingsActivity"
|
android:name="org.thoughtcrime.securesms.preferences.HelpSettingsActivity"
|
||||||
android:label="@string/activity_help_settings_title"
|
android:label="@string/sessionHelp"
|
||||||
android:screenOrientation="portrait" />
|
android:screenOrientation="portrait" />
|
||||||
<activity android:name="org.thoughtcrime.securesms.preferences.appearance.AppearanceSettingsActivity"
|
<activity android:name="org.thoughtcrime.securesms.preferences.appearance.AppearanceSettingsActivity"
|
||||||
android:screenOrientation="portrait"/>
|
android:screenOrientation="portrait"/>
|
||||||
@ -268,18 +268,10 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.MediaPreviewActivity"
|
android:name="org.thoughtcrime.securesms.MediaPreviewActivity"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
android:label="@string/AndroidManifest__media_preview"
|
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.Session.DayNight.NoActionBar"
|
android:theme="@style/Theme.Session.DayNight.NoActionBar"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:windowSoftInputMode="stateHidden" />
|
android:windowSoftInputMode="stateHidden" />
|
||||||
<activity
|
|
||||||
android:name="org.thoughtcrime.securesms.MediaOverviewActivity"
|
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:screenOrientation="portrait"
|
|
||||||
android:theme="@style/Theme.Session.DayNight.NoActionBar"
|
|
||||||
android:windowSoftInputMode="stateHidden" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.DummyActivity"
|
android:name="org.thoughtcrime.securesms.DummyActivity"
|
||||||
android:allowTaskReparenting="true"
|
android:allowTaskReparenting="true"
|
||||||
|
@ -8,19 +8,8 @@ class DeleteMediaDialog {
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun show(context: Context, recordCount: Int, doDelete: Runnable) = context.showSessionDialog {
|
fun show(context: Context, recordCount: Int, doDelete: Runnable) = context.showSessionDialog {
|
||||||
iconAttribute(R.attr.dialog_alert_icon)
|
iconAttribute(R.attr.dialog_alert_icon)
|
||||||
title(
|
title(context.resources.getQuantityString(R.plurals.deleteMessage, recordCount, recordCount))
|
||||||
context.resources.getQuantityString(
|
text(context.resources.getString(R.string.deleteMessageDescriptionEveryone))
|
||||||
R.plurals.MediaOverviewActivity_Media_delete_confirm_title,
|
|
||||||
recordCount,
|
|
||||||
recordCount
|
|
||||||
)
|
|
||||||
)
|
|
||||||
text(
|
|
||||||
context.resources.getQuantityString(R.plurals.MediaOverviewActivity_Media_delete_confirm_message,
|
|
||||||
recordCount,
|
|
||||||
recordCount
|
|
||||||
)
|
|
||||||
)
|
|
||||||
button(R.string.delete) { doDelete.run() }
|
button(R.string.delete) { doDelete.run() }
|
||||||
cancelButton()
|
cancelButton()
|
||||||
}
|
}
|
||||||
|
@ -9,9 +9,9 @@ class DeleteMediaPreviewDialog {
|
|||||||
fun show(context: Context, doDelete: Runnable) {
|
fun show(context: Context, doDelete: Runnable) {
|
||||||
context.showSessionDialog {
|
context.showSessionDialog {
|
||||||
iconAttribute(R.attr.dialog_alert_icon)
|
iconAttribute(R.attr.dialog_alert_icon)
|
||||||
title(R.string.MediaPreviewActivity_media_delete_confirmation_title)
|
title(context.resources.getQuantityString(R.plurals.deleteMessage, 1, 1))
|
||||||
text(R.string.MediaPreviewActivity_media_delete_confirmation_message)
|
text(R.string.deleteMessageDescriptionEveryone)
|
||||||
button(R.string.delete) { doDelete.run() }
|
dangerButton(R.string.delete) { doDelete.run() }
|
||||||
cancelButton()
|
cancelButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,8 @@
|
|||||||
*/
|
*/
|
||||||
package org.thoughtcrime.securesms;
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
|
import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
@ -23,8 +25,8 @@ import android.database.Cursor;
|
|||||||
import android.database.CursorIndexOutOfBoundsException;
|
import android.database.CursorIndexOutOfBoundsException;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Build.VERSION;
|
import android.os.Build.VERSION;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
@ -42,7 +44,6 @@ import android.view.WindowInsetsController;
|
|||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
@ -54,10 +55,14 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
|||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import androidx.viewpager.widget.PagerAdapter;
|
import androidx.viewpager.widget.PagerAdapter;
|
||||||
import androidx.viewpager.widget.ViewPager;
|
import androidx.viewpager.widget.ViewPager;
|
||||||
|
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
import com.bumptech.glide.RequestManager;
|
import com.bumptech.glide.RequestManager;
|
||||||
|
import com.squareup.phrase.Phrase;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.WeakHashMap;
|
||||||
|
import kotlin.Unit;
|
||||||
|
import network.loki.messenger.R;
|
||||||
import org.session.libsession.messaging.messages.control.DataExtractionNotification;
|
import org.session.libsession.messaging.messages.control.DataExtractionNotification;
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
|
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
|
||||||
@ -78,15 +83,8 @@ import org.thoughtcrime.securesms.mms.Slide;
|
|||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.util.AttachmentUtil;
|
import org.thoughtcrime.securesms.util.AttachmentUtil;
|
||||||
import org.thoughtcrime.securesms.util.DateUtils;
|
import org.thoughtcrime.securesms.util.DateUtils;
|
||||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
|
||||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
|
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
|
||||||
|
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.WeakHashMap;
|
|
||||||
|
|
||||||
import kotlin.Unit;
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity for displaying media attachments in-app
|
* Activity for displaying media attachments in-app
|
||||||
@ -242,12 +240,12 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
CharSequence relativeTimeSpan;
|
CharSequence relativeTimeSpan;
|
||||||
|
|
||||||
if (mediaItem.date > 0) {
|
if (mediaItem.date > 0) {
|
||||||
relativeTimeSpan = DateUtils.getDisplayFormattedTimeSpanString(this, Locale.getDefault(), mediaItem.date);
|
relativeTimeSpan = DateUtils.INSTANCE.getDisplayFormattedTimeSpanString(this, Locale.getDefault(), mediaItem.date);
|
||||||
} else {
|
} else {
|
||||||
relativeTimeSpan = getString(R.string.MediaPreviewActivity_draft);
|
relativeTimeSpan = getString(R.string.draft);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mediaItem.outgoing) getSupportActionBar().setTitle(getString(R.string.MediaPreviewActivity_you));
|
if (mediaItem.outgoing) getSupportActionBar().setTitle(getString(R.string.you));
|
||||||
else if (mediaItem.recipient != null) getSupportActionBar().setTitle(mediaItem.recipient.toShortString());
|
else if (mediaItem.recipient != null) getSupportActionBar().setTitle(mediaItem.recipient.toShortString());
|
||||||
else getSupportActionBar().setTitle("");
|
else getSupportActionBar().setTitle("");
|
||||||
|
|
||||||
@ -258,7 +256,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
initializeMedia();
|
initializeMedia();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -291,7 +288,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
captionContainer = findViewById(R.id.media_preview_caption_container);
|
captionContainer = findViewById(R.id.media_preview_caption_container);
|
||||||
playbackControlsContainer = findViewById(R.id.media_preview_playback_controls_container);
|
playbackControlsContainer = findViewById(R.id.media_preview_playback_controls_container);
|
||||||
|
|
||||||
setSupportActionBar(findViewById(R.id.toolbar));
|
setSupportActionBar(findViewById(R.id.search_toolbar));
|
||||||
ActionBar actionBar = getSupportActionBar();
|
ActionBar actionBar = getSupportActionBar();
|
||||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||||
actionBar.setHomeButtonEnabled(true);
|
actionBar.setHomeButtonEnabled(true);
|
||||||
@ -361,7 +358,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
private void initializeMedia() {
|
private void initializeMedia() {
|
||||||
if (!isContentTypeSupported(initialMediaType)) {
|
if (!isContentTypeSupported(initialMediaType)) {
|
||||||
Log.w(TAG, "Unsupported media type sent to MediaPreviewActivity, finishing.");
|
Log.w(TAG, "Unsupported media type sent to MediaPreviewActivity, finishing.");
|
||||||
Toast.makeText(getApplicationContext(), R.string.MediaPreviewActivity_unssuported_media_type, Toast.LENGTH_LONG).show();
|
Toast.makeText(getApplicationContext(), R.string.attachmentsErrorNotSupported, Toast.LENGTH_LONG).show();
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -411,12 +408,19 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
MediaItem mediaItem = getCurrentMediaItem();
|
MediaItem mediaItem = getCurrentMediaItem();
|
||||||
if (mediaItem == null) return;
|
if (mediaItem == null) return;
|
||||||
|
|
||||||
SaveAttachmentTask.showWarningDialog(this, 1, () -> {
|
SaveAttachmentTask.showOneTimeWarningDialogOrSave(this, 1, () -> {
|
||||||
Permissions.with(this)
|
Permissions.with(this)
|
||||||
.request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
.request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
.maxSdkVersion(Build.VERSION_CODES.P)
|
.maxSdkVersion(Build.VERSION_CODES.P)
|
||||||
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
|
.withPermanentDenialDialog(Phrase.from(getApplicationContext(), R.string.permissionsStorageSaveDenied)
|
||||||
.onAnyDenied(() -> Toast.makeText(this, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
|
.put(APP_NAME_KEY, getString(R.string.app_name))
|
||||||
|
.format().toString())
|
||||||
|
.onAnyDenied(() -> {
|
||||||
|
String txt = Phrase.from(getApplicationContext(), R.string.permissionsStorageSaveDenied)
|
||||||
|
.put(APP_NAME_KEY, getString(R.string.app_name))
|
||||||
|
.format().toString();
|
||||||
|
Toast.makeText(this, txt, Toast.LENGTH_LONG).show();
|
||||||
|
})
|
||||||
.onAllGranted(() -> {
|
.onAllGranted(() -> {
|
||||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this);
|
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this);
|
||||||
long saveDate = (mediaItem.date > 0) ? mediaItem.date : SnodeAPI.getNowWithOffset();
|
long saveDate = (mediaItem.date > 0) ? mediaItem.date : SnodeAPI.getNowWithOffset();
|
||||||
@ -482,6 +486,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
super.onOptionsItemSelected(item);
|
super.onOptionsItemSelected(item);
|
||||||
|
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
|
// TODO / WARNING: R.id values are NON-CONSTANT in Gradle 8.0+ - what would be the best way to address this?! -AL 2024/08/26
|
||||||
case R.id.media_preview__overview: showOverview(); return true;
|
case R.id.media_preview__overview: showOverview(); return true;
|
||||||
case R.id.media_preview__forward: forward(); return true;
|
case R.id.media_preview__forward: forward(); return true;
|
||||||
case R.id.save: saveToDisk(); return true;
|
case R.id.save: saveToDisk(); return true;
|
||||||
@ -532,15 +537,11 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
throw new RuntimeException("restartItem = " + restartItem + ", data.second = " + data.second + " leftIsRecent = " + leftIsRecent, e);
|
throw new RuntimeException("restartItem = " + restartItem + ", data.second = " + data.second + " leftIsRecent = " + leftIsRecent, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item == 0) {
|
if (item == 0) { viewPagerListener.onPageSelected(0); }
|
||||||
viewPagerListener.onPageSelected(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoaderReset(@NonNull Loader<Pair<Cursor, Integer>> loader) {
|
public void onLoaderReset(@NonNull Loader<Pair<Cursor, Integer>> loader) { /* Do nothing */ }
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ViewPagerListener implements ViewPager.OnPageChangeListener {
|
private class ViewPagerListener implements ViewPager.OnPageChangeListener {
|
||||||
|
|
||||||
@ -575,13 +576,11 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
|
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
|
||||||
|
/* Do nothing */
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPageScrollStateChanged(int state) {
|
public void onPageScrollStateChanged(int state) { /* Do nothing */ }
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class SingleItemPagerAdapter extends MediaItemAdapter {
|
private static class SingleItemPagerAdapter extends MediaItemAdapter {
|
||||||
@ -646,9 +645,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void pause(int position) {
|
public void pause(int position) { /* Do nothing */ }
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable View getPlaybackControls(int position) {
|
public @Nullable View getPlaybackControls(int position) {
|
||||||
|
@ -4,24 +4,45 @@ import android.content.Context
|
|||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.LocalisedTimeUtil
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.TIME_LARGE_KEY
|
||||||
|
import org.thoughtcrime.securesms.ui.getSubbedString
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
fun showMuteDialog(
|
fun showMuteDialog(
|
||||||
context: Context,
|
context: Context,
|
||||||
onMuteDuration: (Long) -> Unit
|
onMuteDuration: (Long) -> Unit
|
||||||
): AlertDialog = context.showSessionDialog {
|
): AlertDialog = context.showSessionDialog {
|
||||||
title(R.string.MuteDialog_mute_notifications)
|
title(R.string.notificationsMute)
|
||||||
items(Option.values().map { it.stringRes }.map(context::getString).toTypedArray()) {
|
|
||||||
onMuteDuration(Option.values()[it].getTime())
|
items(Option.entries.mapIndexed { index, entry ->
|
||||||
|
|
||||||
|
if (entry.stringRes == R.string.notificationsMute) {
|
||||||
|
context.getString(R.string.notificationsMute)
|
||||||
|
} else {
|
||||||
|
val largeTimeUnitString = LocalisedTimeUtil.getDurationWithSingleLargestTimeUnit(context, Option.entries[index].getTime().milliseconds)
|
||||||
|
context.getSubbedString(entry.stringRes, TIME_LARGE_KEY to largeTimeUnitString)
|
||||||
|
}
|
||||||
|
}.toTypedArray()) {
|
||||||
|
// Note: We add the current timestamp to the mute duration to get the un-mute timestamp
|
||||||
|
// that gets stored in the database via ConversationMenuHelper.mute().
|
||||||
|
// Also: This is a kludge, but we ADD one second to the mute duration because otherwise by
|
||||||
|
// the time the view for how long the conversation is muted for gets set then it's actually
|
||||||
|
// less than the entire duration - so 1 hour becomes 59 minutes, 1 day becomes 23 hours etc.
|
||||||
|
// As we really want to see the actual set time (1 hour / 1 day etc.) then we'll bump it by
|
||||||
|
// 1 second which is neither here nor there in the grand scheme of things.
|
||||||
|
onMuteDuration(Option.entries[it].getTime() + System.currentTimeMillis() + 1.seconds.inWholeMilliseconds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum class Option(@StringRes val stringRes: Int, val getTime: () -> Long) {
|
private enum class Option(@StringRes val stringRes: Int, val getTime: () -> Long) {
|
||||||
ONE_HOUR(R.string.arrays__mute_for_one_hour, duration = TimeUnit.HOURS.toMillis(1)),
|
ONE_HOUR(R.string.notificationsMuteFor, duration = TimeUnit.HOURS.toMillis(1)),
|
||||||
TWO_HOURS(R.string.arrays__mute_for_two_hours, duration = TimeUnit.HOURS.toMillis(2)),
|
TWO_HOURS(R.string.notificationsMuteFor, duration = TimeUnit.HOURS.toMillis(2)),
|
||||||
ONE_DAY(R.string.arrays__mute_for_one_day, duration = TimeUnit.DAYS.toMillis(1)),
|
ONE_DAY(R.string.notificationsMuteFor, duration = TimeUnit.DAYS.toMillis(1)),
|
||||||
SEVEN_DAYS(R.string.arrays__mute_for_seven_days, duration = TimeUnit.DAYS.toMillis(7)),
|
SEVEN_DAYS(R.string.notificationsMuteFor, duration = TimeUnit.DAYS.toMillis(7)),
|
||||||
FOREVER(R.string.arrays__mute_forever, getTime = { Long.MAX_VALUE });
|
FOREVER(R.string.notificationsMute, getTime = { Long.MAX_VALUE } );
|
||||||
|
|
||||||
constructor(@StringRes stringRes: Int, duration: Long): this(stringRes, { System.currentTimeMillis() + duration })
|
constructor(@StringRes stringRes: Int, duration: Long): this(stringRes, { duration } )
|
||||||
}
|
}
|
@ -16,6 +16,8 @@
|
|||||||
*/
|
*/
|
||||||
package org.thoughtcrime.securesms;
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
|
import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
|
||||||
|
|
||||||
import android.animation.Animator;
|
import android.animation.Animator;
|
||||||
import android.app.KeyguardManager;
|
import android.app.KeyguardManager;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
@ -25,20 +27,18 @@ import android.content.ServiceConnection;
|
|||||||
import android.graphics.PorterDuff;
|
import android.graphics.PorterDuff;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.text.SpannableString;
|
|
||||||
import android.text.Spanned;
|
|
||||||
import android.text.style.RelativeSizeSpan;
|
|
||||||
import android.text.style.TypefaceSpan;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.animation.Animation;
|
import android.view.animation.Animation;
|
||||||
import android.view.animation.BounceInterpolator;
|
import android.view.animation.BounceInterpolator;
|
||||||
import android.view.animation.TranslateAnimation;
|
import android.view.animation.TranslateAnimation;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
|
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
|
||||||
import androidx.core.os.CancellationSignal;
|
import androidx.core.os.CancellationSignal;
|
||||||
|
import com.squareup.phrase.Phrase;
|
||||||
|
import java.security.Signature;
|
||||||
|
import network.loki.messenger.R;
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.components.AnimatingToggle;
|
import org.thoughtcrime.securesms.components.AnimatingToggle;
|
||||||
@ -46,11 +46,6 @@ import org.thoughtcrime.securesms.crypto.BiometricSecretProvider;
|
|||||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||||
import org.thoughtcrime.securesms.util.AnimationCompleteListener;
|
import org.thoughtcrime.securesms.util.AnimationCompleteListener;
|
||||||
|
|
||||||
import java.security.InvalidKeyException;
|
|
||||||
import java.security.Signature;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
//TODO Rename to ScreenLockActivity and refactor to Kotlin.
|
//TODO Rename to ScreenLockActivity and refactor to Kotlin.
|
||||||
public class PassphrasePromptActivity extends BaseActionBarActivity {
|
public class PassphrasePromptActivity extends BaseActionBarActivity {
|
||||||
|
|
||||||
@ -158,6 +153,16 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initializeResources() {
|
private void initializeResources() {
|
||||||
|
|
||||||
|
TextView statusTitle = findViewById(R.id.app_lock_status_title);
|
||||||
|
if (statusTitle != null) {
|
||||||
|
Context c = getApplicationContext();
|
||||||
|
String lockedTxt = Phrase.from(c, R.string.lockAppLocked)
|
||||||
|
.put(APP_NAME_KEY, c.getString(R.string.app_name))
|
||||||
|
.format().toString();
|
||||||
|
statusTitle.setText(lockedTxt);
|
||||||
|
}
|
||||||
|
|
||||||
visibilityToggle = findViewById(R.id.button_toggle);
|
visibilityToggle = findViewById(R.id.button_toggle);
|
||||||
fingerprintPrompt = findViewById(R.id.fingerprint_auth_container);
|
fingerprintPrompt = findViewById(R.id.fingerprint_auth_container);
|
||||||
lockScreenButton = findViewById(R.id.lock_screen_auth_container);
|
lockScreenButton = findViewById(R.id.lock_screen_auth_container);
|
||||||
@ -165,10 +170,6 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
|
|||||||
fingerprintCancellationSignal = new CancellationSignal();
|
fingerprintCancellationSignal = new CancellationSignal();
|
||||||
fingerprintListener = new FingerprintListener();
|
fingerprintListener = new FingerprintListener();
|
||||||
|
|
||||||
SpannableString hint = new SpannableString(" " + getString(R.string.PassphrasePromptActivity_enter_passphrase));
|
|
||||||
hint.setSpan(new RelativeSizeSpan(0.9f), 0, hint.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
|
|
||||||
hint.setSpan(new TypefaceSpan("sans-serif"), 0, hint.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
|
|
||||||
|
|
||||||
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
|
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
|
||||||
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_primary), PorterDuff.Mode.SRC_IN);
|
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_primary), PorterDuff.Mode.SRC_IN);
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package org.thoughtcrime.securesms
|
package org.thoughtcrime.securesms
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@ -7,12 +9,14 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.LinearLayout.VERTICAL
|
import android.widget.LinearLayout.VERTICAL
|
||||||
import android.widget.Space
|
import android.widget.Space
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.AttrRes
|
import androidx.annotation.AttrRes
|
||||||
|
import androidx.annotation.ColorRes
|
||||||
import androidx.annotation.LayoutRes
|
import androidx.annotation.LayoutRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.annotation.StyleRes
|
import androidx.annotation.StyleRes
|
||||||
@ -30,6 +34,7 @@ annotation class DialogDsl
|
|||||||
@DialogDsl
|
@DialogDsl
|
||||||
class SessionDialogBuilder(val context: Context) {
|
class SessionDialogBuilder(val context: Context) {
|
||||||
|
|
||||||
|
private val dp8 = toPx(8, context.resources)
|
||||||
private val dp20 = toPx(20, context.resources)
|
private val dp20 = toPx(20, context.resources)
|
||||||
private val dp40 = toPx(40, context.resources)
|
private val dp40 = toPx(40, context.resources)
|
||||||
private val dp60 = toPx(60, context.resources)
|
private val dp60 = toPx(60, context.resources)
|
||||||
@ -37,13 +42,15 @@ class SessionDialogBuilder(val context: Context) {
|
|||||||
private val dialogBuilder: AlertDialog.Builder = AlertDialog.Builder(context)
|
private val dialogBuilder: AlertDialog.Builder = AlertDialog.Builder(context)
|
||||||
|
|
||||||
private var dialog: AlertDialog? = null
|
private var dialog: AlertDialog? = null
|
||||||
private fun dismiss() = dialog?.dismiss()
|
fun dismiss() = dialog?.dismiss()
|
||||||
|
|
||||||
private val topView = LinearLayout(context)
|
private val topView = LinearLayout(context)
|
||||||
.apply { setPadding(0, dp20, 0, 0) }
|
.apply { setPadding(0, dp20, 0, 0) }
|
||||||
.apply { orientation = VERTICAL }
|
.apply { orientation = VERTICAL }
|
||||||
.also(dialogBuilder::setCustomTitle)
|
.also(dialogBuilder::setCustomTitle)
|
||||||
|
|
||||||
private val contentView = LinearLayout(context).apply { orientation = VERTICAL }
|
private val contentView = LinearLayout(context).apply { orientation = VERTICAL }
|
||||||
|
|
||||||
private val buttonLayout = LinearLayout(context)
|
private val buttonLayout = LinearLayout(context)
|
||||||
|
|
||||||
private val root = LinearLayout(context).apply { orientation = VERTICAL }
|
private val root = LinearLayout(context).apply { orientation = VERTICAL }
|
||||||
@ -53,24 +60,29 @@ class SessionDialogBuilder(val context: Context) {
|
|||||||
addView(buttonLayout)
|
addView(buttonLayout)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun title(@StringRes id: Int) = title(context.getString(id))
|
// Main title entry point
|
||||||
|
|
||||||
fun title(text: CharSequence?) = title(text?.toString())
|
|
||||||
fun title(text: String?) {
|
fun title(text: String?) {
|
||||||
text(text, R.style.TextAppearance_AppCompat_Title) { setPadding(dp20, 0, dp20, 0) }
|
text(text, R.style.TextAppearance_Session_Dialog_Title) { setPadding(dp20, 0, dp20, 0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun text(@StringRes id: Int, style: Int = 0) = text(context.getString(id), style)
|
// Convenience assessor for title that takes a string resource
|
||||||
fun text(text: CharSequence?, @StyleRes style: Int = 0) {
|
fun title(@StringRes id: Int) = title(context.getString(id))
|
||||||
|
|
||||||
|
// Convenience accessor for title that takes a CharSequence
|
||||||
|
fun title(text: CharSequence?) = title(text?.toString())
|
||||||
|
|
||||||
|
fun text(@StringRes id: Int, style: Int? = null) = text(context.getString(id), style)
|
||||||
|
|
||||||
|
fun text(text: CharSequence?, @StyleRes style: Int? = null) {
|
||||||
text(text, style) {
|
text(text, style) {
|
||||||
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
||||||
.apply { updateMargins(dp40, 0, dp40, 0) }
|
.apply { updateMargins(dp40, 0, dp40, 0) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun text(text: CharSequence?, @StyleRes style: Int, modify: TextView.() -> Unit) {
|
private fun text(text: CharSequence?, @StyleRes style: Int? = null, modify: TextView.() -> Unit) {
|
||||||
text ?: return
|
text ?: return
|
||||||
TextView(context, null, 0, style)
|
TextView(context, null, 0, style ?: R.style.TextAppearance_Session_Dialog_Message)
|
||||||
.apply {
|
.apply {
|
||||||
setText(text)
|
setText(text)
|
||||||
textAlignment = View.TEXT_ALIGNMENT_CENTER
|
textAlignment = View.TEXT_ALIGNMENT_CENTER
|
||||||
@ -78,7 +90,7 @@ class SessionDialogBuilder(val context: Context) {
|
|||||||
}.let(topView::addView)
|
}.let(topView::addView)
|
||||||
|
|
||||||
Space(context).apply {
|
Space(context).apply {
|
||||||
layoutParams = LinearLayout.LayoutParams(0, dp20)
|
layoutParams = LinearLayout.LayoutParams(0, dp8)
|
||||||
}.let(topView::addView)
|
}.let(topView::addView)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,17 +107,31 @@ class SessionDialogBuilder(val context: Context) {
|
|||||||
fun singleChoiceItems(
|
fun singleChoiceItems(
|
||||||
options: Collection<String>,
|
options: Collection<String>,
|
||||||
currentSelected: Int = 0,
|
currentSelected: Int = 0,
|
||||||
|
dismissOnRadioSelect: Boolean = true,
|
||||||
onSelect: (Int) -> Unit
|
onSelect: (Int) -> Unit
|
||||||
) = singleChoiceItems(options.toTypedArray(), currentSelected, onSelect)
|
) = singleChoiceItems(
|
||||||
|
options.toTypedArray(),
|
||||||
|
currentSelected,
|
||||||
|
dismissOnRadioSelect,
|
||||||
|
onSelect
|
||||||
|
)
|
||||||
|
|
||||||
fun singleChoiceItems(
|
fun singleChoiceItems(
|
||||||
options: Array<String>,
|
options: Array<String>,
|
||||||
currentSelected: Int = 0,
|
currentSelected: Int = 0,
|
||||||
|
dismissOnRadioSelect: Boolean = true,
|
||||||
onSelect: (Int) -> Unit
|
onSelect: (Int) -> Unit
|
||||||
): AlertDialog.Builder = dialogBuilder.setSingleChoiceItems(
|
): AlertDialog.Builder{
|
||||||
options,
|
val adapter = ArrayAdapter<CharSequence>(context, R.layout.view_dialog_single_choice_item, options)
|
||||||
currentSelected
|
|
||||||
) { dialog, it -> onSelect(it); dialog.dismiss() }
|
return dialogBuilder.setSingleChoiceItems(
|
||||||
|
adapter,
|
||||||
|
currentSelected
|
||||||
|
) { dialog, it ->
|
||||||
|
onSelect(it)
|
||||||
|
if(dismissOnRadioSelect) dialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun items(
|
fun items(
|
||||||
options: Array<String>,
|
options: Array<String>,
|
||||||
@ -125,16 +151,21 @@ class SessionDialogBuilder(val context: Context) {
|
|||||||
) { listener() }
|
) { listener() }
|
||||||
|
|
||||||
fun okButton(listener: (() -> Unit) = {}) = button(android.R.string.ok) { listener() }
|
fun okButton(listener: (() -> Unit) = {}) = button(android.R.string.ok) { listener() }
|
||||||
fun cancelButton(listener: (() -> Unit) = {}) = button(android.R.string.cancel, R.string.AccessibilityId_cancel_button) { listener() }
|
|
||||||
|
fun cancelButton(listener: (() -> Unit) = {}) = button(android.R.string.cancel, R.string.AccessibilityId_cancel) { listener() }
|
||||||
|
|
||||||
fun button(
|
fun button(
|
||||||
@StringRes text: Int,
|
@StringRes text: Int,
|
||||||
@StringRes contentDescriptionRes: Int = text,
|
@StringRes contentDescriptionRes: Int = text,
|
||||||
@StyleRes style: Int = R.style.Widget_Session_Button_Dialog_UnimportantText,
|
@StyleRes style: Int = R.style.Widget_Session_Button_Dialog_UnimportantText,
|
||||||
|
@ColorRes textColor: Int? = null,
|
||||||
dismiss: Boolean = true,
|
dismiss: Boolean = true,
|
||||||
listener: (() -> Unit) = {}
|
listener: (() -> Unit) = {}
|
||||||
) = Button(context, null, 0, style).apply {
|
) = Button(context, null, 0, style).apply {
|
||||||
setText(text)
|
setText(text)
|
||||||
|
textColor?.let{
|
||||||
|
setTextColor(it)
|
||||||
|
}
|
||||||
contentDescription = resources.getString(contentDescriptionRes)
|
contentDescription = resources.getString(contentDescriptionRes)
|
||||||
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, dp60, 1f)
|
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, dp60, 1f)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
@ -149,22 +180,18 @@ class SessionDialogBuilder(val context: Context) {
|
|||||||
|
|
||||||
fun Context.showSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
|
fun Context.showSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
|
||||||
SessionDialogBuilder(this).apply { build() }.show()
|
SessionDialogBuilder(this).apply { build() }.show()
|
||||||
fun Context.showOpenUrlDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
|
|
||||||
SessionDialogBuilder(this).apply {
|
|
||||||
title(R.string.urlOpen)
|
|
||||||
text(R.string.urlOpenBrowser)
|
|
||||||
build()
|
|
||||||
}.show()
|
|
||||||
|
|
||||||
fun Context.showOpenUrlDialog(url: String): AlertDialog =
|
public fun Context.copyURLToClipboard(url: String) {
|
||||||
showOpenUrlDialog {
|
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
okButton { openUrl(url) }
|
val clip = ClipData.newPlainText(url, url)
|
||||||
cancelButton()
|
clipboard.setPrimaryClip(clip)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Method to actually open a given URL via an Intent that will use the default browser
|
||||||
fun Context.openUrl(url: String) = Intent(Intent.ACTION_VIEW, Uri.parse(url)).let(::startActivity)
|
fun Context.openUrl(url: String) = Intent(Intent.ACTION_VIEW, Uri.parse(url)).let(::startActivity)
|
||||||
|
|
||||||
fun Fragment.showSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
|
fun Fragment.showSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
|
||||||
SessionDialogBuilder(requireContext()).apply { build() }.show()
|
SessionDialogBuilder(requireContext()).apply { build() }.show()
|
||||||
|
|
||||||
fun Fragment.createSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
|
fun Fragment.createSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
|
||||||
SessionDialogBuilder(requireContext()).apply { build() }.create()
|
SessionDialogBuilder(requireContext()).apply { build() }.create()
|
||||||
|
@ -29,12 +29,14 @@ import android.provider.OpenableColumns;
|
|||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.widget.Toolbar;
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import network.loki.messenger.R;
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
import org.session.libsession.utilities.DistributionTypes;
|
import org.session.libsession.utilities.DistributionTypes;
|
||||||
import org.session.libsession.utilities.ViewUtil;
|
import org.session.libsession.utilities.ViewUtil;
|
||||||
@ -49,12 +51,6 @@ import org.thoughtcrime.securesms.mms.PartAuthority;
|
|||||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||||
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An activity to quickly share content with contacts
|
* An activity to quickly share content with contacts
|
||||||
*
|
*
|
||||||
@ -69,7 +65,6 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
|
|||||||
public static final String EXTRA_ADDRESS_MARSHALLED = "address_marshalled";
|
public static final String EXTRA_ADDRESS_MARSHALLED = "address_marshalled";
|
||||||
public static final String EXTRA_DISTRIBUTION_TYPE = "distribution_type";
|
public static final String EXTRA_DISTRIBUTION_TYPE = "distribution_type";
|
||||||
|
|
||||||
|
|
||||||
private ContactSelectionListFragment contactsFragment;
|
private ContactSelectionListFragment contactsFragment;
|
||||||
private SearchToolbar searchToolbar;
|
private SearchToolbar searchToolbar;
|
||||||
private ImageView searchAction;
|
private ImageView searchAction;
|
||||||
@ -132,7 +127,7 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initializeToolbar() {
|
private void initializeToolbar() {
|
||||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
Toolbar toolbar = findViewById(R.id.search_toolbar);
|
||||||
setSupportActionBar(toolbar);
|
setSupportActionBar(toolbar);
|
||||||
ActionBar actionBar = getSupportActionBar();
|
ActionBar actionBar = getSupportActionBar();
|
||||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||||
|
@ -37,7 +37,7 @@ public class ShortcutLauncherActivity extends AppCompatActivity {
|
|||||||
String serializedAddress = getIntent().getStringExtra(KEY_SERIALIZED_ADDRESS);
|
String serializedAddress = getIntent().getStringExtra(KEY_SERIALIZED_ADDRESS);
|
||||||
|
|
||||||
if (serializedAddress == null) {
|
if (serializedAddress == null) {
|
||||||
Toast.makeText(this, R.string.ShortcutLauncherActivity_invalid_shortcut, Toast.LENGTH_SHORT).show();
|
Toast.makeText(this, R.string.invalidShortcut, Toast.LENGTH_SHORT).show();
|
||||||
startActivity(new Intent(this, HomeActivity.class));
|
startActivity(new Intent(this, HomeActivity.class));
|
||||||
finish();
|
finish();
|
||||||
return;
|
return;
|
||||||
|
@ -8,10 +8,9 @@ import android.hardware.SensorManager;
|
|||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
import android.os.PowerManager;
|
|
||||||
import android.os.PowerManager.WakeLock;
|
import android.os.PowerManager.WakeLock;
|
||||||
|
import android.os.PowerManager;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.OptIn;
|
import androidx.annotation.OptIn;
|
||||||
@ -23,7 +22,8 @@ import androidx.media3.common.PlaybackParameters;
|
|||||||
import androidx.media3.common.Player;
|
import androidx.media3.common.Player;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.media3.exoplayer.ExoPlayer;
|
import androidx.media3.exoplayer.ExoPlayer;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.session.libsession.utilities.ServiceUtil;
|
import org.session.libsession.utilities.ServiceUtil;
|
||||||
import org.session.libsession.utilities.Util;
|
import org.session.libsession.utilities.Util;
|
||||||
@ -32,9 +32,6 @@ import org.session.libsignal.utilities.guava.Optional;
|
|||||||
import org.thoughtcrime.securesms.attachments.AttachmentServer;
|
import org.thoughtcrime.securesms.attachments.AttachmentServer;
|
||||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
|
|
||||||
public class AudioSlidePlayer implements SensorEventListener {
|
public class AudioSlidePlayer implements SensorEventListener {
|
||||||
|
|
||||||
private static final String TAG = AudioSlidePlayer.class.getSimpleName();
|
private static final String TAG = AudioSlidePlayer.class.getSimpleName();
|
||||||
@ -170,7 +167,6 @@ public class AudioSlidePlayer implements SensorEventListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlayerError(PlaybackException error) {
|
public void onPlayerError(PlaybackException error) {
|
||||||
Log.w(TAG, "MediaPlayer Error: " + error);
|
Log.w(TAG, "MediaPlayer Error: " + error);
|
||||||
@ -209,9 +205,7 @@ public class AudioSlidePlayer implements SensorEventListener {
|
|||||||
this.mediaPlayer.release();
|
this.mediaPlayer.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.audioAttachmentServer != null) {
|
if (this.audioAttachmentServer != null) { this.audioAttachmentServer.stop(); }
|
||||||
this.audioAttachmentServer.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
sensorManager.unregisterListener(AudioSlidePlayer.this);
|
sensorManager.unregisterListener(AudioSlidePlayer.this);
|
||||||
|
|
||||||
@ -220,9 +214,7 @@ public class AudioSlidePlayer implements SensorEventListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public synchronized static void stopAll() {
|
public synchronized static void stopAll() {
|
||||||
if (playing.isPresent()) {
|
if (playing.isPresent()) { playing.get().stop(); }
|
||||||
playing.get().stop();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized boolean isReady() {
|
public synchronized boolean isReady() {
|
||||||
@ -364,9 +356,8 @@ public class AudioSlidePlayer implements SensorEventListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
public void onAccuracyChanged(Sensor sensor, int accuracy) { /* Do nothing */ }
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface Listener {
|
public interface Listener {
|
||||||
void onPlayerStart(@NonNull AudioSlidePlayer player);
|
void onPlayerStart(@NonNull AudioSlidePlayer player);
|
||||||
|
@ -32,7 +32,7 @@ class AvatarSelection(
|
|||||||
private val bgColor by lazy { activity.getColorFromAttr(android.R.attr.colorPrimary) }
|
private val bgColor by lazy { activity.getColorFromAttr(android.R.attr.colorPrimary) }
|
||||||
private val txtColor by lazy { activity.getColorFromAttr(android.R.attr.textColorPrimary) }
|
private val txtColor by lazy { activity.getColorFromAttr(android.R.attr.textColorPrimary) }
|
||||||
private val imageScrim by lazy { ContextCompat.getColor(activity, R.color.avatar_background) }
|
private val imageScrim by lazy { ContextCompat.getColor(activity, R.color.avatar_background) }
|
||||||
private val activityTitle by lazy { activity.getString(R.string.CropImageActivity_profile_avatar) }
|
private val activityTitle by lazy { activity.getString(R.string.image) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns result on [.REQUEST_CODE_CROP_IMAGE]
|
* Returns result on [.REQUEST_CODE_CROP_IMAGE]
|
||||||
@ -120,7 +120,7 @@ class AvatarSelection(
|
|||||||
|
|
||||||
val chooserIntent = Intent.createChooser(
|
val chooserIntent = Intent.createChooser(
|
||||||
galleryIntent,
|
galleryIntent,
|
||||||
context.getString(R.string.CreateProfileActivity_profile_photo)
|
context.getString(R.string.image)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!extraIntents.isEmpty()) {
|
if (!extraIntents.isEmpty()) {
|
||||||
|
@ -13,11 +13,13 @@ import android.view.MenuItem
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewOutlineProvider
|
import android.view.ViewOutlineProvider
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
|
import com.squareup.phrase.Phrase
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@ -27,6 +29,7 @@ import network.loki.messenger.R
|
|||||||
import network.loki.messenger.databinding.ActivityWebrtcBinding
|
import network.loki.messenger.databinding.ActivityWebrtcBinding
|
||||||
import org.apache.commons.lang3.time.DurationFormatUtils
|
import org.apache.commons.lang3.time.DurationFormatUtils
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.truncateIdForDisplay
|
import org.session.libsession.utilities.truncateIdForDisplay
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
@ -202,6 +205,10 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Substitute "Session" into the "{app_name} Call" text
|
||||||
|
val sessionCallTV = findViewById<TextView>(R.id.sessionCallText)
|
||||||
|
sessionCallTV?.text = Phrase.from(this, R.string.callsSessionCall).put(APP_NAME_KEY, getString(R.string.app_name)).format()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,299 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.components;
|
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.animation.Animator;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.drawable.BitmapDrawable;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.util.Pair;
|
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewAnimationUtils;
|
|
||||||
import android.view.ViewTreeObserver;
|
|
||||||
import android.view.animation.Animation;
|
|
||||||
import android.view.animation.AnimationSet;
|
|
||||||
import android.view.animation.OvershootInterpolator;
|
|
||||||
import android.view.animation.ScaleAnimation;
|
|
||||||
import android.view.animation.TranslateAnimation;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.PopupWindow;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.loader.app.LoaderManager;
|
|
||||||
|
|
||||||
import org.session.libsession.utilities.ViewUtil;
|
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
public class AttachmentTypeSelector extends PopupWindow {
|
|
||||||
|
|
||||||
public static final int ADD_GALLERY = 1;
|
|
||||||
public static final int ADD_DOCUMENT = 2;
|
|
||||||
public static final int ADD_SOUND = 3;
|
|
||||||
public static final int ADD_CONTACT_INFO = 4;
|
|
||||||
public static final int TAKE_PHOTO = 5;
|
|
||||||
public static final int ADD_LOCATION = 6;
|
|
||||||
public static final int ADD_GIF = 7;
|
|
||||||
|
|
||||||
private static final int ANIMATION_DURATION = 300;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
private static final String TAG = AttachmentTypeSelector.class.getSimpleName();
|
|
||||||
|
|
||||||
private final @NonNull Context context;
|
|
||||||
public int keyboardHeight;
|
|
||||||
private final @NonNull LoaderManager loaderManager;
|
|
||||||
private final @NonNull RecentPhotoViewRail recentRail;
|
|
||||||
private final @NonNull ImageView imageButton;
|
|
||||||
private final @NonNull ImageView audioButton;
|
|
||||||
private final @NonNull ImageView documentButton;
|
|
||||||
private final @NonNull ImageView contactButton;
|
|
||||||
private final @NonNull ImageView cameraButton;
|
|
||||||
private final @NonNull ImageView locationButton;
|
|
||||||
private final @NonNull ImageView gifButton;
|
|
||||||
private final @NonNull ImageView closeButton;
|
|
||||||
|
|
||||||
private @Nullable View currentAnchor;
|
|
||||||
private @Nullable AttachmentClickedListener listener;
|
|
||||||
|
|
||||||
public AttachmentTypeSelector(@NonNull Context context, @NonNull LoaderManager loaderManager, @Nullable AttachmentClickedListener listener, int keyboardHeight) {
|
|
||||||
super(context);
|
|
||||||
|
|
||||||
this.context = context;
|
|
||||||
this.keyboardHeight = keyboardHeight;
|
|
||||||
|
|
||||||
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
|
||||||
LinearLayout layout = (LinearLayout) inflater.inflate(R.layout.attachment_type_selector, null, true);
|
|
||||||
|
|
||||||
this.listener = listener;
|
|
||||||
this.loaderManager = loaderManager;
|
|
||||||
this.recentRail = ViewUtil.findById(layout, R.id.recent_photos);
|
|
||||||
this.imageButton = ViewUtil.findById(layout, R.id.gallery_button);
|
|
||||||
this.audioButton = ViewUtil.findById(layout, R.id.audio_button);
|
|
||||||
this.documentButton = ViewUtil.findById(layout, R.id.document_button);
|
|
||||||
this.contactButton = ViewUtil.findById(layout, R.id.contact_button);
|
|
||||||
this.cameraButton = ViewUtil.findById(layout, R.id.camera_button);
|
|
||||||
this.locationButton = ViewUtil.findById(layout, R.id.location_button);
|
|
||||||
this.gifButton = ViewUtil.findById(layout, R.id.giphy_button);
|
|
||||||
this.closeButton = ViewUtil.findById(layout, R.id.close_button);
|
|
||||||
|
|
||||||
this.imageButton.setOnClickListener(new PropagatingClickListener(ADD_GALLERY));
|
|
||||||
this.audioButton.setOnClickListener(new PropagatingClickListener(ADD_SOUND));
|
|
||||||
this.documentButton.setOnClickListener(new PropagatingClickListener(ADD_DOCUMENT));
|
|
||||||
this.contactButton.setOnClickListener(new PropagatingClickListener(ADD_CONTACT_INFO));
|
|
||||||
this.cameraButton.setOnClickListener(new PropagatingClickListener(TAKE_PHOTO));
|
|
||||||
this.locationButton.setOnClickListener(new PropagatingClickListener(ADD_LOCATION));
|
|
||||||
this.gifButton.setOnClickListener(new PropagatingClickListener(ADD_GIF));
|
|
||||||
this.closeButton.setOnClickListener(new CloseClickListener());
|
|
||||||
this.recentRail.setListener(new RecentPhotoSelectedListener());
|
|
||||||
|
|
||||||
setContentView(layout);
|
|
||||||
setWidth(LinearLayout.LayoutParams.MATCH_PARENT);
|
|
||||||
setHeight(LinearLayout.LayoutParams.WRAP_CONTENT);
|
|
||||||
setBackgroundDrawable(new BitmapDrawable());
|
|
||||||
setAnimationStyle(0);
|
|
||||||
setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
|
|
||||||
setFocusable(true);
|
|
||||||
setTouchable(true);
|
|
||||||
|
|
||||||
updateHeight();
|
|
||||||
|
|
||||||
loaderManager.initLoader(1, null, recentRail);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void show(@NonNull Activity activity, final @NonNull View anchor) {
|
|
||||||
updateHeight();
|
|
||||||
|
|
||||||
if (Permissions.hasAll(activity, Manifest.permission.READ_EXTERNAL_STORAGE)) {
|
|
||||||
recentRail.setVisibility(View.VISIBLE);
|
|
||||||
loaderManager.restartLoader(1, null, recentRail);
|
|
||||||
} else {
|
|
||||||
recentRail.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentAnchor = anchor;
|
|
||||||
|
|
||||||
showAtLocation(anchor, Gravity.BOTTOM, 0, 0);
|
|
||||||
|
|
||||||
getContentView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
|
||||||
@Override
|
|
||||||
public void onGlobalLayout() {
|
|
||||||
getContentView().getViewTreeObserver().removeGlobalOnLayoutListener(this);
|
|
||||||
|
|
||||||
animateWindowInCircular(anchor, getContentView());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
animateButtonIn(imageButton, ANIMATION_DURATION / 2);
|
|
||||||
animateButtonIn(cameraButton, ANIMATION_DURATION / 2);
|
|
||||||
|
|
||||||
animateButtonIn(audioButton, ANIMATION_DURATION / 3);
|
|
||||||
animateButtonIn(locationButton, ANIMATION_DURATION / 3);
|
|
||||||
animateButtonIn(documentButton, ANIMATION_DURATION / 4);
|
|
||||||
animateButtonIn(gifButton, ANIMATION_DURATION / 4);
|
|
||||||
animateButtonIn(contactButton, 0);
|
|
||||||
animateButtonIn(closeButton, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateHeight() {
|
|
||||||
int thresholdInDP = 120;
|
|
||||||
float scale = context.getResources().getDisplayMetrics().density;
|
|
||||||
int thresholdInPX = (int)(thresholdInDP * scale);
|
|
||||||
View contentView = ViewUtil.findById(getContentView(), R.id.contentView);
|
|
||||||
LinearLayout.LayoutParams contentViewLayoutParams = (LinearLayout.LayoutParams)contentView.getLayoutParams();
|
|
||||||
contentViewLayoutParams.height = keyboardHeight > thresholdInPX ? keyboardHeight : LinearLayout.LayoutParams.WRAP_CONTENT;
|
|
||||||
contentView.setLayoutParams(contentViewLayoutParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void dismiss() {
|
|
||||||
animateWindowOutCircular(currentAnchor, getContentView());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setListener(@Nullable AttachmentClickedListener listener) {
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void animateButtonIn(View button, int delay) {
|
|
||||||
AnimationSet animation = new AnimationSet(true);
|
|
||||||
Animation scale = new ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f,
|
|
||||||
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.0f);
|
|
||||||
|
|
||||||
animation.addAnimation(scale);
|
|
||||||
animation.setInterpolator(new OvershootInterpolator(1));
|
|
||||||
animation.setDuration(ANIMATION_DURATION);
|
|
||||||
animation.setStartOffset(delay);
|
|
||||||
button.startAnimation(animation);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void animateWindowInCircular(@Nullable View anchor, @NonNull View contentView) {
|
|
||||||
Pair<Integer, Integer> coordinates = getClickOrigin(anchor, contentView);
|
|
||||||
Animator animator = ViewAnimationUtils.createCircularReveal(contentView,
|
|
||||||
coordinates.first,
|
|
||||||
coordinates.second,
|
|
||||||
0,
|
|
||||||
Math.max(contentView.getWidth(), contentView.getHeight()));
|
|
||||||
animator.setDuration(ANIMATION_DURATION);
|
|
||||||
animator.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void animateWindowInTranslate(@NonNull View contentView) {
|
|
||||||
Animation animation = new TranslateAnimation(0, 0, contentView.getHeight(), 0);
|
|
||||||
animation.setDuration(ANIMATION_DURATION);
|
|
||||||
|
|
||||||
getContentView().startAnimation(animation);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void animateWindowOutCircular(@Nullable View anchor, @NonNull View contentView) {
|
|
||||||
Pair<Integer, Integer> coordinates = getClickOrigin(anchor, contentView);
|
|
||||||
Animator animator = ViewAnimationUtils.createCircularReveal(getContentView(),
|
|
||||||
coordinates.first,
|
|
||||||
coordinates.second,
|
|
||||||
Math.max(getContentView().getWidth(), getContentView().getHeight()),
|
|
||||||
0);
|
|
||||||
|
|
||||||
animator.setDuration(ANIMATION_DURATION);
|
|
||||||
animator.addListener(new Animator.AnimatorListener() {
|
|
||||||
@Override
|
|
||||||
public void onAnimationStart(Animator animation) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationEnd(Animator animation) {
|
|
||||||
AttachmentTypeSelector.super.dismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationCancel(Animator animation) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationRepeat(Animator animation) {
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
animator.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void animateWindowOutTranslate(@NonNull View contentView) {
|
|
||||||
Animation animation = new TranslateAnimation(0, 0, 0, contentView.getTop() + contentView.getHeight());
|
|
||||||
animation.setDuration(ANIMATION_DURATION);
|
|
||||||
animation.setAnimationListener(new Animation.AnimationListener() {
|
|
||||||
@Override
|
|
||||||
public void onAnimationStart(Animation animation) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationEnd(Animation animation) {
|
|
||||||
AttachmentTypeSelector.super.dismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationRepeat(Animation animation) {
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
getContentView().startAnimation(animation);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Pair<Integer, Integer> getClickOrigin(@Nullable View anchor, @NonNull View contentView) {
|
|
||||||
if (anchor == null) return new Pair<>(0, 0);
|
|
||||||
|
|
||||||
final int[] anchorCoordinates = new int[2];
|
|
||||||
anchor.getLocationOnScreen(anchorCoordinates);
|
|
||||||
anchorCoordinates[0] += anchor.getWidth() / 2;
|
|
||||||
anchorCoordinates[1] += anchor.getHeight() / 2;
|
|
||||||
|
|
||||||
final int[] contentCoordinates = new int[2];
|
|
||||||
contentView.getLocationOnScreen(contentCoordinates);
|
|
||||||
|
|
||||||
int x = anchorCoordinates[0] - contentCoordinates[0];
|
|
||||||
int y = anchorCoordinates[1] - contentCoordinates[1];
|
|
||||||
|
|
||||||
return new Pair<>(x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class RecentPhotoSelectedListener implements RecentPhotoViewRail.OnItemClickedListener {
|
|
||||||
@Override
|
|
||||||
public void onItemClicked(Uri uri, String mimeType, String bucketId, long dateTaken, int width, int height, long size) {
|
|
||||||
animateWindowOutTranslate(getContentView());
|
|
||||||
|
|
||||||
if (listener != null) listener.onQuickAttachment(uri, mimeType, bucketId, dateTaken, width, height, size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class PropagatingClickListener implements View.OnClickListener {
|
|
||||||
|
|
||||||
private final int type;
|
|
||||||
|
|
||||||
private PropagatingClickListener(int type) {
|
|
||||||
this.type = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
animateWindowOutTranslate(getContentView());
|
|
||||||
|
|
||||||
if (listener != null) listener.onClick(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private class CloseClickListener implements View.OnClickListener {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
dismiss();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface AttachmentClickedListener {
|
|
||||||
void onClick(int type);
|
|
||||||
void onQuickAttachment(Uri uri, String mimeType, String bucketId, long dateTaken, int width, int height, long size);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,259 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.components;
|
|
||||||
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.TypedArray;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.preference.DialogPreference;
|
|
||||||
import androidx.preference.PreferenceDialogFragmentCompat;
|
|
||||||
import android.text.Editable;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.text.TextWatcher;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import org.session.libsignal.utilities.Log;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.AdapterView;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.Spinner;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
import org.thoughtcrime.securesms.components.CustomDefaultPreference.CustomDefaultPreferenceDialogFragmentCompat.CustomPreferenceValidator;
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
|
|
||||||
|
|
||||||
public class CustomDefaultPreference extends DialogPreference {
|
|
||||||
|
|
||||||
private static final String TAG = CustomDefaultPreference.class.getSimpleName();
|
|
||||||
|
|
||||||
private final int inputType;
|
|
||||||
private final String customPreference;
|
|
||||||
private final String customToggle;
|
|
||||||
|
|
||||||
private CustomPreferenceValidator validator;
|
|
||||||
private String defaultValue;
|
|
||||||
|
|
||||||
public CustomDefaultPreference(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
|
|
||||||
int[] attributeNames = new int[]{android.R.attr.inputType, R.attr.custom_pref_toggle};
|
|
||||||
TypedArray attributes = context.obtainStyledAttributes(attrs, attributeNames);
|
|
||||||
|
|
||||||
this.inputType = attributes.getInt(0, 0);
|
|
||||||
this.customPreference = getKey();
|
|
||||||
this.customToggle = attributes.getString(1);
|
|
||||||
this.validator = new CustomDefaultPreferenceDialogFragmentCompat.NullValidator();
|
|
||||||
|
|
||||||
attributes.recycle();
|
|
||||||
|
|
||||||
setPersistent(false);
|
|
||||||
setDialogLayoutResource(R.layout.custom_default_preference_dialog);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CustomDefaultPreference setValidator(CustomPreferenceValidator validator) {
|
|
||||||
this.validator = validator;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CustomDefaultPreference setDefaultValue(String defaultValue) {
|
|
||||||
this.defaultValue = defaultValue;
|
|
||||||
this.setSummary(getSummary());
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getSummary() {
|
|
||||||
if (isCustom()) {
|
|
||||||
return getContext().getString(R.string.CustomDefaultPreference_using_custom,
|
|
||||||
getPrettyPrintValue(getCustomValue()));
|
|
||||||
} else {
|
|
||||||
return getContext().getString(R.string.CustomDefaultPreference_using_default,
|
|
||||||
getPrettyPrintValue(getDefaultValue()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getPrettyPrintValue(String value) {
|
|
||||||
if (TextUtils.isEmpty(value)) return getContext().getString(R.string.CustomDefaultPreference_none);
|
|
||||||
else return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isCustom() {
|
|
||||||
return TextSecurePreferences.getBooleanPreference(getContext(), customToggle, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setCustom(boolean custom) {
|
|
||||||
TextSecurePreferences.setBooleanPreference(getContext(), customToggle, custom);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getCustomValue() {
|
|
||||||
return TextSecurePreferences.getStringPreference(getContext(), customPreference, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setCustomValue(String value) {
|
|
||||||
TextSecurePreferences.setStringPreference(getContext(), customPreference, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getDefaultValue() {
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static class CustomDefaultPreferenceDialogFragmentCompat extends PreferenceDialogFragmentCompat {
|
|
||||||
|
|
||||||
private static final String INPUT_TYPE = "input_type";
|
|
||||||
|
|
||||||
private Spinner spinner;
|
|
||||||
private EditText customText;
|
|
||||||
private TextView defaultLabel;
|
|
||||||
|
|
||||||
public static CustomDefaultPreferenceDialogFragmentCompat newInstance(String key) {
|
|
||||||
CustomDefaultPreferenceDialogFragmentCompat fragment = new CustomDefaultPreferenceDialogFragmentCompat();
|
|
||||||
Bundle b = new Bundle(1);
|
|
||||||
b.putString(PreferenceDialogFragmentCompat.ARG_KEY, key);
|
|
||||||
fragment.setArguments(b);
|
|
||||||
return fragment;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onBindDialogView(@NonNull View view) {
|
|
||||||
Log.i(TAG, "onBindDialogView");
|
|
||||||
super.onBindDialogView(view);
|
|
||||||
|
|
||||||
CustomDefaultPreference preference = (CustomDefaultPreference)getPreference();
|
|
||||||
|
|
||||||
this.spinner = (Spinner) view.findViewById(R.id.default_or_custom);
|
|
||||||
this.defaultLabel = (TextView) view.findViewById(R.id.default_label);
|
|
||||||
this.customText = (EditText) view.findViewById(R.id.custom_edit);
|
|
||||||
|
|
||||||
this.customText.setInputType(preference.inputType);
|
|
||||||
this.customText.addTextChangedListener(new TextValidator());
|
|
||||||
this.customText.setText(preference.getCustomValue());
|
|
||||||
this.spinner.setOnItemSelectedListener(new SelectionLister());
|
|
||||||
this.defaultLabel.setText(preference.getPrettyPrintValue(preference.defaultValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NonNull Dialog onCreateDialog(Bundle instanceState) {
|
|
||||||
Dialog dialog = super.onCreateDialog(instanceState);
|
|
||||||
|
|
||||||
CustomDefaultPreference preference = (CustomDefaultPreference)getPreference();
|
|
||||||
|
|
||||||
if (preference.isCustom()) spinner.setSelection(1, true);
|
|
||||||
else spinner.setSelection(0, true);
|
|
||||||
|
|
||||||
return dialog;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDialogClosed(boolean positiveResult) {
|
|
||||||
CustomDefaultPreference preference = (CustomDefaultPreference)getPreference();
|
|
||||||
|
|
||||||
if (positiveResult) {
|
|
||||||
if (spinner != null) preference.setCustom(spinner.getSelectedItemPosition() == 1);
|
|
||||||
if (customText != null) preference.setCustomValue(customText.getText().toString());
|
|
||||||
|
|
||||||
preference.setSummary(preference.getSummary());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CustomPreferenceValidator {
|
|
||||||
public boolean isValid(String value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class NullValidator implements CustomPreferenceValidator {
|
|
||||||
@Override
|
|
||||||
public boolean isValid(String value) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TextValidator implements TextWatcher {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterTextChanged(Editable s) {
|
|
||||||
CustomDefaultPreference preference = (CustomDefaultPreference)getPreference();
|
|
||||||
|
|
||||||
if (spinner.getSelectedItemPosition() == 1) {
|
|
||||||
Button positiveButton = ((AlertDialog)getDialog()).getButton(AlertDialog.BUTTON_POSITIVE);
|
|
||||||
positiveButton.setEnabled(preference.validator.isValid(s.toString()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class UriValidator implements CustomPreferenceValidator {
|
|
||||||
@Override
|
|
||||||
public boolean isValid(String value) {
|
|
||||||
if (TextUtils.isEmpty(value)) return true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
new URI(value);
|
|
||||||
return true;
|
|
||||||
} catch (URISyntaxException mue) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class HostnameValidator implements CustomPreferenceValidator {
|
|
||||||
@Override
|
|
||||||
public boolean isValid(String value) {
|
|
||||||
if (TextUtils.isEmpty(value)) return true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
URI uri = new URI(null, value, null, null);
|
|
||||||
return true;
|
|
||||||
} catch (URISyntaxException mue) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class PortValidator implements CustomPreferenceValidator {
|
|
||||||
@Override
|
|
||||||
public boolean isValid(String value) {
|
|
||||||
try {
|
|
||||||
Integer.parseInt(value);
|
|
||||||
return true;
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SelectionLister implements AdapterView.OnItemSelectedListener {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
|
||||||
CustomDefaultPreference preference = (CustomDefaultPreference)getPreference();
|
|
||||||
Button positiveButton = ((AlertDialog)getDialog()).getButton(AlertDialog.BUTTON_POSITIVE);
|
|
||||||
|
|
||||||
defaultLabel.setVisibility(position == 0 ? View.VISIBLE : View.GONE);
|
|
||||||
customText.setVisibility(position == 0 ? View.GONE : View.VISIBLE);
|
|
||||||
positiveButton.setEnabled(position == 0 || preference.validator.isValid(customText.getText().toString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNothingSelected(AdapterView<?> parent) {
|
|
||||||
defaultLabel.setVisibility(View.VISIBLE);
|
|
||||||
customText.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -105,7 +105,7 @@ public class DocumentView extends FrameLayout {
|
|||||||
|
|
||||||
this.documentSlide = documentSlide;
|
this.documentSlide = documentSlide;
|
||||||
|
|
||||||
this.fileName.setText(documentSlide.getFileName().or(getContext().getString(R.string.DocumentView_unknown_file)));
|
this.fileName.setText(documentSlide.getFileName().or(getContext().getString(R.string.attachmentsErrorNotSupported)));
|
||||||
this.fileSize.setText(Util.getPrettyFileSize(documentSlide.getFileSize()));
|
this.fileSize.setText(Util.getPrettyFileSize(documentSlide.getFileSize()));
|
||||||
this.document.setText(getFileType(documentSlide.getFileName()));
|
this.document.setText(getFileType(documentSlide.getFileName()));
|
||||||
this.setOnClickListener(new OpenClickedListener(documentSlide));
|
this.setOnClickListener(new OpenClickedListener(documentSlide));
|
||||||
|
@ -54,7 +54,7 @@ public class FromTextView extends EmojiTextView {
|
|||||||
|
|
||||||
|
|
||||||
if (recipient.isLocalNumber()) {
|
if (recipient.isLocalNumber()) {
|
||||||
builder.append(getContext().getString(R.string.note_to_self));
|
builder.append(getContext().getString(R.string.noteToSelf));
|
||||||
} else if (recipient.getName() == null && !TextUtils.isEmpty(recipient.getProfileName())) {
|
} else if (recipient.getName() == null && !TextUtils.isEmpty(recipient.getProfileName())) {
|
||||||
SpannableString profileName = new SpannableString(" (~" + recipient.getProfileName() + ") ");
|
SpannableString profileName = new SpannableString(" (~" + recipient.getProfileName() + ") ");
|
||||||
profileName.setSpan(new CenterAlignedRelativeSizeSpan(0.75f), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
profileName.setSpan(new CenterAlignedRelativeSizeSpan(0.75f), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
@ -44,10 +44,9 @@ public class SearchToolbar extends LinearLayout {
|
|||||||
inflate(getContext(), R.layout.search_toolbar, this);
|
inflate(getContext(), R.layout.search_toolbar, this);
|
||||||
setOrientation(VERTICAL);
|
setOrientation(VERTICAL);
|
||||||
|
|
||||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
Toolbar toolbar = findViewById(R.id.search_toolbar);
|
||||||
|
|
||||||
toolbar.setNavigationIcon(
|
toolbar.setNavigationIcon(getContext().getResources().getDrawable(R.drawable.ic_baseline_clear_24));
|
||||||
getContext().getResources().getDrawable(R.drawable.ic_baseline_clear_24));
|
|
||||||
toolbar.inflateMenu(R.menu.conversation_list_search);
|
toolbar.inflateMenu(R.menu.conversation_list_search);
|
||||||
|
|
||||||
this.searchItem = toolbar.getMenu().findItem(R.id.action_filter_search);
|
this.searchItem = toolbar.getMenu().findItem(R.id.action_filter_search);
|
||||||
@ -56,8 +55,8 @@ public class SearchToolbar extends LinearLayout {
|
|||||||
|
|
||||||
searchView.setSubmitButtonEnabled(false);
|
searchView.setSubmitButtonEnabled(false);
|
||||||
|
|
||||||
if (searchText != null) searchText.setHint(R.string.SearchToolbar_search);
|
if (searchText != null) searchText.setHint(R.string.search);
|
||||||
else searchView.setQueryHint(getResources().getString(R.string.SearchToolbar_search));
|
else searchView.setQueryHint(getResources().getString(R.string.search));
|
||||||
|
|
||||||
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
|
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -1,50 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.components;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
|
|
||||||
import androidx.preference.CheckBoxPreference;
|
|
||||||
import androidx.preference.Preference;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
public class SwitchPreferenceCompat extends CheckBoxPreference {
|
|
||||||
|
|
||||||
private Preference.OnPreferenceClickListener listener;
|
|
||||||
|
|
||||||
public SwitchPreferenceCompat(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
||||||
super(context, attrs, defStyleAttr);
|
|
||||||
setLayoutRes();
|
|
||||||
}
|
|
||||||
|
|
||||||
public SwitchPreferenceCompat(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
|
||||||
super(context, attrs, defStyleAttr, defStyleRes);
|
|
||||||
setLayoutRes();
|
|
||||||
}
|
|
||||||
|
|
||||||
public SwitchPreferenceCompat(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
setLayoutRes();
|
|
||||||
}
|
|
||||||
|
|
||||||
public SwitchPreferenceCompat(Context context) {
|
|
||||||
super(context);
|
|
||||||
setLayoutRes();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setLayoutRes() {
|
|
||||||
setWidgetLayoutResource(R.layout.switch_compat_preference);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setOnPreferenceClickListener(Preference.OnPreferenceClickListener listener) {
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onClick() {
|
|
||||||
if (listener == null || !listener.onPreferenceClick(this)) {
|
|
||||||
super.onClick();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,59 @@
|
|||||||
|
package org.thoughtcrime.securesms.components
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import androidx.preference.CheckBoxPreference
|
||||||
|
import com.squareup.phrase.Phrase
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
|
||||||
|
import org.thoughtcrime.securesms.ui.getSubbedCharSequence
|
||||||
|
import org.thoughtcrime.securesms.ui.getSubbedString
|
||||||
|
|
||||||
|
class SwitchPreferenceCompat : CheckBoxPreference {
|
||||||
|
private var listener: OnPreferenceClickListener? = null
|
||||||
|
|
||||||
|
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context!!, attrs, defStyleAttr) {
|
||||||
|
setLayoutRes()
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context!!, attrs, defStyleAttr, defStyleRes) {
|
||||||
|
setLayoutRes()
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(context: Context?, attrs: AttributeSet?) : super(context!!, attrs) {
|
||||||
|
setLayoutRes()
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(context: Context?) : super(context!!) {
|
||||||
|
setLayoutRes()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setLayoutRes() {
|
||||||
|
widgetLayoutResource = R.layout.switch_compat_preference
|
||||||
|
|
||||||
|
if (this.hasKey()) {
|
||||||
|
val key = this.key
|
||||||
|
|
||||||
|
// Substitute app name into lockscreen preference summary
|
||||||
|
if (key.equals(LOCK_SCREEN_KEY, ignoreCase = true)) {
|
||||||
|
val c = context
|
||||||
|
val substitutedSummaryCS = c.getSubbedCharSequence(R.string.lockAppDescription, APP_NAME_KEY to c.getString(R.string.app_name))
|
||||||
|
this.summary = substitutedSummaryCS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setOnPreferenceClickListener(listener: OnPreferenceClickListener?) {
|
||||||
|
this.listener = listener
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick() {
|
||||||
|
if (listener == null || !listener!!.onPreferenceClick(this)) {
|
||||||
|
super.onClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val LOCK_SCREEN_KEY = "pref_android_screen_lock"
|
||||||
|
}
|
||||||
|
}
|
@ -1,227 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.components;
|
|
||||||
|
|
||||||
import android.animation.LayoutTransition;
|
|
||||||
import android.content.Context;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
|
||||||
import com.pnikosis.materialishprogress.ProgressWheel;
|
|
||||||
|
|
||||||
import org.greenrobot.eventbus.EventBus;
|
|
||||||
import org.greenrobot.eventbus.Subscribe;
|
|
||||||
import org.greenrobot.eventbus.ThreadMode;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
|
||||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
|
||||||
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
|
||||||
|
|
||||||
import org.session.libsession.utilities.ViewUtil;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
public class TransferControlView extends FrameLayout {
|
|
||||||
|
|
||||||
@Nullable private List<Slide> slides;
|
|
||||||
@Nullable private View current;
|
|
||||||
|
|
||||||
private final ProgressWheel progressWheel;
|
|
||||||
private final View downloadDetails;
|
|
||||||
private final TextView downloadDetailsText;
|
|
||||||
|
|
||||||
private final Map<Attachment, Float> downloadProgress;
|
|
||||||
|
|
||||||
public TransferControlView(Context context) {
|
|
||||||
this(context, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public TransferControlView(Context context, AttributeSet attrs) {
|
|
||||||
this(context, attrs, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public TransferControlView(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
||||||
super(context, attrs, defStyleAttr);
|
|
||||||
inflate(context, R.layout.transfer_controls_view, this);
|
|
||||||
|
|
||||||
setLongClickable(false);
|
|
||||||
ViewUtil.setBackground(this, ContextCompat.getDrawable(context, R.drawable.transfer_controls_background));
|
|
||||||
setVisibility(GONE);
|
|
||||||
setLayoutTransition(new LayoutTransition());
|
|
||||||
|
|
||||||
this.downloadProgress = new HashMap<>();
|
|
||||||
this.progressWheel = ViewUtil.findById(this, R.id.progress_wheel);
|
|
||||||
this.downloadDetails = ViewUtil.findById(this, R.id.download_details);
|
|
||||||
this.downloadDetailsText = ViewUtil.findById(this, R.id.download_details_text);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setFocusable(boolean focusable) {
|
|
||||||
super.setFocusable(focusable);
|
|
||||||
downloadDetails.setFocusable(focusable);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setClickable(boolean clickable) {
|
|
||||||
super.setClickable(clickable);
|
|
||||||
downloadDetails.setClickable(clickable);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onAttachedToWindow() {
|
|
||||||
super.onAttachedToWindow();
|
|
||||||
if (!EventBus.getDefault().isRegistered(this)) EventBus.getDefault().register(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDetachedFromWindow() {
|
|
||||||
super.onDetachedFromWindow();
|
|
||||||
EventBus.getDefault().unregister(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSlide(final @NonNull Slide slides) {
|
|
||||||
setSlides(Collections.singletonList(slides));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSlides(final @NonNull List<Slide> slides) {
|
|
||||||
if (slides.isEmpty()) {
|
|
||||||
throw new IllegalArgumentException("Must provide at least one slide.");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.slides = slides;
|
|
||||||
|
|
||||||
if (!isUpdateToExistingSet(slides)) {
|
|
||||||
downloadProgress.clear();
|
|
||||||
Stream.of(slides).forEach(s -> downloadProgress.put(s.asAttachment(), 0f));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (Slide slide : slides) {
|
|
||||||
if (slide.asAttachment().getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE) {
|
|
||||||
downloadProgress.put(slide.asAttachment(), 1f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (getTransferState(slides)) {
|
|
||||||
case AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED:
|
|
||||||
showProgressSpinner(calculateProgress(downloadProgress));
|
|
||||||
break;
|
|
||||||
case AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING:
|
|
||||||
case AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED:
|
|
||||||
downloadDetailsText.setText(getDownloadText(this.slides));
|
|
||||||
display(downloadDetails);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
display(null);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void showProgressSpinner() {
|
|
||||||
showProgressSpinner(calculateProgress(downloadProgress));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void showProgressSpinner(float progress) {
|
|
||||||
if (progress == 0) {
|
|
||||||
progressWheel.spin();
|
|
||||||
} else {
|
|
||||||
progressWheel.setInstantProgress(progress);
|
|
||||||
}
|
|
||||||
|
|
||||||
display(progressWheel);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDownloadClickListener(final @Nullable OnClickListener listener) {
|
|
||||||
downloadDetails.setOnClickListener(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clear() {
|
|
||||||
clearAnimation();
|
|
||||||
setVisibility(GONE);
|
|
||||||
if (current != null) {
|
|
||||||
current.clearAnimation();
|
|
||||||
current.setVisibility(GONE);
|
|
||||||
}
|
|
||||||
current = null;
|
|
||||||
slides = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setShowDownloadText(boolean showDownloadText) {
|
|
||||||
downloadDetailsText.setVisibility(showDownloadText ? VISIBLE : GONE);
|
|
||||||
forceLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isUpdateToExistingSet(@NonNull List<Slide> slides) {
|
|
||||||
if (slides.size() != downloadProgress.size()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (Slide slide : slides) {
|
|
||||||
if (!downloadProgress.containsKey(slide.asAttachment())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getTransferState(@NonNull List<Slide> slides) {
|
|
||||||
int transferState = AttachmentTransferProgress.TRANSFER_PROGRESS_DONE;
|
|
||||||
for (Slide slide : slides) {
|
|
||||||
if (slide.getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING && transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE) {
|
|
||||||
transferState = slide.getTransferState();
|
|
||||||
} else {
|
|
||||||
transferState = Math.max(transferState, slide.getTransferState());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return transferState;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getDownloadText(@NonNull List<Slide> slides) {
|
|
||||||
if (slides.size() == 1) {
|
|
||||||
return slides.get(0).getContentDescription();
|
|
||||||
} else {
|
|
||||||
int downloadCount = Stream.of(slides).reduce(0, (count, slide) -> slide.getTransferState() != AttachmentTransferProgress.TRANSFER_PROGRESS_DONE ? count + 1 : count);
|
|
||||||
return getContext().getResources().getQuantityString(R.plurals.TransferControlView_n_items, downloadCount, downloadCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void display(@Nullable final View view) {
|
|
||||||
if (current != null) {
|
|
||||||
current.setVisibility(GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (view != null) {
|
|
||||||
view.setVisibility(VISIBLE);
|
|
||||||
} else {
|
|
||||||
setVisibility(GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
current = view;
|
|
||||||
}
|
|
||||||
|
|
||||||
private float calculateProgress(@NonNull Map<Attachment, Float> downloadProgress) {
|
|
||||||
float totalProgress = 0;
|
|
||||||
for (float progress : downloadProgress.values()) {
|
|
||||||
totalProgress += progress / downloadProgress.size();
|
|
||||||
}
|
|
||||||
return totalProgress;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
|
||||||
public void onEventAsync(final PartProgressEvent event) {
|
|
||||||
if (downloadProgress.containsKey(event.attachment)) {
|
|
||||||
downloadProgress.put(event.attachment, ((float) event.progress) / event.total);
|
|
||||||
progressWheel.setInstantProgress(calculateProgress(downloadProgress));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,182 @@
|
|||||||
|
package org.thoughtcrime.securesms.components
|
||||||
|
|
||||||
|
import android.animation.LayoutTransition
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.annimon.stream.Stream
|
||||||
|
import com.pnikosis.materialishprogress.ProgressWheel
|
||||||
|
import kotlin.math.max
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import org.greenrobot.eventbus.Subscribe
|
||||||
|
import org.greenrobot.eventbus.ThreadMode
|
||||||
|
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
|
||||||
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.COUNT_KEY
|
||||||
|
import org.session.libsession.utilities.ViewUtil
|
||||||
|
import org.thoughtcrime.securesms.events.PartProgressEvent
|
||||||
|
import org.thoughtcrime.securesms.mms.Slide
|
||||||
|
import org.thoughtcrime.securesms.ui.getSubbedString
|
||||||
|
|
||||||
|
class TransferControlView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : FrameLayout(context!!, attrs, defStyleAttr) {
|
||||||
|
private var slides: List<Slide>? = null
|
||||||
|
private var current: View? = null
|
||||||
|
|
||||||
|
private val progressWheel: ProgressWheel
|
||||||
|
private val downloadDetails: View
|
||||||
|
private val downloadDetailsText: TextView
|
||||||
|
private val downloadProgress: MutableMap<Attachment, Float>
|
||||||
|
|
||||||
|
init {
|
||||||
|
inflate(context, R.layout.transfer_controls_view, this)
|
||||||
|
|
||||||
|
isLongClickable = false
|
||||||
|
ViewUtil.setBackground(this, ContextCompat.getDrawable(context!!, R.drawable.transfer_controls_background))
|
||||||
|
visibility = GONE
|
||||||
|
layoutTransition = LayoutTransition()
|
||||||
|
|
||||||
|
this.downloadProgress = HashMap()
|
||||||
|
this.progressWheel = ViewUtil.findById(this, R.id.progress_wheel)
|
||||||
|
this.downloadDetails = ViewUtil.findById(this, R.id.download_details)
|
||||||
|
this.downloadDetailsText = ViewUtil.findById(this, R.id.download_details_text)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setFocusable(focusable: Boolean) {
|
||||||
|
super.setFocusable(focusable)
|
||||||
|
downloadDetails.isFocusable = focusable
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setClickable(clickable: Boolean) {
|
||||||
|
super.setClickable(clickable)
|
||||||
|
downloadDetails.isClickable = clickable
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachedToWindow() {
|
||||||
|
super.onAttachedToWindow()
|
||||||
|
if (!EventBus.getDefault().isRegistered(this)) EventBus.getDefault().register(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromWindow() {
|
||||||
|
super.onDetachedFromWindow()
|
||||||
|
EventBus.getDefault().unregister(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setSlides(slides: List<Slide>) {
|
||||||
|
require(slides.isNotEmpty()) { "Must provide at least one slide." }
|
||||||
|
|
||||||
|
this.slides = slides
|
||||||
|
|
||||||
|
if (!isUpdateToExistingSet(slides)) {
|
||||||
|
downloadProgress.clear()
|
||||||
|
Stream.of(slides).forEach { s: Slide -> downloadProgress[s.asAttachment()] = 0f }
|
||||||
|
}
|
||||||
|
|
||||||
|
for (slide in slides) {
|
||||||
|
if (slide.asAttachment().transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE) {
|
||||||
|
downloadProgress[slide.asAttachment()] = 1f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when (getTransferState(slides)) {
|
||||||
|
AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED -> showProgressSpinner(calculateProgress(downloadProgress))
|
||||||
|
AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING, AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED -> {
|
||||||
|
downloadDetailsText.text = getDownloadText(this.slides!!)
|
||||||
|
display(downloadDetails)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> display(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
fun showProgressSpinner(progress: Float = calculateProgress(downloadProgress)) {
|
||||||
|
if (progress == 0f) {
|
||||||
|
progressWheel.spin()
|
||||||
|
} else {
|
||||||
|
progressWheel.setInstantProgress(progress)
|
||||||
|
}
|
||||||
|
display(progressWheel)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
clearAnimation()
|
||||||
|
visibility = GONE
|
||||||
|
if (current != null) {
|
||||||
|
current!!.clearAnimation()
|
||||||
|
current!!.visibility = GONE
|
||||||
|
}
|
||||||
|
current = null
|
||||||
|
slides = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isUpdateToExistingSet(slides: List<Slide>): Boolean {
|
||||||
|
if (slides.size != downloadProgress.size) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (slide in slides) {
|
||||||
|
if (!downloadProgress.containsKey(slide.asAttachment())) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTransferState(slides: List<Slide>): Int {
|
||||||
|
var transferState = AttachmentTransferProgress.TRANSFER_PROGRESS_DONE
|
||||||
|
for (slide in slides) {
|
||||||
|
transferState = if (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING && transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE) {
|
||||||
|
slide.transferState
|
||||||
|
} else {
|
||||||
|
max(transferState.toDouble(), slide.transferState.toDouble()).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return transferState
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDownloadText(slides: List<Slide>): String {
|
||||||
|
if (slides.size == 1) {
|
||||||
|
return slides[0].contentDescription
|
||||||
|
} else {
|
||||||
|
val downloadCount = Stream.of(slides).reduce(0) { count: Int, slide: Slide ->
|
||||||
|
if (slide.transferState != AttachmentTransferProgress.TRANSFER_PROGRESS_DONE) count + 1 else count
|
||||||
|
}
|
||||||
|
return context.getSubbedString(R.string.andMore, COUNT_KEY to downloadCount.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun display(view: View?) {
|
||||||
|
if (current != null) {
|
||||||
|
current!!.visibility = GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
if (view != null) {
|
||||||
|
view.visibility = VISIBLE
|
||||||
|
} else {
|
||||||
|
visibility = GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
current = view
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun calculateProgress(downloadProgress: Map<Attachment, Float>): Float {
|
||||||
|
var totalProgress = 0f
|
||||||
|
for (progress in downloadProgress.values) {
|
||||||
|
totalProgress += progress / downloadProgress.size
|
||||||
|
}
|
||||||
|
return totalProgress
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||||
|
fun onEventAsync(event: PartProgressEvent) {
|
||||||
|
if (downloadProgress.containsKey(event.attachment)) {
|
||||||
|
downloadProgress[event.attachment] = event.progress.toFloat() / event.total
|
||||||
|
progressWheel.setInstantProgress(calculateProgress(downloadProgress))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
@ -73,7 +73,7 @@ public class ContactAccessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (context.getString(R.string.note_to_self).toLowerCase().contains(constraint.toLowerCase()) &&
|
// if (context.getString(R.string.noteToSelf).toLowerCase().contains(constraint.toLowerCase()) &&
|
||||||
// !numberList.contains(TextSecurePreferences.getLocalNumber(context)))
|
// !numberList.contains(TextSecurePreferences.getLocalNumber(context)))
|
||||||
// {
|
// {
|
||||||
// numberList.add(TextSecurePreferences.getLocalNumber(context));
|
// numberList.add(TextSecurePreferences.getLocalNumber(context));
|
||||||
|
@ -36,7 +36,7 @@ class ContactSelectionListLoader(context: Context, val mode: Int, val filter: St
|
|||||||
list.addAll(getClosedGroups(contacts))
|
list.addAll(getClosedGroups(contacts))
|
||||||
}
|
}
|
||||||
if (isFlagSet(DisplayMode.FLAG_OPEN_GROUPS)) {
|
if (isFlagSet(DisplayMode.FLAG_OPEN_GROUPS)) {
|
||||||
list.addAll(getOpenGroups(contacts))
|
list.addAll(getCommunities(contacts))
|
||||||
}
|
}
|
||||||
if (isFlagSet(DisplayMode.FLAG_CONTACTS)) {
|
if (isFlagSet(DisplayMode.FLAG_CONTACTS)) {
|
||||||
list.addAll(getContacts(contacts))
|
list.addAll(getContacts(contacts))
|
||||||
@ -45,19 +45,19 @@ class ContactSelectionListLoader(context: Context, val mode: Int, val filter: St
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getContacts(contacts: List<Recipient>): List<ContactSelectionListItem> {
|
private fun getContacts(contacts: List<Recipient>): List<ContactSelectionListItem> {
|
||||||
return getItems(contacts, context.getString(R.string.fragment_contact_selection_contacts_title)) {
|
return getItems(contacts, context.getString(R.string.contactContacts)) {
|
||||||
!it.isGroupRecipient
|
!it.isGroupRecipient
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getClosedGroups(contacts: List<Recipient>): List<ContactSelectionListItem> {
|
private fun getClosedGroups(contacts: List<Recipient>): List<ContactSelectionListItem> {
|
||||||
return getItems(contacts, context.getString(R.string.fragment_contact_selection_closed_groups_title)) {
|
return getItems(contacts, context.getString(R.string.conversationsGroups)) {
|
||||||
it.address.isClosedGroup
|
it.address.isClosedGroup
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getOpenGroups(contacts: List<Recipient>): List<ContactSelectionListItem> {
|
private fun getCommunities(contacts: List<Recipient>): List<ContactSelectionListItem> {
|
||||||
return getItems(contacts, context.getString(R.string.fragment_contact_selection_open_groups_title)) {
|
return getItems(contacts, context.getString(R.string.conversationsCommunities)) {
|
||||||
it.address.isCommunity
|
it.address.isCommunity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,10 +18,10 @@ public final class ContactUtil {
|
|||||||
String contactName = ContactUtil.getDisplayName(contact);
|
String contactName = ContactUtil.getDisplayName(contact);
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(contactName)) {
|
if (!TextUtils.isEmpty(contactName)) {
|
||||||
return context.getString(R.string.MessageNotifier_contact_message, EmojiStrings.BUST_IN_SILHOUETTE, contactName);
|
return EmojiStrings.BUST_IN_SILHOUETTE + " " + contactName;
|
||||||
}
|
}
|
||||||
|
|
||||||
return SpanUtil.italic(context.getString(R.string.MessageNotifier_unknown_contact_message));
|
return SpanUtil.italic(context.getString(R.string.unknown));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static @NonNull String getDisplayName(@Nullable Contact contact) {
|
private static @NonNull String getDisplayName(@Nullable Contact contact) {
|
||||||
|
@ -159,7 +159,7 @@ public class ContactsCursorLoader extends CursorLoader {
|
|||||||
|
|
||||||
private Cursor getGroupsHeaderCursor() {
|
private Cursor getGroupsHeaderCursor() {
|
||||||
MatrixCursor groupHeader = new MatrixCursor(CONTACT_PROJECTION, 1);
|
MatrixCursor groupHeader = new MatrixCursor(CONTACT_PROJECTION, 1);
|
||||||
groupHeader.addRow(new Object[]{ getContext().getString(R.string.ContactsCursorLoader_groups),
|
groupHeader.addRow(new Object[]{ getContext().getString(R.string.conversationsGroups),
|
||||||
"",
|
"",
|
||||||
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE,
|
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE,
|
||||||
"",
|
"",
|
||||||
@ -221,16 +221,6 @@ public class ContactsCursorLoader extends CursorLoader {
|
|||||||
return groupContacts;
|
return groupContacts;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Cursor getNewNumberCursor() {
|
|
||||||
MatrixCursor newNumberCursor = new MatrixCursor(CONTACT_PROJECTION, 1);
|
|
||||||
newNumberCursor.addRow(new Object[] { getContext().getString(R.string.contact_selection_list__unknown_contact),
|
|
||||||
filter,
|
|
||||||
ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM,
|
|
||||||
"\u21e2",
|
|
||||||
NEW_TYPE });
|
|
||||||
return newNumberCursor;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isCursorListEmpty(List<Cursor> list) {
|
private static boolean isCursorListEmpty(List<Cursor> list) {
|
||||||
int sum = 0;
|
int sum = 0;
|
||||||
for (Cursor cursor : list) {
|
for (Cursor cursor : list) {
|
||||||
|
@ -35,7 +35,7 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana
|
|||||||
super.onCreate(savedInstanceState, isReady)
|
super.onCreate(savedInstanceState, isReady)
|
||||||
binding = ActivitySelectContactsBinding.inflate(layoutInflater)
|
binding = ActivitySelectContactsBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
supportActionBar!!.title = resources.getString(R.string.activity_select_contacts_title)
|
supportActionBar!!.title = resources.getString(R.string.membersInvite)
|
||||||
|
|
||||||
usersToExclude = intent.getStringArrayExtra(usersToExcludeKey)?.toSet() ?: setOf()
|
usersToExclude = intent.getStringArrayExtra(usersToExcludeKey)?.toSet() ?: setOf()
|
||||||
val emptyStateText = intent.getStringExtra(emptyStateTextKey)
|
val emptyStateText = intent.getStringExtra(emptyStateTextKey)
|
||||||
|
@ -14,7 +14,6 @@ import com.bumptech.glide.RequestManager
|
|||||||
|
|
||||||
class UserView : LinearLayout {
|
class UserView : LinearLayout {
|
||||||
private lateinit var binding: ViewUserBinding
|
private lateinit var binding: ViewUserBinding
|
||||||
var openGroupThreadID: Long = -1 // FIXME: This is a bit ugly
|
|
||||||
|
|
||||||
enum class ActionIndicator {
|
enum class ActionIndicator {
|
||||||
None,
|
None,
|
||||||
@ -47,11 +46,13 @@ class UserView : LinearLayout {
|
|||||||
// region Updating
|
// region Updating
|
||||||
fun bind(user: Recipient, glide: RequestManager, actionIndicator: ActionIndicator, isSelected: Boolean = false) {
|
fun bind(user: Recipient, glide: RequestManager, actionIndicator: ActionIndicator, isSelected: Boolean = false) {
|
||||||
val isLocalUser = user.isLocalNumber
|
val isLocalUser = user.isLocalNumber
|
||||||
|
|
||||||
fun getUserDisplayName(publicKey: String): String {
|
fun getUserDisplayName(publicKey: String): String {
|
||||||
if (isLocalUser) return context.getString(R.string.MessageRecord_you)
|
if (isLocalUser) return context.getString(R.string.you)
|
||||||
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithAccountID(publicKey)
|
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithAccountID(publicKey)
|
||||||
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
|
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
val address = user.address.serialize()
|
val address = user.address.serialize()
|
||||||
binding.profilePictureView.update(user)
|
binding.profilePictureView.update(user)
|
||||||
binding.actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24)
|
binding.actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24)
|
||||||
@ -84,8 +85,6 @@ class UserView : LinearLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unbind() {
|
fun unbind() { binding.profilePictureView.recycle() }
|
||||||
binding.profilePictureView.recycle()
|
|
||||||
}
|
|
||||||
// endregion
|
// endregion
|
||||||
}
|
}
|
||||||
|
@ -11,21 +11,26 @@ import androidx.recyclerview.widget.ListAdapter
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
|
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
|
import com.squareup.phrase.Phrase
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ViewConversationActionBarBinding
|
import network.loki.messenger.databinding.ViewConversationActionBarBinding
|
||||||
import network.loki.messenger.databinding.ViewConversationSettingBinding
|
import network.loki.messenger.databinding.ViewConversationSettingBinding
|
||||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
import network.loki.messenger.libsession_util.util.ExpiryMode.AfterRead
|
||||||
|
import org.session.libsession.LocalisedTimeUtil
|
||||||
import org.session.libsession.messaging.messages.ExpirationConfiguration
|
import org.session.libsession.messaging.messages.ExpirationConfiguration
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||||
import org.session.libsession.utilities.ExpirationUtil
|
import org.session.libsession.utilities.ExpirationUtil
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.DISAPPEARING_MESSAGES_TYPE_KEY
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.TIME_KEY
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.TIME_LARGE_KEY
|
||||||
import org.session.libsession.utilities.modifyLayoutParams
|
import org.session.libsession.utilities.modifyLayoutParams
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase
|
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||||
import org.thoughtcrime.securesms.database.LokiAPIDatabase
|
import org.thoughtcrime.securesms.database.LokiAPIDatabase
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import org.thoughtcrime.securesms.ui.getSubbedString
|
||||||
import java.util.Locale
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ConversationActionBarView @JvmOverloads constructor(
|
class ConversationActionBarView @JvmOverloads constructor(
|
||||||
@ -82,7 +87,7 @@ class ConversationActionBarView @JvmOverloads constructor(
|
|||||||
|
|
||||||
fun update(recipient: Recipient, openGroup: OpenGroup? = null, config: ExpirationConfiguration? = null) {
|
fun update(recipient: Recipient, openGroup: OpenGroup? = null, config: ExpirationConfiguration? = null) {
|
||||||
binding.profilePictureView.update(recipient)
|
binding.profilePictureView.update(recipient)
|
||||||
binding.conversationTitleView.text = recipient.takeUnless { it.isLocalNumber }?.toShortString() ?: context.getString(R.string.note_to_self)
|
binding.conversationTitleView.text = recipient.takeUnless { it.isLocalNumber }?.toShortString() ?: context.getString(R.string.noteToSelf)
|
||||||
updateSubtitle(recipient, openGroup, config)
|
updateSubtitle(recipient, openGroup, config)
|
||||||
|
|
||||||
binding.conversationTitleContainer.modifyLayoutParams<MarginLayoutParams> {
|
binding.conversationTitleContainer.modifyLayoutParams<MarginLayoutParams> {
|
||||||
@ -92,37 +97,56 @@ class ConversationActionBarView @JvmOverloads constructor(
|
|||||||
|
|
||||||
fun updateSubtitle(recipient: Recipient, openGroup: OpenGroup? = null, config: ExpirationConfiguration? = null) {
|
fun updateSubtitle(recipient: Recipient, openGroup: OpenGroup? = null, config: ExpirationConfiguration? = null) {
|
||||||
val settings = mutableListOf<ConversationSetting>()
|
val settings = mutableListOf<ConversationSetting>()
|
||||||
|
|
||||||
|
// Specify the disappearing messages subtitle if we should
|
||||||
if (config?.isEnabled == true) {
|
if (config?.isEnabled == true) {
|
||||||
val prefix = when (config.expiryMode) {
|
// Get the type of disappearing message and the abbreviated duration..
|
||||||
is ExpiryMode.AfterRead -> R.string.expiration_type_disappear_after_read
|
val dmTypeString = when (config.expiryMode) {
|
||||||
else -> R.string.expiration_type_disappear_after_send
|
is AfterRead -> context.getString(R.string.read)
|
||||||
}.let(context::getString)
|
else -> context.getString(R.string.send)
|
||||||
|
}
|
||||||
|
val durationAbbreviated = ExpirationUtil.getExpirationAbbreviatedDisplayValue(config.expiryMode.expirySeconds)
|
||||||
|
|
||||||
|
// ..then substitute into the string..
|
||||||
|
val subtitleTxt = context.getSubbedString(R.string.disappearingMessagesDisappear,
|
||||||
|
DISAPPEARING_MESSAGES_TYPE_KEY to dmTypeString,
|
||||||
|
TIME_KEY to durationAbbreviated
|
||||||
|
)
|
||||||
|
|
||||||
|
// .. and apply to the subtitle.
|
||||||
settings += ConversationSetting(
|
settings += ConversationSetting(
|
||||||
"$prefix - ${ExpirationUtil.getExpirationAbbreviatedDisplayValue(context, config.expiryMode.expirySeconds)}",
|
subtitleTxt,
|
||||||
ConversationSettingType.EXPIRATION,
|
ConversationSettingType.EXPIRATION,
|
||||||
R.drawable.ic_timer,
|
R.drawable.ic_timer,
|
||||||
resources.getString(R.string.AccessibilityId_disappearing_messages_type_and_time)
|
resources.getString(R.string.AccessibilityId_disappearingMessagesDisappear)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recipient.isMuted) {
|
if (recipient.isMuted) {
|
||||||
settings += ConversationSetting(
|
settings += ConversationSetting(
|
||||||
recipient.mutedUntil.takeUnless { it == Long.MAX_VALUE }
|
recipient.mutedUntil.takeUnless { it == Long.MAX_VALUE }
|
||||||
?.let { context.getString(R.string.ConversationActivity_muted_until_date, DateUtils.getFormattedDateTime(it, "EEE, MMM d, yyyy HH:mm", Locale.getDefault())) }
|
?.let {
|
||||||
?: context.getString(R.string.ConversationActivity_muted_forever),
|
val mutedDuration = (it - System.currentTimeMillis()).milliseconds
|
||||||
|
val durationString = LocalisedTimeUtil.getDurationWithSingleLargestTimeUnit(context, mutedDuration)
|
||||||
|
context.getSubbedString(R.string.notificationsMuteFor, TIME_LARGE_KEY to durationString)
|
||||||
|
}
|
||||||
|
?: context.getString(R.string.notificationsMuted),
|
||||||
ConversationSettingType.NOTIFICATION,
|
ConversationSettingType.NOTIFICATION,
|
||||||
R.drawable.ic_outline_notifications_off_24
|
R.drawable.ic_outline_notifications_off_24
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recipient.isGroupRecipient) {
|
if (recipient.isGroupRecipient) {
|
||||||
val title = if (recipient.isCommunityRecipient) {
|
val title = if (recipient.isCommunityRecipient) {
|
||||||
val userCount = openGroup?.let { lokiApiDb.getUserCount(it.room, it.server) } ?: 0
|
val userCount = openGroup?.let { lokiApiDb.getUserCount(it.room, it.server) } ?: 0
|
||||||
context.getString(R.string.ConversationActivity_active_member_count, userCount)
|
resources.getQuantityString(R.plurals.membersActive, userCount, userCount)
|
||||||
} else {
|
} else {
|
||||||
val userCount = groupDb.getGroupMemberAddresses(recipient.address.toGroupString(), true).size
|
val userCount = groupDb.getGroupMemberAddresses(recipient.address.toGroupString(), true).size
|
||||||
context.getString(R.string.ConversationActivity_member_count, userCount)
|
resources.getQuantityString(R.plurals.members, userCount, userCount)
|
||||||
}
|
}
|
||||||
settings += ConversationSetting(title, ConversationSettingType.MEMBER_COUNT)
|
settings += ConversationSetting(title, ConversationSettingType.MEMBER_COUNT)
|
||||||
}
|
}
|
||||||
|
|
||||||
settingsAdapter.submitList(settings)
|
settingsAdapter.submitList(settings)
|
||||||
binding.settingsTabLayout.isVisible = settings.size > 1
|
binding.settingsTabLayout.isVisible = settings.size > 1
|
||||||
}
|
}
|
||||||
|
@ -12,10 +12,14 @@ import org.session.libsession.snode.SnodeAPI
|
|||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.ExpirationUtil
|
import org.session.libsession.utilities.ExpirationUtil
|
||||||
import org.session.libsession.utilities.SSKEnvironment.MessageExpirationManagerProtocol
|
import org.session.libsession.utilities.SSKEnvironment.MessageExpirationManagerProtocol
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.DISAPPEARING_MESSAGES_TYPE_KEY
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.TIME_KEY
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.getExpirationTypeDisplayValue
|
import org.session.libsession.utilities.getExpirationTypeDisplayValue
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.showSessionDialog
|
import org.thoughtcrime.securesms.showSessionDialog
|
||||||
|
import org.thoughtcrime.securesms.ui.getSubbedCharSequence
|
||||||
|
import org.thoughtcrime.securesms.ui.getSubbedString
|
||||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
@ -43,22 +47,18 @@ class DisappearingMessages @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun showFollowSettingDialog(context: Context, message: MessageRecord) = context.showSessionDialog {
|
fun showFollowSettingDialog(context: Context, message: MessageRecord) = context.showSessionDialog {
|
||||||
title(R.string.dialog_disappearing_messages_follow_setting_title)
|
title(R.string.disappearingMessagesFollowSetting)
|
||||||
text(if (message.expiresIn == 0L) {
|
text(if (message.expiresIn == 0L) {
|
||||||
context.getString(R.string.dialog_disappearing_messages_follow_setting_off_body)
|
context.getText(R.string.disappearingMessagesFollowSettingOff)
|
||||||
} else {
|
} else {
|
||||||
context.getString(
|
context.getSubbedCharSequence(R.string.disappearingMessagesFollowSettingOn,
|
||||||
R.string.dialog_disappearing_messages_follow_setting_on_body,
|
TIME_KEY to ExpirationUtil.getExpirationDisplayValue(context, message.expiresIn.milliseconds),
|
||||||
ExpirationUtil.getExpirationDisplayValue(
|
DISAPPEARING_MESSAGES_TYPE_KEY to context.getExpirationTypeDisplayValue(message.isNotDisappearAfterRead))
|
||||||
context,
|
|
||||||
message.expiresIn.milliseconds
|
|
||||||
),
|
|
||||||
context.getExpirationTypeDisplayValue(message.isNotDisappearAfterRead)
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
dangerButton(
|
dangerButton(
|
||||||
text = if (message.expiresIn == 0L) R.string.dialog_disappearing_messages_follow_setting_confirm else R.string.dialog_disappearing_messages_follow_setting_set,
|
text = if (message.expiresIn == 0L) R.string.confirm else R.string.set,
|
||||||
contentDescription = if (message.expiresIn == 0L) R.string.AccessibilityId_confirm else R.string.AccessibilityId_set_button
|
contentDescription = if (message.expiresIn == 0L) R.string.AccessibilityId_confirm else R.string.AccessibilityId_setButton
|
||||||
) {
|
) {
|
||||||
set(message.threadId, message.recipient.address, message.expiryMode, message.recipient.isClosedGroupRecipient)
|
set(message.threadId, message.recipient.address, message.expiryMode, message.recipient.isClosedGroupRecipient)
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,7 @@ class DisappearingMessagesActivity: PassphraseRequiredActionBarActivity() {
|
|||||||
viewModel.event.collect {
|
viewModel.event.collect {
|
||||||
when (it) {
|
when (it) {
|
||||||
Event.SUCCESS -> finish()
|
Event.SUCCESS -> finish()
|
||||||
Event.FAIL -> showToast(getString(R.string.DisappearingMessagesActivity_settings_not_updated))
|
Event.FAIL -> showToast(getString(R.string.communityErrorDescription))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -72,9 +72,9 @@ class DisappearingMessagesActivity: PassphraseRequiredActionBarActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setUpToolbar() {
|
private fun setUpToolbar() {
|
||||||
setSupportActionBar(binding.toolbar)
|
setSupportActionBar(binding.searchToolbar)
|
||||||
supportActionBar?.apply {
|
supportActionBar?.apply {
|
||||||
title = getString(R.string.activity_disappearing_messages_title)
|
title = getString(R.string.disappearingMessages)
|
||||||
setDisplayHomeAsUpEnabled(true)
|
setDisplayHomeAsUpEnabled(true)
|
||||||
setHomeButtonEnabled(true)
|
setHomeButtonEnabled(true)
|
||||||
}
|
}
|
||||||
|
@ -24,8 +24,8 @@ data class State(
|
|||||||
val showDebugOptions: Boolean = false
|
val showDebugOptions: Boolean = false
|
||||||
) {
|
) {
|
||||||
val subtitle get() = when {
|
val subtitle get() = when {
|
||||||
isGroup || isNoteToSelf -> GetString(R.string.activity_disappearing_messages_subtitle_sent)
|
isGroup || isNoteToSelf -> GetString(R.string.disappearingMessagesDisappearAfterSendDescription)
|
||||||
else -> GetString(R.string.activity_disappearing_messages_subtitle)
|
else -> GetString(R.string.disappearingMessagesDescription1)
|
||||||
}
|
}
|
||||||
|
|
||||||
val typeOptionsHidden get() = isNoteToSelf || (isGroup && isNewConfigEnabled)
|
val typeOptionsHidden get() = isNoteToSelf || (isGroup && isNewConfigEnabled)
|
||||||
@ -51,25 +51,25 @@ enum class ExpiryType(
|
|||||||
) {
|
) {
|
||||||
NONE(
|
NONE(
|
||||||
{ ExpiryMode.NONE },
|
{ ExpiryMode.NONE },
|
||||||
R.string.expiration_off,
|
R.string.off,
|
||||||
contentDescription = R.string.AccessibilityId_disable_disappearing_messages,
|
contentDescription = R.string.AccessibilityId_disappearingMessagesOff,
|
||||||
),
|
),
|
||||||
LEGACY(
|
LEGACY(
|
||||||
ExpiryMode::Legacy,
|
ExpiryMode::Legacy,
|
||||||
R.string.expiration_type_disappear_legacy,
|
R.string.expiration_type_disappear_legacy,
|
||||||
contentDescription = R.string.expiration_type_disappear_legacy_description
|
contentDescription = R.string.AccessibilityId_disappearingMessagesLegacy
|
||||||
),
|
),
|
||||||
AFTER_READ(
|
AFTER_READ(
|
||||||
ExpiryMode::AfterRead,
|
ExpiryMode::AfterRead,
|
||||||
R.string.expiration_type_disappear_after_read,
|
R.string.disappearingMessagesDisappearAfterRead,
|
||||||
R.string.expiration_type_disappear_after_read_description,
|
R.string.disappearingMessagesDisappearAfterReadDescription,
|
||||||
R.string.AccessibilityId_disappear_after_read_option
|
R.string.AccessibilityId_disappearingMessagesDisappearAfterRead
|
||||||
),
|
),
|
||||||
AFTER_SEND(
|
AFTER_SEND(
|
||||||
ExpiryMode::AfterSend,
|
ExpiryMode::AfterSend,
|
||||||
R.string.expiration_type_disappear_after_send,
|
R.string.disappearingMessagesDisappearAfterSend,
|
||||||
R.string.expiration_type_disappear_after_send_description,
|
R.string.disappearingMessagesDisappearAfterSendDescription,
|
||||||
R.string.AccessibilityId_disappear_after_send_option
|
R.string.AccessibilityId_disappearingMessagesDisappearAfterSent
|
||||||
);
|
);
|
||||||
|
|
||||||
fun mode(seconds: Long) = if (seconds != 0L) createMode(seconds) else ExpiryMode.NONE
|
fun mode(seconds: Long) = if (seconds != 0L) createMode(seconds) else ExpiryMode.NONE
|
||||||
|
@ -13,8 +13,8 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
|
|
||||||
fun State.toUiState() = UiState(
|
fun State.toUiState() = UiState(
|
||||||
cards = listOfNotNull(
|
cards = listOfNotNull(
|
||||||
typeOptions()?.let { ExpiryOptionsCardData(GetString(R.string.activity_disappearing_messages_delete_type), it) },
|
typeOptions()?.let { ExpiryOptionsCardData(GetString(R.string.disappearingMessagesDeleteType), it) },
|
||||||
timeOptions()?.let { ExpiryOptionsCardData(GetString(R.string.activity_disappearing_messages_timer), it) }
|
timeOptions()?.let { ExpiryOptionsCardData(GetString(R.string.disappearingMessagesTimer), it) }
|
||||||
),
|
),
|
||||||
showGroupFooter = isGroup && isNewConfigEnabled,
|
showGroupFooter = isGroup && isNewConfigEnabled,
|
||||||
showSetButton = isSelfAdmin
|
showSetButton = isSelfAdmin
|
||||||
@ -66,11 +66,14 @@ private fun State.typeOption(
|
|||||||
)
|
)
|
||||||
|
|
||||||
private fun debugTimes(isDebug: Boolean) = if (isDebug) listOf(10.seconds, 30.seconds, 1.minutes) else emptyList()
|
private fun debugTimes(isDebug: Boolean) = if (isDebug) listOf(10.seconds, 30.seconds, 1.minutes) else emptyList()
|
||||||
|
|
||||||
private fun debugModes(isDebug: Boolean, type: ExpiryType) =
|
private fun debugModes(isDebug: Boolean, type: ExpiryType) =
|
||||||
debugTimes(isDebug).map { type.mode(it.inWholeSeconds) }
|
debugTimes(isDebug).map { type.mode(it.inWholeSeconds) }
|
||||||
|
|
||||||
private fun State.debugOptions(): List<ExpiryRadioOption> =
|
private fun State.debugOptions(): List<ExpiryRadioOption> =
|
||||||
debugModes(showDebugOptions, nextType).map { timeOption(it, subtitle = GetString("for testing purposes")) }
|
debugModes(showDebugOptions, nextType).map { timeOption(it, subtitle = GetString("for testing purposes")) }
|
||||||
|
|
||||||
|
// Standard list of available disappearing message times
|
||||||
private val afterSendTimes = listOf(12.hours, 1.days, 7.days, 14.days)
|
private val afterSendTimes = listOf(12.hours, 1.days, 7.days, 14.days)
|
||||||
|
|
||||||
private val afterReadTimes = buildList {
|
private val afterReadTimes = buildList {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.disappearingmessages.ui
|
package org.thoughtcrime.securesms.conversation.disappearingmessages.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@ -16,21 +15,20 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||||
import org.thoughtcrime.securesms.ui.Callbacks
|
import org.thoughtcrime.securesms.ui.Callbacks
|
||||||
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
|
|
||||||
import org.thoughtcrime.securesms.ui.NoOpCallbacks
|
import org.thoughtcrime.securesms.ui.NoOpCallbacks
|
||||||
import org.thoughtcrime.securesms.ui.OptionsCard
|
import org.thoughtcrime.securesms.ui.OptionsCard
|
||||||
import org.thoughtcrime.securesms.ui.RadioOption
|
import org.thoughtcrime.securesms.ui.RadioOption
|
||||||
import org.thoughtcrime.securesms.ui.theme.LocalColors
|
|
||||||
import org.thoughtcrime.securesms.ui.components.SlimOutlineButton
|
import org.thoughtcrime.securesms.ui.components.SlimOutlineButton
|
||||||
import org.thoughtcrime.securesms.ui.contentDescription
|
import org.thoughtcrime.securesms.ui.contentDescription
|
||||||
import org.thoughtcrime.securesms.ui.fadingEdges
|
import org.thoughtcrime.securesms.ui.fadingEdges
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.LocalColors
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
|
||||||
import org.thoughtcrime.securesms.ui.theme.LocalType
|
import org.thoughtcrime.securesms.ui.theme.LocalType
|
||||||
|
|
||||||
typealias ExpiryCallbacks = Callbacks<ExpiryMode>
|
typealias ExpiryCallbacks = Callbacks<ExpiryMode>
|
||||||
typealias ExpiryRadioOption = RadioOption<ExpiryMode>
|
typealias ExpiryRadioOption = RadioOption<ExpiryMode>
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -59,7 +57,9 @@ fun DisappearingMessages(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state.showGroupFooter) Text(
|
if (state.showGroupFooter) Text(
|
||||||
text = stringResource(R.string.activity_disappearing_messages_group_footer),
|
text = stringResource(R.string.disappearingMessagesDescription) +
|
||||||
|
"\n" +
|
||||||
|
stringResource(R.string.disappearingMessagesOnlyAdmins),
|
||||||
style = LocalType.current.extraSmall,
|
style = LocalType.current.extraSmall,
|
||||||
fontWeight = FontWeight(400),
|
fontWeight = FontWeight(400),
|
||||||
color = LocalColors.current.textSecondary,
|
color = LocalColors.current.textSecondary,
|
||||||
@ -72,9 +72,9 @@ fun DisappearingMessages(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state.showSetButton) SlimOutlineButton(
|
if (state.showSetButton) SlimOutlineButton(
|
||||||
stringResource(R.string.disappearing_messages_set_button_title),
|
stringResource(R.string.set),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.contentDescription(R.string.AccessibilityId_set_button)
|
.contentDescription(R.string.AccessibilityId_setButton)
|
||||||
.align(Alignment.CenterHorizontally)
|
.align(Alignment.CenterHorizontally)
|
||||||
.padding(bottom = LocalDimensions.current.spacing),
|
.padding(bottom = LocalDimensions.current.spacing),
|
||||||
onClick = callbacks::onSetClick
|
onClick = callbacks::onSetClick
|
||||||
|
@ -15,6 +15,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
@ -41,12 +42,14 @@ internal fun StartConversationScreen(
|
|||||||
accountId: String,
|
accountId: String,
|
||||||
delegate: StartConversationDelegate
|
delegate: StartConversationDelegate
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
Column(modifier = Modifier.background(
|
Column(modifier = Modifier.background(
|
||||||
LocalColors.current.backgroundSecondary,
|
LocalColors.current.backgroundSecondary,
|
||||||
shape = MaterialTheme.shapes.small
|
shape = MaterialTheme.shapes.small
|
||||||
)) {
|
)) {
|
||||||
BasicAppBar(
|
BasicAppBar(
|
||||||
title = stringResource(R.string.dialog_start_conversation_title),
|
title = stringResource(R.string.conversationsStart),
|
||||||
backgroundColor = Color.Transparent, // transparent to show the rounded shape of the container
|
backgroundColor = Color.Transparent, // transparent to show the rounded shape of the container
|
||||||
actions = { AppBarCloseIcon(onClose = delegate::onDialogClosePressed) }
|
actions = { AppBarCloseIcon(onClose = delegate::onDialogClosePressed) }
|
||||||
)
|
)
|
||||||
@ -57,30 +60,31 @@ internal fun StartConversationScreen(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier.verticalScroll(rememberScrollState())
|
modifier = Modifier.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
|
val newMessageTitleTxt:String = context.resources.getQuantityString(R.plurals.messageNew, 1, 1)
|
||||||
ItemButton(
|
ItemButton(
|
||||||
textId = R.string.messageNew,
|
text = newMessageTitleTxt,
|
||||||
icon = R.drawable.ic_message,
|
icon = R.drawable.ic_message,
|
||||||
modifier = Modifier.contentDescription(R.string.AccessibilityId_new_direct_message),
|
modifier = Modifier.contentDescription(R.string.AccessibilityId_messageNew),
|
||||||
onClick = delegate::onNewMessageSelected)
|
onClick = delegate::onNewMessageSelected)
|
||||||
Divider(startIndent = LocalDimensions.current.dividerIndent)
|
Divider(startIndent = LocalDimensions.current.dividerIndent)
|
||||||
ItemButton(
|
ItemButton(
|
||||||
textId = R.string.activity_create_group_title,
|
textId = R.string.groupCreate,
|
||||||
icon = R.drawable.ic_group,
|
icon = R.drawable.ic_group,
|
||||||
modifier = Modifier.contentDescription(R.string.AccessibilityId_create_group),
|
modifier = Modifier.contentDescription(R.string.AccessibilityId_groupCreate),
|
||||||
onClick = delegate::onCreateGroupSelected
|
onClick = delegate::onCreateGroupSelected
|
||||||
)
|
)
|
||||||
Divider(startIndent = LocalDimensions.current.dividerIndent)
|
Divider(startIndent = LocalDimensions.current.dividerIndent)
|
||||||
ItemButton(
|
ItemButton(
|
||||||
textId = R.string.dialog_join_community_title,
|
textId = R.string.communityJoin,
|
||||||
icon = R.drawable.ic_globe,
|
icon = R.drawable.ic_globe,
|
||||||
modifier = Modifier.contentDescription(R.string.AccessibilityId_join_community),
|
modifier = Modifier.contentDescription(R.string.AccessibilityId_communityJoin),
|
||||||
onClick = delegate::onJoinCommunitySelected
|
onClick = delegate::onJoinCommunitySelected
|
||||||
)
|
)
|
||||||
Divider(startIndent = LocalDimensions.current.dividerIndent)
|
Divider(startIndent = LocalDimensions.current.dividerIndent)
|
||||||
ItemButton(
|
ItemButton(
|
||||||
textId = R.string.activity_settings_invite_button_title,
|
textId = R.string.sessionInviteAFriend,
|
||||||
icon = R.drawable.ic_invite_friend,
|
icon = R.drawable.ic_invite_friend,
|
||||||
Modifier.contentDescription(R.string.AccessibilityId_invite_friend_button),
|
Modifier.contentDescription(R.string.AccessibilityId_sessionInviteAFriendButton),
|
||||||
onClick = delegate::onInviteFriend
|
onClick = delegate::onInviteFriend
|
||||||
)
|
)
|
||||||
Column(
|
Column(
|
||||||
@ -99,7 +103,7 @@ internal fun StartConversationScreen(
|
|||||||
Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing))
|
Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing))
|
||||||
QrImage(
|
QrImage(
|
||||||
string = accountId,
|
string = accountId,
|
||||||
Modifier.contentDescription(R.string.AccessibilityId_qr_code),
|
Modifier.contentDescription(R.string.AccessibilityId_qrCode),
|
||||||
icon = R.drawable.session
|
icon = R.drawable.session
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -14,10 +14,13 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import com.squareup.phrase.Phrase
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
|
||||||
import org.thoughtcrime.securesms.ui.components.AppBarCloseIcon
|
import org.thoughtcrime.securesms.ui.components.AppBarCloseIcon
|
||||||
import org.thoughtcrime.securesms.ui.components.BackAppBar
|
import org.thoughtcrime.securesms.ui.components.BackAppBar
|
||||||
import org.thoughtcrime.securesms.ui.components.SlimOutlineButton
|
import org.thoughtcrime.securesms.ui.components.SlimOutlineButton
|
||||||
@ -43,7 +46,7 @@ internal fun InviteFriend(
|
|||||||
shape = MaterialTheme.shapes.small
|
shape = MaterialTheme.shapes.small
|
||||||
)) {
|
)) {
|
||||||
BackAppBar(
|
BackAppBar(
|
||||||
title = stringResource(R.string.invite_a_friend),
|
title = stringResource(R.string.sessionInviteAFriend),
|
||||||
backgroundColor = Color.Transparent, // transparent to show the rounded shape of the container
|
backgroundColor = Color.Transparent, // transparent to show the rounded shape of the container
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
actions = { AppBarCloseIcon(onClose = onClose) }
|
actions = { AppBarCloseIcon(onClose = onClose) }
|
||||||
@ -55,7 +58,7 @@ internal fun InviteFriend(
|
|||||||
Text(
|
Text(
|
||||||
accountId,
|
accountId,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.contentDescription(R.string.AccessibilityId_account_id)
|
.contentDescription(R.string.AccessibilityId_shareAccountId)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.border()
|
.border()
|
||||||
.padding(LocalDimensions.current.spacing),
|
.padding(LocalDimensions.current.spacing),
|
||||||
@ -66,7 +69,10 @@ internal fun InviteFriend(
|
|||||||
Spacer(modifier = Modifier.height(LocalDimensions.current.xsSpacing))
|
Spacer(modifier = Modifier.height(LocalDimensions.current.xsSpacing))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.invite_your_friend_to_chat_with_you_on_session_by_sharing_your_account_id_with_them),
|
stringResource(R.string.shareAccountIdDescription).let { txt ->
|
||||||
|
val c = LocalContext.current
|
||||||
|
Phrase.from(txt).put(APP_NAME_KEY, c.getString(R.string.app_name)).format().toString()
|
||||||
|
},
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
style = LocalType.current.small,
|
style = LocalType.current.small,
|
||||||
color = LocalColors.current.textSecondary,
|
color = LocalColors.current.textSecondary,
|
||||||
|
@ -32,6 +32,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||||
@ -61,7 +62,7 @@ import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider
|
|||||||
import org.thoughtcrime.securesms.ui.theme.ThemeColors
|
import org.thoughtcrime.securesms.ui.theme.ThemeColors
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
private val TITLES = listOf(R.string.enter_account_id, R.string.qrScan)
|
private val TITLES = listOf(R.string.accountIdEnter, R.string.qrScan)
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@ -79,8 +80,12 @@ internal fun NewMessage(
|
|||||||
LocalColors.current.backgroundSecondary,
|
LocalColors.current.backgroundSecondary,
|
||||||
shape = MaterialTheme.shapes.small
|
shape = MaterialTheme.shapes.small
|
||||||
)) {
|
)) {
|
||||||
|
// `messageNew` is now a plurals string so get the singular version
|
||||||
|
val context = LocalContext.current
|
||||||
|
val newMessageTitleTxt:String = context.resources.getQuantityString(R.plurals.messageNew, 1, 1)
|
||||||
|
|
||||||
BackAppBar(
|
BackAppBar(
|
||||||
title = stringResource(R.string.messageNew),
|
title = newMessageTitleTxt,
|
||||||
backgroundColor = Color.Transparent, // transparent to show the rounded shape of the container
|
backgroundColor = Color.Transparent, // transparent to show the rounded shape of the container
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
actions = { AppBarCloseIcon(onClose = onClose) }
|
actions = { AppBarCloseIcon(onClose = onClose) }
|
||||||
@ -88,7 +93,7 @@ internal fun NewMessage(
|
|||||||
SessionTabRow(pagerState, TITLES)
|
SessionTabRow(pagerState, TITLES)
|
||||||
HorizontalPager(pagerState) {
|
HorizontalPager(pagerState) {
|
||||||
when (TITLES[it]) {
|
when (TITLES[it]) {
|
||||||
R.string.enter_account_id -> EnterAccountId(state, callbacks, onHelp)
|
R.string.accountIdEnter -> EnterAccountId(state, callbacks, onHelp)
|
||||||
R.string.qrScan -> QRScannerScreen(qrErrors, onScan = callbacks::onScanQrCode)
|
R.string.qrScan -> QRScannerScreen(qrErrors, onScan = callbacks::onScanQrCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -116,7 +121,7 @@ private fun EnterAccountId(
|
|||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
|
|
||||||
// There is a known issue with the ime padding on android versions below 30
|
// There is a known issue with the ime padding on android versions below 30
|
||||||
/// So on these older versions we need to resort to some manual padding based on the visible height
|
// So on these older versions we need to resort to some manual padding based on the visible height
|
||||||
// when the keyboard is up
|
// when the keyboard is up
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||||
val keyboardHeight by keyboardHeight()
|
val keyboardHeight by keyboardHeight()
|
||||||
@ -149,9 +154,9 @@ private fun EnterAccountId(
|
|||||||
Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsSpacing))
|
Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsSpacing))
|
||||||
|
|
||||||
BorderlessButtonWithIcon(
|
BorderlessButtonWithIcon(
|
||||||
text = stringResource(R.string.messageNewDescription),
|
text = stringResource(R.string.messageNewDescriptionMobile),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.contentDescription(R.string.AccessibilityId_help_desk_link)
|
.contentDescription(R.string.AccessibilityId_messageNewDescriptionMobile)
|
||||||
.padding(horizontal = LocalDimensions.current.mediumSpacing)
|
.padding(horizontal = LocalDimensions.current.mediumSpacing)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
style = LocalType.current.small,
|
style = LocalType.current.small,
|
||||||
|
@ -4,6 +4,8 @@ import android.app.Application
|
|||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import java.util.concurrent.TimeoutException
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
@ -19,8 +21,6 @@ import org.session.libsession.snode.SnodeAPI
|
|||||||
import org.session.libsignal.utilities.PublicKeyValidation
|
import org.session.libsignal.utilities.PublicKeyValidation
|
||||||
import org.session.libsignal.utilities.timeout
|
import org.session.libsignal.utilities.timeout
|
||||||
import org.thoughtcrime.securesms.ui.GetString
|
import org.thoughtcrime.securesms.ui.GetString
|
||||||
import java.util.concurrent.TimeoutException
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
internal class NewMessageViewModel @Inject constructor(
|
internal class NewMessageViewModel @Inject constructor(
|
||||||
@ -41,7 +41,6 @@ internal class NewMessageViewModel @Inject constructor(
|
|||||||
override fun onChange(value: String) {
|
override fun onChange(value: String) {
|
||||||
loadOnsJob?.cancel()
|
loadOnsJob?.cancel()
|
||||||
loadOnsJob = null
|
loadOnsJob = null
|
||||||
|
|
||||||
_state.update { it.copy(newMessageIdOrOns = value, isTextErrorColor = false, loading = false) }
|
_state.update { it.copy(newMessageIdOrOns = value, isTextErrorColor = false, loading = false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,7 +58,7 @@ internal class NewMessageViewModel @Inject constructor(
|
|||||||
if (PublicKeyValidation.isValid(value, isPrefixRequired = false) && PublicKeyValidation.hasValidPrefix(value)) {
|
if (PublicKeyValidation.isValid(value, isPrefixRequired = false) && PublicKeyValidation.hasValidPrefix(value)) {
|
||||||
onPublicKey(value)
|
onPublicKey(value)
|
||||||
} else {
|
} else {
|
||||||
_qrErrors.tryEmit(application.getString(R.string.this_qr_code_does_not_contain_an_account_id))
|
_qrErrors.tryEmit(application.getString(R.string.qrNotAccountId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,7 +98,7 @@ internal class NewMessageViewModel @Inject constructor(
|
|||||||
private fun Exception.toMessage() = when (this) {
|
private fun Exception.toMessage() = when (this) {
|
||||||
is SnodeAPI.Error.Generic -> application.getString(R.string.onsErrorNotRecognized)
|
is SnodeAPI.Error.Generic -> application.getString(R.string.onsErrorNotRecognized)
|
||||||
is TimeoutException -> application.getString(R.string.onsErrorUnableToSearch)
|
is TimeoutException -> application.getString(R.string.onsErrorUnableToSearch)
|
||||||
else -> application.getString(R.string.fragment_enter_public_key_error_message)
|
else -> application.getString(R.string.accountIdErrorInvalid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,4 +111,4 @@ internal data class State(
|
|||||||
val isNextButtonEnabled: Boolean get() = newMessageIdOrOns.isNotBlank()
|
val isNextButtonEnabled: Boolean get() = newMessageIdOrOns.isNotBlank()
|
||||||
}
|
}
|
||||||
|
|
||||||
internal data class Success(val publicKey: String)
|
internal data class Success(val publicKey: String)
|
@ -7,6 +7,7 @@ import android.content.ClipData
|
|||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
@ -18,10 +19,7 @@ import android.os.Bundle
|
|||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.text.SpannableStringBuilder
|
|
||||||
import android.text.SpannedString
|
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.text.style.StyleSpan
|
|
||||||
import android.util.Pair
|
import android.util.Pair
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.ActionMode
|
import android.view.ActionMode
|
||||||
@ -35,8 +33,12 @@ import android.widget.Toast
|
|||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.core.text.set
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.core.text.toSpannable
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.drawToBitmap
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
@ -51,6 +53,8 @@ import androidx.loader.content.Loader
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.annimon.stream.Stream
|
import com.annimon.stream.Stream
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.squareup.phrase.Phrase
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
@ -64,7 +68,6 @@ import network.loki.messenger.databinding.ActivityConversationV2Binding
|
|||||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||||
import nl.komponents.kovenant.ui.successUi
|
import nl.komponents.kovenant.ui.successUi
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
|
||||||
import org.session.libsession.messaging.messages.ExpirationConfiguration
|
import org.session.libsession.messaging.messages.ExpirationConfiguration
|
||||||
import org.session.libsession.messaging.messages.applyExpiryMode
|
import org.session.libsession.messaging.messages.applyExpiryMode
|
||||||
import org.session.libsession.messaging.messages.control.DataExtractionNotification
|
import org.session.libsession.messaging.messages.control.DataExtractionNotification
|
||||||
@ -84,6 +87,10 @@ import org.session.libsession.utilities.Address
|
|||||||
import org.session.libsession.utilities.Address.Companion.fromSerialized
|
import org.session.libsession.utilities.Address.Companion.fromSerialized
|
||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsession.utilities.MediaTypes
|
import org.session.libsession.utilities.MediaTypes
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.CONVERSATION_NAME_KEY
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY
|
||||||
import org.session.libsession.utilities.Stub
|
import org.session.libsession.utilities.Stub
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.concurrent.SimpleTask
|
import org.session.libsession.utilities.concurrent.SimpleTask
|
||||||
@ -97,6 +104,7 @@ import org.session.libsignal.utilities.guava.Optional
|
|||||||
import org.session.libsignal.utilities.hexEncodedPrivateKey
|
import org.session.libsignal.utilities.hexEncodedPrivateKey
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
|
import org.thoughtcrime.securesms.SessionDialogBuilder
|
||||||
import org.thoughtcrime.securesms.attachments.ScreenshotObserver
|
import org.thoughtcrime.securesms.attachments.ScreenshotObserver
|
||||||
import org.thoughtcrime.securesms.audio.AudioRecorder
|
import org.thoughtcrime.securesms.audio.AudioRecorder
|
||||||
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel
|
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel
|
||||||
@ -155,7 +163,6 @@ import org.thoughtcrime.securesms.mediasend.Media
|
|||||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivity
|
import org.thoughtcrime.securesms.mediasend.MediaSendActivity
|
||||||
import org.thoughtcrime.securesms.mms.AudioSlide
|
import org.thoughtcrime.securesms.mms.AudioSlide
|
||||||
import org.thoughtcrime.securesms.mms.GifSlide
|
import org.thoughtcrime.securesms.mms.GifSlide
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import org.thoughtcrime.securesms.mms.ImageSlide
|
import org.thoughtcrime.securesms.mms.ImageSlide
|
||||||
import org.thoughtcrime.securesms.mms.MediaConstraints
|
import org.thoughtcrime.securesms.mms.MediaConstraints
|
||||||
import org.thoughtcrime.securesms.mms.Slide
|
import org.thoughtcrime.securesms.mms.Slide
|
||||||
@ -165,19 +172,21 @@ import org.thoughtcrime.securesms.permissions.Permissions
|
|||||||
import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment
|
import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment
|
||||||
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment
|
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment
|
||||||
import org.thoughtcrime.securesms.showSessionDialog
|
import org.thoughtcrime.securesms.showSessionDialog
|
||||||
|
import org.thoughtcrime.securesms.ui.OpenURLAlertDialog
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.SessionMaterialTheme
|
||||||
import org.thoughtcrime.securesms.util.ActivityDispatcher
|
import org.thoughtcrime.securesms.util.ActivityDispatcher
|
||||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil
|
import org.thoughtcrime.securesms.util.MediaUtil
|
||||||
import org.thoughtcrime.securesms.util.NetworkUtils
|
import org.thoughtcrime.securesms.util.NetworkUtils
|
||||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask
|
import org.thoughtcrime.securesms.util.SaveAttachmentTask
|
||||||
import org.thoughtcrime.securesms.util.drawToBitmap
|
|
||||||
import org.thoughtcrime.securesms.util.isScrolledToBottom
|
import org.thoughtcrime.securesms.util.isScrolledToBottom
|
||||||
import org.thoughtcrime.securesms.util.isScrolledToWithin30dpOfBottom
|
import org.thoughtcrime.securesms.util.isScrolledToWithin30dpOfBottom
|
||||||
import org.thoughtcrime.securesms.util.push
|
import org.thoughtcrime.securesms.util.push
|
||||||
import org.thoughtcrime.securesms.util.show
|
import org.thoughtcrime.securesms.util.show
|
||||||
import org.thoughtcrime.securesms.util.toPx
|
import org.thoughtcrime.securesms.util.toPx
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
import java.util.LinkedList
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
@ -188,6 +197,8 @@ import kotlin.math.abs
|
|||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlin.math.sqrt
|
import kotlin.math.sqrt
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
|
|
||||||
private const val TAG = "ConversationActivityV2"
|
private const val TAG = "ConversationActivityV2"
|
||||||
|
|
||||||
@ -231,6 +242,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
.get(LinkPreviewViewModel::class.java)
|
.get(LinkPreviewViewModel::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var openLinkDialogUrl: String? by mutableStateOf(null)
|
||||||
|
|
||||||
private val threadId: Long by lazy {
|
private val threadId: Long by lazy {
|
||||||
var threadId = intent.getLongExtra(THREAD_ID, -1L)
|
var threadId = intent.getLongExtra(THREAD_ID, -1L)
|
||||||
if (threadId == -1L) {
|
if (threadId == -1L) {
|
||||||
@ -279,8 +292,17 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
var searchViewItem: MenuItem? = null
|
var searchViewItem: MenuItem? = null
|
||||||
|
|
||||||
private val bufferedLastSeenChannel = Channel<Long>(capacity = 512, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
private val bufferedLastSeenChannel = Channel<Long>(capacity = 512, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||||
|
|
||||||
private var emojiPickerVisible = false
|
private var emojiPickerVisible = false
|
||||||
|
|
||||||
|
// Queue of timestamps used to rate-limit emoji reactions
|
||||||
|
private val emojiRateLimiterQueue = LinkedList<Long>()
|
||||||
|
|
||||||
|
// Constants used to enforce the given maximum emoji reactions allowed per minute (emoji reactions
|
||||||
|
// that occur above this limit will result in a "Slow down" toast rather than adding the reaction).
|
||||||
|
private val EMOJI_REACTIONS_ALLOWED_PER_MINUTE = 20
|
||||||
|
private val ONE_MINUTE_IN_MILLISECONDS = 1.minutes.inWholeMilliseconds
|
||||||
|
|
||||||
private val isScrolledToBottom: Boolean
|
private val isScrolledToBottom: Boolean
|
||||||
get() = binding.conversationRecyclerView.isScrolledToBottom
|
get() = binding.conversationRecyclerView.isScrolledToBottom
|
||||||
|
|
||||||
@ -385,12 +407,33 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
fun showOpenUrlDialog(url: String){
|
||||||
|
openLinkDialogUrl = url
|
||||||
|
}
|
||||||
|
|
||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||||
super.onCreate(savedInstanceState, isReady)
|
super.onCreate(savedInstanceState, isReady)
|
||||||
binding = ActivityConversationV2Binding.inflate(layoutInflater)
|
binding = ActivityConversationV2Binding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
// set the compose dialog content
|
||||||
|
binding.dialogOpenUrl.apply {
|
||||||
|
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||||
|
setContent {
|
||||||
|
SessionMaterialTheme {
|
||||||
|
if(!openLinkDialogUrl.isNullOrEmpty()){
|
||||||
|
OpenURLAlertDialog(
|
||||||
|
url = openLinkDialogUrl!!,
|
||||||
|
onDismissRequest = {
|
||||||
|
openLinkDialogUrl = null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// messageIdToScroll
|
// messageIdToScroll
|
||||||
messageToScrollTimestamp.set(intent.getLongExtra(SCROLL_MESSAGE_ID, -1))
|
messageToScrollTimestamp.set(intent.getLongExtra(SCROLL_MESSAGE_ID, -1))
|
||||||
messageToScrollAuthor.set(intent.getParcelableExtra(SCROLL_MESSAGE_AUTHOR))
|
messageToScrollAuthor.set(intent.getParcelableExtra(SCROLL_MESSAGE_AUTHOR))
|
||||||
@ -704,7 +747,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(e: ExecutionException?) {
|
override fun onFailure(e: ExecutionException?) {
|
||||||
Toast.makeText(this@ConversationActivityV2, R.string.activity_conversation_attachment_prep_failed, Toast.LENGTH_LONG).show()
|
Toast.makeText(this@ConversationActivityV2, R.string.attachmentsErrorLoad, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@ -755,9 +798,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
// called from onCreate
|
// called from onCreate
|
||||||
private fun setUpBlockedBanner() {
|
private fun setUpBlockedBanner() {
|
||||||
val recipient = viewModel.recipient?.takeUnless { it.isGroupRecipient } ?: return
|
val recipient = viewModel.recipient?.takeUnless { it.isGroupRecipient } ?: return
|
||||||
val accountID = recipient.address.toString()
|
binding.blockedBannerTextView.text = applicationContext.getString(R.string.blockBlockedDescription)
|
||||||
val name = sessionContactDb.getContactWithAccountID(accountID)?.displayName(Contact.ContactContext.REGULAR) ?: accountID
|
|
||||||
binding.blockedBannerTextView.text = resources.getString(R.string.activity_conversation_blocked_banner_text, name)
|
|
||||||
binding.blockedBanner.isVisible = recipient.isBlocked
|
binding.blockedBanner.isVisible = recipient.isBlocked
|
||||||
binding.blockedBanner.setOnClickListener { viewModel.unblock() }
|
binding.blockedBanner.setOnClickListener { viewModel.unblock() }
|
||||||
}
|
}
|
||||||
@ -770,8 +811,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
|
|
||||||
binding.outdatedBanner.isVisible = shouldShowLegacy
|
binding.outdatedBanner.isVisible = shouldShowLegacy
|
||||||
if (shouldShowLegacy) {
|
if (shouldShowLegacy) {
|
||||||
binding.outdatedBannerTextView.text =
|
|
||||||
resources.getString(R.string.activity_conversation_outdated_client_banner_text, legacyRecipient!!.name)
|
val txt = Phrase.from(applicationContext, R.string.disappearingMessagesLegacy)
|
||||||
|
.put(NAME_KEY, legacyRecipient!!.name)
|
||||||
|
.format()
|
||||||
|
binding?.outdatedBannerTextView?.text = txt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1056,34 +1100,48 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
updateUnreadCountIndicator()
|
updateUnreadCountIndicator()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update placeholder / control messages in a conversation
|
||||||
private fun updatePlaceholder() {
|
private fun updatePlaceholder() {
|
||||||
val recipient = viewModel.recipient ?: return Log.w("Loki", "recipient was null in placeholder update")
|
val recipient = viewModel.recipient ?: return Log.w("Loki", "recipient was null in placeholder update")
|
||||||
val blindedRecipient = viewModel.blindedRecipient
|
val blindedRecipient = viewModel.blindedRecipient
|
||||||
val openGroup = viewModel.openGroup
|
val openGroup = viewModel.openGroup
|
||||||
|
|
||||||
val (textResource, insertParam) = when {
|
// Get the correct placeholder text for this type of empty conversation
|
||||||
recipient.isLocalNumber -> R.string.activity_conversation_empty_state_note_to_self to null
|
val isNoteToSelf = recipient.isLocalNumber
|
||||||
openGroup != null && !openGroup.canWrite -> R.string.activity_conversation_empty_state_read_only to recipient.toShortString()
|
val txtCS: CharSequence = when {
|
||||||
blindedRecipient?.blocksCommunityMessageRequests == true -> R.string.activity_conversation_empty_state_blocks_community_requests to recipient.toShortString()
|
recipient.isLocalNumber -> getString(R.string.noteToSelfEmpty)
|
||||||
else -> R.string.activity_conversation_empty_state_default to recipient.toShortString()
|
|
||||||
|
// If this is a community which we cannot write to
|
||||||
|
openGroup != null && !openGroup.canWrite -> {
|
||||||
|
Phrase.from(applicationContext, R.string.conversationsEmpty)
|
||||||
|
.put(CONVERSATION_NAME_KEY, openGroup.name)
|
||||||
|
.format()
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're trying to message someone who has blocked community message requests
|
||||||
|
blindedRecipient?.blocksCommunityMessageRequests == true -> {
|
||||||
|
Phrase.from(applicationContext, R.string.messageRequestsTurnedOff)
|
||||||
|
.put(NAME_KEY, recipient.toShortString())
|
||||||
|
.format()
|
||||||
|
}
|
||||||
|
|
||||||
|
recipient.isGroupRecipient -> {
|
||||||
|
// If this is a group or community that we CAN send messages to
|
||||||
|
Phrase.from(applicationContext, R.string.groupNoMessages)
|
||||||
|
.put(GROUP_NAME_KEY, recipient.toShortString())
|
||||||
|
.format()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
Log.w(TAG, "Something else happened in updatePlaceholder - we're not sure what.")
|
||||||
|
""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val showPlaceholder = adapter.itemCount == 0
|
val showPlaceholder = adapter.itemCount == 0
|
||||||
binding.placeholderText.isVisible = showPlaceholder
|
binding.placeholderText.isVisible = showPlaceholder
|
||||||
if (showPlaceholder) {
|
if (showPlaceholder) {
|
||||||
if (insertParam != null) {
|
binding.placeholderText.text = txtCS
|
||||||
val span = getText(textResource) as SpannedString
|
|
||||||
val annotations = span.getSpans(0, span.length, StyleSpan::class.java)
|
|
||||||
val boldSpan = annotations.first()
|
|
||||||
val spannedParam = insertParam.toSpannable()
|
|
||||||
spannedParam[0 until spannedParam.length] = StyleSpan(boldSpan.style)
|
|
||||||
val originalStart = span.getSpanStart(boldSpan)
|
|
||||||
val originalEnd = span.getSpanEnd(boldSpan)
|
|
||||||
val newString = SpannableStringBuilder(span)
|
|
||||||
.replace(originalStart, originalEnd, spannedParam)
|
|
||||||
binding.placeholderText.text = newString
|
|
||||||
} else {
|
|
||||||
binding.placeholderText.setText(textResource)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1117,11 +1175,21 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun block(deleteThread: Boolean) {
|
override fun block(deleteThread: Boolean) {
|
||||||
|
val recipient = viewModel.recipient ?: return Log.w("Loki", "Recipient was null for block action")
|
||||||
showSessionDialog {
|
showSessionDialog {
|
||||||
title(R.string.RecipientPreferenceActivity_block_this_contact_question)
|
title(R.string.block)
|
||||||
text(R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact)
|
text(
|
||||||
dangerButton(R.string.RecipientPreferenceActivity_block, R.string.AccessibilityId_block_confirm) {
|
Phrase.from(context, R.string.blockDescription)
|
||||||
|
.put(NAME_KEY, recipient.name)
|
||||||
|
.format()
|
||||||
|
)
|
||||||
|
dangerButton(R.string.block, R.string.AccessibilityId_blockConfirm) {
|
||||||
viewModel.block()
|
viewModel.block()
|
||||||
|
|
||||||
|
// Block confirmation toast added as per SS-64
|
||||||
|
val txt = Phrase.from(context, R.string.blockBlockedUser).put(NAME_KEY, recipient.name).format().toString()
|
||||||
|
Toast.makeText(context, txt, Toast.LENGTH_LONG).show()
|
||||||
|
|
||||||
if (deleteThread) {
|
if (deleteThread) {
|
||||||
viewModel.deleteThread()
|
viewModel.deleteThread()
|
||||||
finish()
|
finish()
|
||||||
@ -1135,7 +1203,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
val clip = ClipData.newPlainText("Account ID", accountId)
|
val clip = ClipData.newPlainText("Account ID", accountId)
|
||||||
val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager
|
val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
manager.setPrimaryClip(clip)
|
manager.setPrimaryClip(clip)
|
||||||
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.copied, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun copyOpenGroupUrl(thread: Recipient) {
|
override fun copyOpenGroupUrl(thread: Recipient) {
|
||||||
@ -1147,7 +1215,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
val clip = ClipData.newPlainText("Community URL", openGroup.joinURL)
|
val clip = ClipData.newPlainText("Community URL", openGroup.joinURL)
|
||||||
val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager
|
val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
manager.setPrimaryClip(clip)
|
manager.setPrimaryClip(clip)
|
||||||
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.copied, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showDisappearingMessages(thread: Recipient) {
|
override fun showDisappearingMessages(thread: Recipient) {
|
||||||
@ -1160,13 +1228,20 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun unblock() {
|
override fun unblock() {
|
||||||
|
val recipient = viewModel.recipient ?: return Log.w("Loki", "Recipient was null for unblock action")
|
||||||
|
|
||||||
|
if (!recipient.isContactRecipient) {
|
||||||
|
return Log.w("Loki", "Cannot unblock a user who is not a contact recipient - aborting unblock attempt.")
|
||||||
|
}
|
||||||
|
|
||||||
showSessionDialog {
|
showSessionDialog {
|
||||||
title(R.string.ConversationActivity_unblock_this_contact_question)
|
title(R.string.blockUnblock)
|
||||||
text(R.string.ConversationActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact)
|
text(
|
||||||
dangerButton(
|
Phrase.from(context, R.string.blockUnblockName)
|
||||||
R.string.ConversationActivity_unblock,
|
.put(NAME_KEY, recipient.name)
|
||||||
R.string.AccessibilityId_block_confirm
|
.format()
|
||||||
) { viewModel.unblock() }
|
)
|
||||||
|
dangerButton(R.string.blockUnblock, R.string.AccessibilityId_unblockConfirm) { viewModel.unblock() }
|
||||||
cancelButton()
|
cancelButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1177,10 +1252,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
if (actionMode != null) {
|
if (actionMode != null) {
|
||||||
onDeselect(message, position, actionMode)
|
onDeselect(message, position, actionMode)
|
||||||
} else {
|
} else {
|
||||||
// NOTE:
|
// NOTE: We have to use onContentClick (rather than a click listener directly on
|
||||||
// We have to use onContentClick (rather than a click listener directly on
|
|
||||||
// the view) so as to not interfere with all the other gestures. Do not add
|
// the view) so as to not interfere with all the other gestures. Do not add
|
||||||
// onClickListeners directly to message content views.
|
// onClickListeners directly to message content views!
|
||||||
view.onContentClick(event)
|
view.onContentClick(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1279,7 +1353,45 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Method to add an emoji to a queue and remove it a short while later - this is used as a
|
||||||
|
// rate-limiting mechanism and is called from the `sendEmojiReaction` method, below.
|
||||||
|
|
||||||
|
fun canPerformEmojiReaction(timestamp: Long): Boolean {
|
||||||
|
// If the emoji reaction queue is full..
|
||||||
|
if (emojiRateLimiterQueue.size >= EMOJI_REACTIONS_ALLOWED_PER_MINUTE) {
|
||||||
|
// ..grab the timestamp of the oldest emoji reaction.
|
||||||
|
val headTimestamp = emojiRateLimiterQueue.peekFirst()
|
||||||
|
if (headTimestamp == null) {
|
||||||
|
Log.w(TAG, "Could not get emoji react head timestamp - should never happen, but we'll allow the emoji reaction.")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// With the queue full, if the earliest emoji reaction occurred less than 1 minute ago
|
||||||
|
// then we reject it..
|
||||||
|
if (System.currentTimeMillis() - headTimestamp <= ONE_MINUTE_IN_MILLISECONDS) {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
// ..otherwise if the earliest emoji reaction was more than a minute ago we'll
|
||||||
|
// remove that early reaction to move the timestamp at index 1 into index 0, add
|
||||||
|
// our new timestamp and return true to accept the emoji reaction.
|
||||||
|
emojiRateLimiterQueue.removeFirst()
|
||||||
|
emojiRateLimiterQueue.addLast(timestamp)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If the queue isn't already full then we add the new timestamp to the back of the queue and allow the emoji reaction
|
||||||
|
emojiRateLimiterQueue.addLast(timestamp)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun sendEmojiReaction(emoji: String, originalMessage: MessageRecord) {
|
private fun sendEmojiReaction(emoji: String, originalMessage: MessageRecord) {
|
||||||
|
// Only allow the emoji reaction if we aren't currently rate limited
|
||||||
|
if (!canPerformEmojiReaction(System.currentTimeMillis())) {
|
||||||
|
Toast.makeText(this, getString(R.string.emojiReactsCoolDown), Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Create the message
|
// Create the message
|
||||||
val recipient = viewModel.recipient ?: return Log.w(TAG, "Could not locate recipient when sending emoji reaction")
|
val recipient = viewModel.recipient ?: return Log.w(TAG, "Could not locate recipient when sending emoji reaction")
|
||||||
val reactionMessage = VisibleMessage()
|
val reactionMessage = VisibleMessage()
|
||||||
@ -1325,6 +1437,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Method to remove a emoji reaction from a message.
|
||||||
|
// Note: We do not count emoji removal towards the emojiRateLimiterQueue.
|
||||||
private fun sendEmojiRemoval(emoji: String, originalMessage: MessageRecord) {
|
private fun sendEmojiRemoval(emoji: String, originalMessage: MessageRecord) {
|
||||||
val recipient = viewModel.recipient ?: return
|
val recipient = viewModel.recipient ?: return
|
||||||
val message = VisibleMessage()
|
val message = VisibleMessage()
|
||||||
@ -1591,9 +1705,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
val isNoteToSelf = (recipient.isContactRecipient && recipient.address.toString() == userPublicKey)
|
val isNoteToSelf = (recipient.isContactRecipient && recipient.address.toString() == userPublicKey)
|
||||||
if (seed in text && !isNoteToSelf && !hasPermissionToSendSeed) {
|
if (seed in text && !isNoteToSelf && !hasPermissionToSendSeed) {
|
||||||
showSessionDialog {
|
showSessionDialog {
|
||||||
title(R.string.dialog_send_seed_title)
|
title(R.string.warning)
|
||||||
text(R.string.dialog_send_seed_explanation)
|
text(R.string.recoveryPasswordWarningSendDescription)
|
||||||
button(R.string.dialog_send_seed_send_button_title) { sendTextOnlyMessage(true) }
|
button(R.string.send) { sendTextOnlyMessage(true) }
|
||||||
cancelButton()
|
cancelButton()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1673,9 +1787,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
val hasSeenGIFMetaDataWarning: Boolean = textSecurePreferences.hasSeenGIFMetaDataWarning()
|
val hasSeenGIFMetaDataWarning: Boolean = textSecurePreferences.hasSeenGIFMetaDataWarning()
|
||||||
if (!hasSeenGIFMetaDataWarning) {
|
if (!hasSeenGIFMetaDataWarning) {
|
||||||
showSessionDialog {
|
showSessionDialog {
|
||||||
title(R.string.giphy_permission_title)
|
title(R.string.giphyWarning)
|
||||||
text(R.string.giphy_permission_message)
|
text(Phrase.from(context, R.string.giphyWarningDescription).put(APP_NAME_KEY, getString(R.string.app_name)).format())
|
||||||
button(R.string.continue_2) {
|
button(R.string.theContinue) {
|
||||||
textSecurePreferences.setHasSeenGIFMetaDataWarning()
|
textSecurePreferences.setHasSeenGIFMetaDataWarning()
|
||||||
selectGif()
|
selectGif()
|
||||||
}
|
}
|
||||||
@ -1718,11 +1832,27 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
val mediaPreppedListener = object : ListenableFuture.Listener<Boolean> {
|
val mediaPreppedListener = object : ListenableFuture.Listener<Boolean> {
|
||||||
|
|
||||||
override fun onSuccess(result: Boolean?) {
|
override fun onSuccess(result: Boolean?) {
|
||||||
|
if (result == null) {
|
||||||
|
Log.w(TAG, "Media prepper returned a null result - bailing.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the attachment was too large or MediaConstraints.isSatisfied failed for some
|
||||||
|
// other reason then we reset the attachment manager & shown buttons then bail..
|
||||||
|
if (!result) {
|
||||||
|
attachmentManager.clear()
|
||||||
|
if (isShowingAttachmentOptions) { toggleAttachmentOptions() }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ..otherwise we can attempt to send the attachment(s).
|
||||||
|
// Note: The only multi-attachment message type is when sending images - all others
|
||||||
|
// attempt send the attachment immediately upon file selection.
|
||||||
sendAttachments(attachmentManager.buildSlideDeck().asAttachments(), null)
|
sendAttachments(attachmentManager.buildSlideDeck().asAttachments(), null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(e: ExecutionException?) {
|
override fun onFailure(e: ExecutionException?) {
|
||||||
Toast.makeText(this@ConversationActivityV2, R.string.activity_conversation_attachment_prep_failed, Toast.LENGTH_LONG).show()
|
Toast.makeText(this@ConversationActivityV2, R.string.attachmentsErrorLoad, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
when (requestCode) {
|
when (requestCode) {
|
||||||
@ -1805,8 +1935,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
} else {
|
} else {
|
||||||
Permissions.with(this)
|
Permissions.with(this)
|
||||||
.request(Manifest.permission.RECORD_AUDIO)
|
.request(Manifest.permission.RECORD_AUDIO)
|
||||||
.withRationaleDialog(getString(R.string.ConversationActivity_to_send_audio_messages_allow_signal_access_to_your_microphone), R.drawable.ic_baseline_mic_48)
|
.withRationaleDialog(getString(R.string.permissionsMicrophoneAccessRequired), R.drawable.ic_baseline_mic_48)
|
||||||
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_requires_the_microphone_permission_in_order_to_send_audio_messages))
|
.withPermanentDenialDialog(Phrase.from(applicationContext, R.string.permissionsMicrophoneAccessRequired)
|
||||||
|
.put(APP_NAME_KEY, getString(R.string.app_name))
|
||||||
|
.format().toString())
|
||||||
.execute()
|
.execute()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1876,7 +2008,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(e: ExecutionException) {
|
override fun onFailure(e: ExecutionException) {
|
||||||
Toast.makeText(this@ConversationActivityV2, R.string.ConversationActivity_unable_to_record_audio, Toast.LENGTH_LONG).show()
|
Toast.makeText(this@ConversationActivityV2, R.string.audioUnableToRecord, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1927,10 +2059,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun showDeleteLocallyUI(messages: Set<MessageRecord>) {
|
private fun showDeleteLocallyUI(messages: Set<MessageRecord>) {
|
||||||
val messageCount = 1
|
|
||||||
showSessionDialog {
|
showSessionDialog {
|
||||||
title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
|
title(resources.getQuantityString(R.plurals.deleteMessage, messages.count(), messages.count()))
|
||||||
text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
|
text(resources.getString(R.string.deleteMessagesDescriptionDevice))
|
||||||
button(R.string.delete) { messages.forEach(viewModel::deleteLocally); endActionMode() }
|
button(R.string.delete) { messages.forEach(viewModel::deleteLocally); endActionMode() }
|
||||||
cancelButton(::endActionMode)
|
cancelButton(::endActionMode)
|
||||||
}
|
}
|
||||||
@ -1950,13 +2081,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
|
|
||||||
// If the recipient is a community OR a Note-to-Self then we delete the message for everyone
|
// If the recipient is a community OR a Note-to-Self then we delete the message for everyone
|
||||||
if (recipient.isCommunityRecipient || recipient.isLocalNumber) {
|
if (recipient.isCommunityRecipient || recipient.isLocalNumber) {
|
||||||
val messageCount = 1 // Only used for plurals string
|
|
||||||
showSessionDialog {
|
showSessionDialog {
|
||||||
title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
|
title(resources.getQuantityString(R.plurals.deleteMessage, messages.count(), messages.count()))
|
||||||
text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
|
text(resources.getString(R.string.deleteMessageDescriptionEveryone))
|
||||||
button(R.string.delete) {
|
button(R.string.delete) { messages.forEach(viewModel::deleteForEveryone); endActionMode() }
|
||||||
messages.forEach(viewModel::deleteForEveryone); endActionMode()
|
|
||||||
}
|
|
||||||
cancelButton { endActionMode() }
|
cancelButton { endActionMode() }
|
||||||
}
|
}
|
||||||
// Otherwise if this is a 1-on-1 conversation we may decided to delete just for ourselves or delete for everyone
|
// Otherwise if this is a 1-on-1 conversation we may decided to delete just for ourselves or delete for everyone
|
||||||
@ -1981,13 +2109,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
}
|
}
|
||||||
else // Finally, if this is a closed group and you are deleting someone else's message(s) then we can only delete locally.
|
else // Finally, if this is a closed group and you are deleting someone else's message(s) then we can only delete locally.
|
||||||
{
|
{
|
||||||
val messageCount = 1
|
|
||||||
showSessionDialog {
|
showSessionDialog {
|
||||||
title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
|
title(resources.getQuantityString(R.plurals.deleteMessage, messages.count(), messages.count()))
|
||||||
text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
|
text(resources.getString(R.string.deleteMessageDescriptionDevice))
|
||||||
button(R.string.delete) {
|
dangerButton(R.string.delete) { messages.forEach(viewModel::deleteLocally); endActionMode() }
|
||||||
messages.forEach(viewModel::deleteLocally); endActionMode()
|
|
||||||
}
|
|
||||||
cancelButton(::endActionMode)
|
cancelButton(::endActionMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1995,18 +2120,18 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
|
|
||||||
override fun banUser(messages: Set<MessageRecord>) {
|
override fun banUser(messages: Set<MessageRecord>) {
|
||||||
showSessionDialog {
|
showSessionDialog {
|
||||||
title(R.string.ConversationFragment_ban_selected_user)
|
title(R.string.banUser)
|
||||||
text("This will ban the selected user from this room. It won't ban them from other rooms.")
|
text(R.string.communityBanDescription)
|
||||||
button(R.string.ban) { viewModel.banUser(messages.first().individualRecipient); endActionMode() }
|
button(R.string.banUser) { viewModel.banUser(messages.first().individualRecipient); endActionMode() }
|
||||||
cancelButton(::endActionMode)
|
cancelButton(::endActionMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun banAndDeleteAll(messages: Set<MessageRecord>) {
|
override fun banAndDeleteAll(messages: Set<MessageRecord>) {
|
||||||
showSessionDialog {
|
showSessionDialog {
|
||||||
title(R.string.ConversationFragment_ban_selected_user)
|
title(R.string.banUser)
|
||||||
text("This will ban the selected user from this room and delete all messages sent by them. It won't ban them from other rooms or delete the messages they sent there.")
|
text(R.string.communityBanDeleteDescription)
|
||||||
button(R.string.ban) { viewModel.banAndDeleteAll(messages.first()); endActionMode() }
|
button(R.string.banUser) { viewModel.banAndDeleteAll(messages.first()); endActionMode() }
|
||||||
cancelButton(::endActionMode)
|
cancelButton(::endActionMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2042,7 +2167,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
if (TextUtils.isEmpty(result)) { return }
|
if (TextUtils.isEmpty(result)) { return }
|
||||||
val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
manager.setPrimaryClip(ClipData.newPlainText("Message Content", result))
|
manager.setPrimaryClip(ClipData.newPlainText("Message Content", result))
|
||||||
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.copied, Toast.LENGTH_SHORT).show()
|
||||||
endActionMode()
|
endActionMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2051,7 +2176,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
val clip = ClipData.newPlainText("Account ID", accountID)
|
val clip = ClipData.newPlainText("Account ID", accountID)
|
||||||
val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
manager.setPrimaryClip(clip)
|
manager.setPrimaryClip(clip)
|
||||||
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.copied, Toast.LENGTH_SHORT).show()
|
||||||
endActionMode()
|
endActionMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2093,41 +2218,95 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
endActionMode()
|
endActionMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun saveAttachments(message: MmsMessageRecord) {
|
||||||
|
val attachments: List<SaveAttachmentTask.Attachment?> = Stream.of(message.slideDeck.slides)
|
||||||
|
.filter { s: Slide -> s.uri != null && (s.hasImage() || s.hasVideo() || s.hasAudio() || s.hasDocument()) }
|
||||||
|
.map { s: Slide -> SaveAttachmentTask.Attachment(s.uri!!, s.contentType, message.dateReceived, s.fileName.orNull()) }
|
||||||
|
.toList()
|
||||||
|
if (attachments.isNotEmpty()) {
|
||||||
|
val saveTask = SaveAttachmentTask(this)
|
||||||
|
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, *attachments.toTypedArray())
|
||||||
|
if (!message.isOutgoing) { sendMediaSavedNotification() }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Implied else that there were no attachment(s)
|
||||||
|
Toast.makeText(this, resources.getString(R.string.attachmentsSaveError), Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasPermission(permission: String): Boolean {
|
||||||
|
val result = ContextCompat.checkSelfPermission(this, permission)
|
||||||
|
return result == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
|
||||||
override fun saveAttachment(messages: Set<MessageRecord>) {
|
override fun saveAttachment(messages: Set<MessageRecord>) {
|
||||||
val message = messages.first() as MmsMessageRecord
|
val message = messages.first() as MmsMessageRecord
|
||||||
|
|
||||||
// Do not allow the user to download a file attachment before it has finished downloading
|
// Note: The save option is only added to the menu in ConversationReactionOverlay.getMenuActionItems
|
||||||
|
// if the attachment has finished downloading, so we don't really have to check for message.isMediaPending
|
||||||
|
// here - but we'll do it anyway and bail should that be the case as a defensive programming strategy.
|
||||||
if (message.isMediaPending) {
|
if (message.isMediaPending) {
|
||||||
Toast.makeText(this, resources.getString(R.string.conversation_activity__wait_until_attachment_has_finished_downloading), Toast.LENGTH_LONG).show()
|
Log.w(TAG, "Somehow we were asked to download an attachment before it had finished downloading - aborting download.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
SaveAttachmentTask.showWarningDialog(this) {
|
// Before saving an attachment, regardless of Android API version or permissions, we always want to ensure
|
||||||
|
// that we've warned the user just _once_ that any attachments they save can be accessed by other apps.
|
||||||
|
val haveWarned = TextSecurePreferences.getHaveWarnedUserAboutSavingAttachments(this)
|
||||||
|
if (haveWarned) {
|
||||||
|
// On Android versions below 30 we require the WRITE_EXTERNAL_STORAGE permission to save attachments.
|
||||||
|
if (Build.VERSION.SDK_INT < 30) {
|
||||||
|
// Save the attachment(s) then bail if we already have permission to do so
|
||||||
|
if (hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||||
|
saveAttachments(message)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
/* If we don't have the permission then do nothing - which means we continue on to the SaveAttachmentTask part below where we ask for permissions */
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// On more modern versions of Android on API 30+ WRITE_EXTERNAL_STORAGE is no longer used and we can just
|
||||||
|
// save files to the public directories like "Downloads", "Pictures" etc.
|
||||||
|
saveAttachments(message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ..otherwise we must ask for it first (only on Android APIs up to 28).
|
||||||
|
SaveAttachmentTask.showOneTimeWarningDialogOrSave(this) {
|
||||||
Permissions.with(this)
|
Permissions.with(this)
|
||||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
.maxSdkVersion(Build.VERSION_CODES.P)
|
.maxSdkVersion(Build.VERSION_CODES.P) // P is 28
|
||||||
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
|
.withPermanentDenialDialog(Phrase.from(applicationContext, R.string.permissionsStorageSaveDenied)
|
||||||
|
.put(APP_NAME_KEY, getString(R.string.app_name))
|
||||||
|
.format().toString())
|
||||||
.onAnyDenied {
|
.onAnyDenied {
|
||||||
endActionMode()
|
endActionMode()
|
||||||
Toast.makeText(this@ConversationActivityV2, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show()
|
|
||||||
|
// If permissions were denied inform the user that we can't proceed without them and offer to take the user to Settings
|
||||||
|
showSessionDialog {
|
||||||
|
title(R.string.permissionsRequired)
|
||||||
|
|
||||||
|
val txt = Phrase.from(applicationContext, R.string.permissionsStorageSaveDenied)
|
||||||
|
.put(APP_NAME_KEY, getString(R.string.app_name))
|
||||||
|
.format().toString()
|
||||||
|
text(txt)
|
||||||
|
|
||||||
|
// Take the user directly to the settings app for Session to grant the permission if they
|
||||||
|
// initially denied it but then have a change of heart when they realise they can't
|
||||||
|
// proceed without it.
|
||||||
|
dangerButton(R.string.theContinue) {
|
||||||
|
val intent = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
val uri = Uri.fromParts("package", packageName, null)
|
||||||
|
intent.setData(uri)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
button(R.string.cancel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onAllGranted {
|
.onAllGranted {
|
||||||
endActionMode()
|
endActionMode()
|
||||||
val attachments: List<SaveAttachmentTask.Attachment?> = Stream.of(message.slideDeck.slides)
|
saveAttachments(message)
|
||||||
.filter { s: Slide -> s.uri != null && (s.hasImage() || s.hasVideo() || s.hasAudio() || s.hasDocument()) }
|
|
||||||
.map { s: Slide -> SaveAttachmentTask.Attachment(s.uri!!, s.contentType, message.dateReceived, s.fileName.orNull()) }
|
|
||||||
.toList()
|
|
||||||
if (attachments.isNotEmpty()) {
|
|
||||||
val saveTask = SaveAttachmentTask(this)
|
|
||||||
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, *attachments.toTypedArray())
|
|
||||||
if (!message.isOutgoing) {
|
|
||||||
sendMediaSavedNotification()
|
|
||||||
}
|
|
||||||
return@onAllGranted
|
|
||||||
}
|
|
||||||
Toast.makeText(this,
|
|
||||||
resources.getQuantityString(R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card, 1),
|
|
||||||
Toast.LENGTH_LONG).show()
|
|
||||||
}
|
}
|
||||||
.execute()
|
.execute()
|
||||||
}
|
}
|
||||||
@ -2182,6 +2361,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
searchViewModel.onMissingResult() }
|
searchViewModel.onMissingResult() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.searchBottomBar.setData(result.position, result.getResults().size)
|
binding.searchBottomBar.setData(result.position, result.getResults().size)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -2191,6 +2371,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
binding.searchBottomBar.visibility = View.VISIBLE
|
binding.searchBottomBar.visibility = View.VISIBLE
|
||||||
binding.searchBottomBar.setData(0, 0)
|
binding.searchBottomBar.setData(0, 0)
|
||||||
binding.inputBar.visibility = View.INVISIBLE
|
binding.inputBar.visibility = View.INVISIBLE
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSearchClosed() {
|
fun onSearchClosed() {
|
||||||
|
@ -12,6 +12,8 @@ import androidx.core.util.getOrDefault
|
|||||||
import androidx.core.util.set
|
import androidx.core.util.set
|
||||||
import androidx.lifecycle.LifecycleCoroutineScope
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||||
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
|
import kotlin.math.min
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
@ -20,6 +22,7 @@ import kotlinx.coroutines.launch
|
|||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY
|
||||||
import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView
|
import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView
|
||||||
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
|
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
|
||||||
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate
|
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate
|
||||||
@ -29,8 +32,8 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
|||||||
import com.bumptech.glide.RequestManager
|
import com.bumptech.glide.RequestManager
|
||||||
import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity
|
import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity
|
||||||
import org.thoughtcrime.securesms.showSessionDialog
|
import org.thoughtcrime.securesms.showSessionDialog
|
||||||
import java.util.concurrent.atomic.AtomicLong
|
import org.thoughtcrime.securesms.ui.getSubbedCharSequence
|
||||||
import kotlin.math.min
|
import org.thoughtcrime.securesms.ui.getSubbedString
|
||||||
|
|
||||||
class ConversationAdapter(
|
class ConversationAdapter(
|
||||||
context: Context,
|
context: Context,
|
||||||
@ -154,9 +157,13 @@ class ConversationAdapter(
|
|||||||
if (message.isCallLog && message.isFirstMissedCall) {
|
if (message.isCallLog && message.isFirstMissedCall) {
|
||||||
viewHolder.view.setOnClickListener {
|
viewHolder.view.setOnClickListener {
|
||||||
context.showSessionDialog {
|
context.showSessionDialog {
|
||||||
title(R.string.CallNotificationBuilder_first_call_title)
|
val titleTxt = context.getSubbedString(R.string.callsMissedCallFrom, NAME_KEY to message.individualRecipient.name!!)
|
||||||
text(R.string.CallNotificationBuilder_first_call_message)
|
title(titleTxt)
|
||||||
button(R.string.activity_settings_title) {
|
|
||||||
|
val bodyTxt = context.getSubbedCharSequence(R.string.callsYouMissedCallPermissions, NAME_KEY to message.individualRecipient.name!!)
|
||||||
|
text(bodyTxt)
|
||||||
|
|
||||||
|
button(R.string.sessionSettings) {
|
||||||
Intent(context, PrivacySettingsActivity::class.java)
|
Intent(context, PrivacySettingsActivity::class.java)
|
||||||
.let(context::startActivity)
|
.let(context::startActivity)
|
||||||
}
|
}
|
||||||
@ -190,7 +197,7 @@ class ConversationAdapter(
|
|||||||
private fun getMessageBefore(position: Int, cursor: Cursor): MessageRecord? {
|
private fun getMessageBefore(position: Int, cursor: Cursor): MessageRecord? {
|
||||||
// The message that's visually before the current one is actually after the current
|
// The message that's visually before the current one is actually after the current
|
||||||
// one for the cursor because the layout is reversed
|
// one for the cursor because the layout is reversed
|
||||||
if (isReversed && !cursor.moveToPosition(position + 1)) { return null }
|
if (isReversed && !cursor.moveToPosition(position + 1)) { return null }
|
||||||
if (!isReversed && !cursor.moveToPosition(position - 1)) { return null }
|
if (!isReversed && !cursor.moveToPosition(position - 1)) { return null }
|
||||||
|
|
||||||
return messageDB.readerFor(cursor).current
|
return messageDB.readerFor(cursor).current
|
||||||
|
@ -22,7 +22,11 @@ import androidx.constraintlayout.widget.ConstraintLayout
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.doOnLayout
|
import androidx.core.view.doOnLayout
|
||||||
import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat
|
import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat
|
||||||
|
import com.squareup.phrase.Phrase
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import java.util.Locale
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@ -30,7 +34,9 @@ import kotlinx.coroutines.flow.filter
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.LocalisedTimeUtil.toShortTwoPartString
|
||||||
import org.session.libsession.snode.SnodeAPI
|
import org.session.libsession.snode.SnodeAPI
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.TIME_LARGE_KEY
|
||||||
import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber
|
import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber
|
||||||
import org.session.libsession.utilities.ThemeUtil
|
import org.session.libsession.utilities.ThemeUtil
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
import org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||||
@ -46,14 +52,6 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get
|
|||||||
import org.thoughtcrime.securesms.repository.ConversationRepository
|
import org.thoughtcrime.securesms.repository.ConversationRepository
|
||||||
import org.thoughtcrime.securesms.util.AnimationCompleteListener
|
import org.thoughtcrime.securesms.util.AnimationCompleteListener
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
import java.util.Locale
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlin.time.Duration
|
|
||||||
import kotlin.time.Duration.Companion.days
|
|
||||||
import kotlin.time.Duration.Companion.hours
|
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
|
||||||
import kotlin.time.Duration.Companion.minutes
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ConversationReactionOverlay : FrameLayout {
|
class ConversationReactionOverlay : FrameLayout {
|
||||||
@ -529,11 +527,11 @@ class ConversationReactionOverlay : FrameLayout {
|
|||||||
?: return emptyList()
|
?: return emptyList()
|
||||||
val userPublicKey = getLocalNumber(context)!!
|
val userPublicKey = getLocalNumber(context)!!
|
||||||
// Select message
|
// Select message
|
||||||
items += ActionItem(R.attr.menu_select_icon, R.string.conversation_context__menu_select, { handleActionItemClicked(Action.SELECT) }, R.string.AccessibilityId_select)
|
items += ActionItem(R.attr.menu_select_icon, R.string.select, { handleActionItemClicked(Action.SELECT) }, R.string.AccessibilityId_select)
|
||||||
// Reply
|
// Reply
|
||||||
val canWrite = openGroup == null || openGroup.canWrite
|
val canWrite = openGroup == null || openGroup.canWrite
|
||||||
if (canWrite && !message.isPending && !message.isFailed && !message.isOpenGroupInvitation) {
|
if (canWrite && !message.isPending && !message.isFailed && !message.isOpenGroupInvitation) {
|
||||||
items += ActionItem(R.attr.menu_reply_icon, R.string.conversation_context__menu_reply, { handleActionItemClicked(Action.REPLY) }, R.string.AccessibilityId_reply_message)
|
items += ActionItem(R.attr.menu_reply_icon, R.string.reply, { handleActionItemClicked(Action.REPLY) }, R.string.AccessibilityId_reply)
|
||||||
}
|
}
|
||||||
// Copy message text
|
// Copy message text
|
||||||
if (!containsControlMessage && hasText) {
|
if (!containsControlMessage && hasText) {
|
||||||
@ -541,34 +539,42 @@ class ConversationReactionOverlay : FrameLayout {
|
|||||||
}
|
}
|
||||||
// Copy Account ID
|
// Copy Account ID
|
||||||
if (recipient.isGroupRecipient && !recipient.isCommunityRecipient && message.recipient.address.toString() != userPublicKey) {
|
if (recipient.isGroupRecipient && !recipient.isCommunityRecipient && message.recipient.address.toString() != userPublicKey) {
|
||||||
items += ActionItem(R.attr.menu_copy_icon, R.string.activity_conversation_menu_copy_account_id, { handleActionItemClicked(Action.COPY_ACCOUNT_ID) })
|
items += ActionItem(R.attr.menu_copy_icon, R.string.accountIDCopy, { handleActionItemClicked(Action.COPY_ACCOUNT_ID) })
|
||||||
}
|
}
|
||||||
// Delete message
|
// Delete message
|
||||||
if (userCanDeleteSelectedItems(context, message, openGroup, userPublicKey, blindedPublicKey)) {
|
if (userCanDeleteSelectedItems(context, message, openGroup, userPublicKey, blindedPublicKey)) {
|
||||||
items += ActionItem(R.attr.menu_trash_icon, R.string.delete, { handleActionItemClicked(Action.DELETE) },
|
items += ActionItem(R.attr.menu_trash_icon, R.string.delete, { handleActionItemClicked(Action.DELETE) },
|
||||||
R.string.AccessibilityId_delete_message, message.subtitle, ThemeUtil.getThemedColor(context, R.attr.danger))
|
R.string.AccessibilityId_deleteMessage, message.subtitle, ThemeUtil.getThemedColor(context, R.attr.danger))
|
||||||
}
|
}
|
||||||
// Ban user
|
// Ban user
|
||||||
if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey)) {
|
if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey)) {
|
||||||
items += ActionItem(R.attr.menu_block_icon, R.string.conversation_context__menu_ban_user, { handleActionItemClicked(Action.BAN_USER) })
|
items += ActionItem(R.attr.menu_block_icon, R.string.banUser, { handleActionItemClicked(Action.BAN_USER) })
|
||||||
}
|
}
|
||||||
// Ban and delete all
|
// Ban and delete all
|
||||||
if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey)) {
|
if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey)) {
|
||||||
items += ActionItem(R.attr.menu_trash_icon, R.string.conversation_context__menu_ban_and_delete_all, { handleActionItemClicked(Action.BAN_AND_DELETE_ALL) })
|
items += ActionItem(R.attr.menu_trash_icon, R.string.banDeleteAll, { handleActionItemClicked(Action.BAN_AND_DELETE_ALL) })
|
||||||
}
|
}
|
||||||
// Message detail
|
// Message detail
|
||||||
items += ActionItem(R.attr.menu_info_icon, R.string.conversation_context__menu_message_details, { handleActionItemClicked(Action.VIEW_INFO) })
|
items += ActionItem(R.attr.menu_info_icon, R.string.messageInfo, { handleActionItemClicked(Action.VIEW_INFO) })
|
||||||
// Resend
|
// Resend
|
||||||
if (message.isFailed) {
|
if (message.isFailed) {
|
||||||
items += ActionItem(R.attr.menu_reply_icon, R.string.conversation_context__menu_resend_message, { handleActionItemClicked(Action.RESEND) })
|
items += ActionItem(R.attr.menu_reply_icon, R.string.resend, { handleActionItemClicked(Action.RESEND) })
|
||||||
}
|
}
|
||||||
// Resync
|
// Resync
|
||||||
if (message.isSyncFailed) {
|
if (message.isSyncFailed) {
|
||||||
items += ActionItem(R.attr.menu_reply_icon, R.string.conversation_context__menu_resync_message, { handleActionItemClicked(Action.RESYNC) })
|
items += ActionItem(R.attr.menu_reply_icon, R.string.resync, { handleActionItemClicked(Action.RESYNC) })
|
||||||
}
|
}
|
||||||
// Save media
|
// Save media..
|
||||||
if (message.isMms && (message as MediaMmsMessageRecord).containsMediaSlide()) {
|
if (message.isMms) {
|
||||||
items += ActionItem(R.attr.menu_save_icon, R.string.conversation_context_image__save_attachment, { handleActionItemClicked(Action.DOWNLOAD) }, R.string.AccessibilityId_save_attachment)
|
// ..but only provide the save option if the there is a media attachment which has finished downloading.
|
||||||
|
val mmsMessage = message as MediaMmsMessageRecord
|
||||||
|
if (mmsMessage.containsMediaSlide() && !mmsMessage.isMediaPending) {
|
||||||
|
items += ActionItem(R.attr.menu_save_icon,
|
||||||
|
R.string.save,
|
||||||
|
{ handleActionItemClicked(Action.DOWNLOAD) },
|
||||||
|
R.string.AccessibilityId_save
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
backgroundView.visibility = VISIBLE
|
backgroundView.visibility = VISIBLE
|
||||||
foregroundView.visibility = VISIBLE
|
foregroundView.visibility = VISIBLE
|
||||||
@ -704,10 +710,6 @@ class ConversationReactionOverlay : FrameLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Duration.to2partString(): String? =
|
|
||||||
toComponents { days, hours, minutes, seconds, nanoseconds -> listOf(days.days, hours.hours, minutes.minutes, seconds.seconds) }
|
|
||||||
.filter { it.inWholeSeconds > 0L }.take(2).takeIf { it.isNotEmpty() }?.joinToString(" ")
|
|
||||||
|
|
||||||
private val MessageRecord.subtitle: ((Context) -> CharSequence?)?
|
private val MessageRecord.subtitle: ((Context) -> CharSequence?)?
|
||||||
get() = if (expiresIn <= 0) {
|
get() = if (expiresIn <= 0) {
|
||||||
null
|
null
|
||||||
@ -715,6 +717,10 @@ private val MessageRecord.subtitle: ((Context) -> CharSequence?)?
|
|||||||
(expiresIn - (SnodeAPI.nowWithOffset - (expireStarted.takeIf { it > 0 } ?: timestamp)))
|
(expiresIn - (SnodeAPI.nowWithOffset - (expireStarted.takeIf { it > 0 } ?: timestamp)))
|
||||||
.coerceAtLeast(0L)
|
.coerceAtLeast(0L)
|
||||||
.milliseconds
|
.milliseconds
|
||||||
.to2partString()
|
.toShortTwoPartString()
|
||||||
?.let { context.getString(R.string.auto_deletes_in, it) }
|
.let {
|
||||||
|
Phrase.from(context, R.string.disappearingMessagesCountdownBigMobile)
|
||||||
|
.put(TIME_LARGE_KEY, it)
|
||||||
|
.format().toString()
|
||||||
|
}
|
||||||
}
|
}
|
@ -58,7 +58,7 @@ class DeleteOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListen
|
|||||||
}
|
}
|
||||||
if (!recipient.isGroupRecipient && !contact.isNullOrEmpty()) {
|
if (!recipient.isGroupRecipient && !contact.isNullOrEmpty()) {
|
||||||
binding.deleteForEveryoneTextView.text =
|
binding.deleteForEveryoneTextView.text =
|
||||||
resources.getString(R.string.delete_message_for_me_and_recipient, contact)
|
resources.getString(R.string.clearMessagesForEveryone, contact)
|
||||||
}
|
}
|
||||||
binding.deleteForEveryoneTextView.isVisible = !recipient.isClosedGroupRecipient
|
binding.deleteForEveryoneTextView.isVisible = !recipient.isClosedGroupRecipient
|
||||||
binding.deleteForMeTextView.setOnClickListener(this)
|
binding.deleteForMeTextView.setOnClickListener(this)
|
||||||
|
@ -100,7 +100,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||||
super.onCreate(savedInstanceState, ready)
|
super.onCreate(savedInstanceState, ready)
|
||||||
|
|
||||||
title = resources.getString(R.string.conversation_context__menu_message_details)
|
title = resources.getString(R.string.messageInfo)
|
||||||
|
|
||||||
viewModel.timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L)
|
viewModel.timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L)
|
||||||
|
|
||||||
@ -313,7 +313,7 @@ fun ExpandButton(modifier: Modifier = Modifier, onClick: () -> Unit) {
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.ic_expand),
|
painter = painterResource(id = R.drawable.ic_expand),
|
||||||
contentDescription = stringResource(id = R.string.expand),
|
contentDescription = stringResource(id = R.string.AccessibilityId_expand),
|
||||||
modifier = Modifier.clickable { onClick() },
|
modifier = Modifier.clickable { onClick() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -331,7 +331,7 @@ fun PreviewMessageDetails(
|
|||||||
imageAttachments = listOf(
|
imageAttachments = listOf(
|
||||||
Attachment(
|
Attachment(
|
||||||
fileDetails = listOf(
|
fileDetails = listOf(
|
||||||
TitledText(R.string.message_details_header__file_id, "Screen Shot 2023-07-06 at 11.35.50 am.png")
|
TitledText(R.string.attachmentsFileId, "Screen Shot 2023-07-06 at 11.35.50 am.png")
|
||||||
),
|
),
|
||||||
fileName = "Screen Shot 2023-07-06 at 11.35.50 am.png",
|
fileName = "Screen Shot 2023-07-06 at 11.35.50 am.png",
|
||||||
uri = Uri.parse(""),
|
uri = Uri.parse(""),
|
||||||
@ -339,7 +339,7 @@ fun PreviewMessageDetails(
|
|||||||
),
|
),
|
||||||
Attachment(
|
Attachment(
|
||||||
fileDetails = listOf(
|
fileDetails = listOf(
|
||||||
TitledText(R.string.message_details_header__file_id, "Screen Shot 2023-07-06 at 11.35.50 am.png")
|
TitledText(R.string.attachmentsFileId, "Screen Shot 2023-07-06 at 11.35.50 am.png")
|
||||||
),
|
),
|
||||||
fileName = "Screen Shot 2023-07-06 at 11.35.50 am.png",
|
fileName = "Screen Shot 2023-07-06 at 11.35.50 am.png",
|
||||||
uri = Uri.parse(""),
|
uri = Uri.parse(""),
|
||||||
@ -347,7 +347,7 @@ fun PreviewMessageDetails(
|
|||||||
),
|
),
|
||||||
Attachment(
|
Attachment(
|
||||||
fileDetails = listOf(
|
fileDetails = listOf(
|
||||||
TitledText(R.string.message_details_header__file_id, "Screen Shot 2023-07-06 at 11.35.50 am.png")
|
TitledText(R.string.attachmentsFileId, "Screen Shot 2023-07-06 at 11.35.50 am.png")
|
||||||
),
|
),
|
||||||
fileName = "Screen Shot 2023-07-06 at 11.35.50 am.png",
|
fileName = "Screen Shot 2023-07-06 at 11.35.50 am.png",
|
||||||
uri = Uri.parse(""),
|
uri = Uri.parse(""),
|
||||||
@ -356,14 +356,14 @@ fun PreviewMessageDetails(
|
|||||||
|
|
||||||
),
|
),
|
||||||
nonImageAttachmentFileDetails = listOf(
|
nonImageAttachmentFileDetails = listOf(
|
||||||
TitledText(R.string.message_details_header__file_id, "Screen Shot 2023-07-06 at 11.35.50 am.png"),
|
TitledText(R.string.attachmentsFileId, "Screen Shot 2023-07-06 at 11.35.50 am.png"),
|
||||||
TitledText(R.string.message_details_header__file_type, "image/png"),
|
TitledText(R.string.attachmentsFileType, "image/png"),
|
||||||
TitledText(R.string.message_details_header__file_size, "195.6kB"),
|
TitledText(R.string.attachmentsFileSize, "195.6kB"),
|
||||||
TitledText(R.string.message_details_header__resolution, "342x312"),
|
TitledText(R.string.attachmentsResolution, "342x312"),
|
||||||
),
|
),
|
||||||
sent = TitledText(R.string.message_details_header__sent, "6:12 AM Tue, 09/08/2022"),
|
sent = TitledText(R.string.sent, "6:12 AM Tue, 09/08/2022"),
|
||||||
received = TitledText(R.string.message_details_header__received, "6:12 AM Tue, 09/08/2022"),
|
received = TitledText(R.string.received, "6:12 AM Tue, 09/08/2022"),
|
||||||
error = TitledText(R.string.message_details_header__error, "Message failed to send"),
|
error = TitledText(R.string.error, "Message failed to send"),
|
||||||
senderInfo = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54"),
|
senderInfo = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -78,9 +78,9 @@ class MessageDetailsViewModel @Inject constructor(
|
|||||||
MessageDetailsState(
|
MessageDetailsState(
|
||||||
attachments = slides.map(::Attachment),
|
attachments = slides.map(::Attachment),
|
||||||
record = record,
|
record = record,
|
||||||
sent = dateSent.let(::Date).toString().let { TitledText(R.string.message_details_header__sent, it) },
|
sent = dateSent.let(::Date).toString().let { TitledText(R.string.sent, it) },
|
||||||
received = dateReceived.let(::Date).toString().let { TitledText(R.string.message_details_header__received, it) },
|
received = dateReceived.let(::Date).toString().let { TitledText(R.string.received, it) },
|
||||||
error = lokiMessageDatabase.getErrorMessage(id)?.let { TitledText(R.string.message_details_header__error, it) },
|
error = lokiMessageDatabase.getErrorMessage(id)?.let { TitledText(R.string.theError, it) },
|
||||||
senderInfo = individualRecipient.run { name?.let { TitledText(it, address.serialize()) } },
|
senderInfo = individualRecipient.run { name?.let { TitledText(it, address.serialize()) } },
|
||||||
sender = individualRecipient,
|
sender = individualRecipient,
|
||||||
thread = threadDb.getRecipientForThreadId(threadId)!!,
|
thread = threadDb.getRecipientForThreadId(threadId)!!,
|
||||||
@ -90,14 +90,14 @@ class MessageDetailsViewModel @Inject constructor(
|
|||||||
|
|
||||||
private val Slide.details: List<TitledText>
|
private val Slide.details: List<TitledText>
|
||||||
get() = listOfNotNull(
|
get() = listOfNotNull(
|
||||||
fileName.orNull()?.let { TitledText(R.string.message_details_header__file_id, it) },
|
fileName.orNull()?.let { TitledText(R.string.attachmentsFileId, it) },
|
||||||
TitledText(R.string.message_details_header__file_type, asAttachment().contentType),
|
TitledText(R.string.attachmentsFileType, asAttachment().contentType),
|
||||||
TitledText(R.string.message_details_header__file_size, Util.getPrettyFileSize(fileSize)),
|
TitledText(R.string.attachmentsFileSize, Util.getPrettyFileSize(fileSize)),
|
||||||
takeIf { it is ImageSlide }
|
takeIf { it is ImageSlide }
|
||||||
?.let(Slide::asAttachment)
|
?.let(Slide::asAttachment)
|
||||||
?.run { "${width}x$height" }
|
?.run { "${width}x$height" }
|
||||||
?.let { TitledText(R.string.message_details_header__resolution, it) },
|
?.let { TitledText(R.string.attachmentsResolution, it) },
|
||||||
attachmentDb.duration(this)?.let { TitledText(R.string.message_details_header__duration, it) },
|
attachmentDb.duration(this)?.let { TitledText(R.string.attachmentsDuration, it) },
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun AttachmentDatabase.duration(slide: Slide): String? =
|
private fun AttachmentDatabase.duration(slide: Slide): String? =
|
||||||
@ -157,7 +157,7 @@ data class MessageDetailsState(
|
|||||||
val sender: Recipient? = null,
|
val sender: Recipient? = null,
|
||||||
val thread: Recipient? = null,
|
val thread: Recipient? = null,
|
||||||
) {
|
) {
|
||||||
val fromTitle = GetString(R.string.message_details_header__from)
|
val fromTitle = GetString(R.string.from)
|
||||||
val canReply = record?.isOpenGroupInvitation != true
|
val canReply = record?.isOpenGroupInvitation != true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,9 +15,12 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
import com.squareup.phrase.Phrase
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.FragmentModalUrlBottomSheetBinding
|
import network.loki.messenger.databinding.FragmentModalUrlBottomSheetBinding
|
||||||
import org.thoughtcrime.securesms.util.UiModeUtilities
|
import org.session.libsession.utilities.StringSubstitutionConstants.URL_KEY
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
|
import org.thoughtcrime.securesms.ui.getSubbedString
|
||||||
|
|
||||||
class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(), View.OnClickListener {
|
class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(), View.OnClickListener {
|
||||||
private lateinit var binding: FragmentModalUrlBottomSheetBinding
|
private lateinit var binding: FragmentModalUrlBottomSheetBinding
|
||||||
@ -29,7 +32,8 @@ class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(),
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
val explanation = resources.getString(R.string.dialog_open_url_explanation, url)
|
if (context == null) { return Log.w("MUBS", "Context is null") }
|
||||||
|
val explanation = requireContext().getSubbedString(R.string.urlOpenDescription, URL_KEY to url)
|
||||||
val spannable = SpannableStringBuilder(explanation)
|
val spannable = SpannableStringBuilder(explanation)
|
||||||
val startIndex = explanation.indexOf(url)
|
val startIndex = explanation.indexOf(url)
|
||||||
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + url.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + url.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
@ -44,7 +48,7 @@ class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(),
|
|||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||||
requireContext().startActivity(intent)
|
requireContext().startActivity(intent)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Toast.makeText(context, R.string.invalid_url, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, R.string.communityEnterUrlErrorInvalid, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
@ -53,7 +57,7 @@ class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(),
|
|||||||
val clip = ClipData.newPlainText("URL", url)
|
val clip = ClipData.newPlainText("URL", url)
|
||||||
val manager = requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
val manager = requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
manager.setPrimaryClip(clip)
|
manager.setPrimaryClip(clip)
|
||||||
Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), R.string.copied, Toast.LENGTH_SHORT).show()
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,22 +17,12 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.v2
|
package org.thoughtcrime.securesms.conversation.v2
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Typeface
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.text.Spannable
|
|
||||||
import android.text.SpannableString
|
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.text.style.StyleSpan
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import com.annimon.stream.Stream
|
import com.annimon.stream.Stream
|
||||||
import com.google.android.mms.pdu_alt.CharacterSets
|
|
||||||
import com.google.android.mms.pdu_alt.EncodedStringValue
|
|
||||||
import org.session.libsignal.utilities.Log
|
|
||||||
import org.thoughtcrime.securesms.components.ComposeText
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.UnsupportedEncodingException
|
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
|
|
||||||
object Util {
|
object Util {
|
||||||
private val TAG: String = Log.tag(Util::class.java)
|
private val TAG: String = Log.tag(Util::class.java)
|
||||||
@ -92,22 +82,6 @@ object Util {
|
|||||||
return sb.toString()
|
return sb.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isEmpty(value: Array<EncodedStringValue?>?): Boolean {
|
|
||||||
return value == null || value.size == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isEmpty(value: ComposeText?): Boolean {
|
|
||||||
return value == null || value.text == null || TextUtils.isEmpty(value.textTrimmed)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isEmpty(collection: Collection<*>?): Boolean {
|
|
||||||
return collection == null || collection.isEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isEmpty(charSequence: CharSequence?): Boolean {
|
|
||||||
return charSequence == null || charSequence.length == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun wait(lock: Any, timeout: Long) {
|
fun wait(lock: Any, timeout: Long) {
|
||||||
try {
|
try {
|
||||||
(lock as Object).wait(timeout)
|
(lock as Object).wait(timeout)
|
||||||
@ -123,8 +97,7 @@ object Util {
|
|||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
val elements =
|
val elements = source.split(delimiter.toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||||
source.split(delimiter.toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
|
||||||
Collections.addAll(results, *elements)
|
Collections.addAll(results, *elements)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
@ -11,10 +11,12 @@ import android.widget.RelativeLayout
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import com.squareup.phrase.Phrase
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.AlbumThumbnailViewBinding
|
import network.loki.messenger.databinding.AlbumThumbnailViewBinding
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.COUNT_KEY
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.MediaPreviewActivity
|
import org.thoughtcrime.securesms.MediaPreviewActivity
|
||||||
import org.thoughtcrime.securesms.components.CornerMask
|
import org.thoughtcrime.securesms.components.CornerMask
|
||||||
@ -97,7 +99,10 @@ class AlbumThumbnailView : RelativeLayout {
|
|||||||
binding.albumCellContainer.findViewById<TextView>(R.id.album_cell_overflow_text)?.let { overflowText ->
|
binding.albumCellContainer.findViewById<TextView>(R.id.album_cell_overflow_text)?.let { overflowText ->
|
||||||
// overflowText will be null if !overflowed
|
// overflowText will be null if !overflowed
|
||||||
overflowText.isVisible = overflowed // more than max album size
|
overflowText.isVisible = overflowed // more than max album size
|
||||||
overflowText.text = context.getString(R.string.AlbumThumbnailView_plus, slides.size - MAX_ALBUM_DISPLAY_SIZE)
|
val txt = Phrase.from(context, R.string.andMore)
|
||||||
|
.put(COUNT_KEY, slides.size - MAX_ALBUM_DISPLAY_SIZE)
|
||||||
|
.format()
|
||||||
|
overflowText.text = txt
|
||||||
}
|
}
|
||||||
this.slideSize = slides.size
|
this.slideSize = slides.size
|
||||||
}
|
}
|
||||||
@ -110,10 +115,9 @@ class AlbumThumbnailView : RelativeLayout {
|
|||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
|
||||||
fun layoutRes(slideCount: Int) = when (slideCount) {
|
fun layoutRes(slideCount: Int) = when (slideCount) {
|
||||||
1 -> R.layout.album_thumbnail_1 // single
|
1 -> R.layout.album_thumbnail_1 // single
|
||||||
2 -> R.layout.album_thumbnail_2// two sidebyside
|
2 -> R.layout.album_thumbnail_2 // two side-by-side
|
||||||
else -> R.layout.album_thumbnail_3 // three stacked with additional text
|
else -> R.layout.album_thumbnail_3 // three stacked with additional text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,9 +11,11 @@ import androidx.fragment.app.DialogFragment
|
|||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.createSessionDialog
|
import org.thoughtcrime.securesms.createSessionDialog
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
|
import org.thoughtcrime.securesms.ui.getSubbedCharSequence
|
||||||
|
|
||||||
/** Shown upon sending a message to a user that's blocked. */
|
/** Shown upon sending a message to a user that's blocked. */
|
||||||
class BlockedDialog(private val recipient: Recipient, private val context: Context) : DialogFragment() {
|
class BlockedDialog(private val recipient: Recipient, private val context: Context) : DialogFragment() {
|
||||||
@ -24,14 +26,14 @@ class BlockedDialog(private val recipient: Recipient, private val context: Conte
|
|||||||
val contact = contactDB.getContactWithAccountID(accountID)
|
val contact = contactDB.getContactWithAccountID(accountID)
|
||||||
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: accountID
|
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: accountID
|
||||||
|
|
||||||
val explanation = resources.getString(R.string.dialog_blocked_explanation, name)
|
val explanationCS = context.getSubbedCharSequence(R.string.blockUnblockName, NAME_KEY to name)
|
||||||
val spannable = SpannableStringBuilder(explanation)
|
val spannable = SpannableStringBuilder(explanationCS)
|
||||||
val startIndex = explanation.indexOf(name)
|
val startIndex = explanationCS.indexOf(name)
|
||||||
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
|
||||||
title(resources.getString(R.string.dialog_blocked_title, name))
|
title(resources.getString(R.string.blockUnblock))
|
||||||
text(spannable)
|
text(spannable)
|
||||||
button(R.string.ConversationActivity_unblock) { unblock() }
|
dangerButton(R.string.blockUnblock, R.string.AccessibilityId_unblockConfirm) { unblock() }
|
||||||
cancelButton { dismiss() }
|
cancelButton { dismiss() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,12 +7,14 @@ import android.text.Spannable
|
|||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.style.StyleSpan
|
import android.text.style.StyleSpan
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.squareup.phrase.Phrase
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
|
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
|
||||||
import org.session.libsession.messaging.jobs.JobQueue
|
import org.session.libsession.messaging.jobs.JobQueue
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.CONVERSATION_NAME_KEY
|
||||||
import org.thoughtcrime.securesms.createSessionDialog
|
import org.thoughtcrime.securesms.createSessionDialog
|
||||||
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
@ -29,15 +31,19 @@ class DownloadDialog(private val recipient: Recipient) : DialogFragment() {
|
|||||||
val accountID = recipient.address.toString()
|
val accountID = recipient.address.toString()
|
||||||
val contact = contactDB.getContactWithAccountID(accountID)
|
val contact = contactDB.getContactWithAccountID(accountID)
|
||||||
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: accountID
|
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: accountID
|
||||||
title(resources.getString(R.string.dialog_download_title, name))
|
|
||||||
|
|
||||||
val explanation = resources.getString(R.string.dialog_download_explanation, name)
|
title(getString(R.string.attachmentsAutoDownloadModalTitle))
|
||||||
|
|
||||||
|
val explanation = Phrase.from(context, R.string.attachmentsAutoDownloadModalDescription)
|
||||||
|
.put(CONVERSATION_NAME_KEY, recipient.name)
|
||||||
|
.format()
|
||||||
val spannable = SpannableStringBuilder(explanation)
|
val spannable = SpannableStringBuilder(explanation)
|
||||||
|
|
||||||
val startIndex = explanation.indexOf(name)
|
val startIndex = explanation.indexOf(name)
|
||||||
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
text(spannable)
|
text(spannable)
|
||||||
|
|
||||||
button(R.string.dialog_download_button_title, R.string.AccessibilityId_download_media) { trust() }
|
button(R.string.download, R.string.AccessibilityId_download) { trust() }
|
||||||
cancelButton { dismiss() }
|
cancelButton { dismiss() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.v2.dialogs
|
package org.thoughtcrime.securesms.conversation.v2.dialogs
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.createSessionDialog
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@ -8,11 +9,13 @@ import android.text.SpannableStringBuilder
|
|||||||
import android.text.style.StyleSpan
|
import android.text.style.StyleSpan
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.squareup.phrase.Phrase
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.utilities.OpenGroupUrlParser
|
import org.session.libsession.utilities.OpenGroupUrlParser
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.COMMUNITY_NAME_KEY
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
import org.session.libsignal.utilities.ThreadUtils
|
import org.session.libsignal.utilities.ThreadUtils
|
||||||
import org.thoughtcrime.securesms.createSessionDialog
|
|
||||||
import org.thoughtcrime.securesms.groups.OpenGroupManager
|
import org.thoughtcrime.securesms.groups.OpenGroupManager
|
||||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||||
|
|
||||||
@ -20,14 +23,18 @@ import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
|||||||
class JoinOpenGroupDialog(private val name: String, private val url: String) : DialogFragment() {
|
class JoinOpenGroupDialog(private val name: String, private val url: String) : DialogFragment() {
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
|
||||||
title(resources.getString(R.string.dialog_join_open_group_title, name))
|
title(resources.getString(R.string.communityJoin))
|
||||||
val explanation = resources.getString(R.string.dialog_join_open_group_explanation, name)
|
val explanation = Phrase.from(context, R.string.communityJoinDescription).put(COMMUNITY_NAME_KEY, name).format()
|
||||||
val spannable = SpannableStringBuilder(explanation)
|
val spannable = SpannableStringBuilder(explanation)
|
||||||
val startIndex = explanation.indexOf(name)
|
var startIndex = explanation.indexOf(name)
|
||||||
|
if (startIndex < 0) {
|
||||||
|
Log.w("JoinOpenGroupDialog", "Could not find $name in explanation dialog: $explanation")
|
||||||
|
startIndex = 0 // Limit the startIndex to zero if not found (will be -1) to prevent a crash
|
||||||
|
}
|
||||||
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
text(spannable)
|
text(spannable)
|
||||||
cancelButton { dismiss() }
|
cancelButton { dismiss() }
|
||||||
button(R.string.open_group_invitation_view__join_accessibility_description) { join() }
|
button(R.string.join) { join() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun join() {
|
private fun join() {
|
||||||
@ -39,7 +46,7 @@ class JoinOpenGroupDialog(private val name: String, private val url: String) : D
|
|||||||
MessagingModuleConfiguration.shared.storage.onOpenGroupAdded(openGroup.server, openGroup.room)
|
MessagingModuleConfiguration.shared.storage.onOpenGroupAdded(openGroup.server, openGroup.room)
|
||||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(activity)
|
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(activity)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Toast.makeText(activity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
|
Toast.makeText(activity, R.string.communityErrorDescription, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dismiss()
|
dismiss()
|
||||||
|
@ -4,18 +4,22 @@ import android.app.Dialog
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.utilities.NonTranslatableStringConstants.APP_NAME
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.thoughtcrime.securesms.createSessionDialog
|
import org.thoughtcrime.securesms.createSessionDialog
|
||||||
|
import org.thoughtcrime.securesms.ui.getSubbedCharSequence
|
||||||
|
|
||||||
/** Shown the first time the user inputs a URL that could generate a link preview, to
|
/** Shown the first time the user inputs a URL that could generate a link preview, to
|
||||||
* let them know that Session offers the ability to send and receive link previews. */
|
* let them know that Session offers the ability to send and receive link previews. */
|
||||||
class LinkPreviewDialog(private val onEnabled: () -> Unit) : DialogFragment() {
|
class LinkPreviewDialog(private val onEnabled: () -> Unit) : DialogFragment() {
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
|
||||||
title(R.string.dialog_link_preview_title)
|
title(R.string.linkPreviewsEnable)
|
||||||
text(R.string.dialog_link_preview_explanation)
|
val txt = context.getSubbedCharSequence(R.string.linkPreviewsFirstDescription, APP_NAME_KEY to APP_NAME)
|
||||||
button(R.string.dialog_link_preview_enable_button_title) { enable() }
|
text(txt)
|
||||||
cancelButton { dismiss() }
|
button(R.string.enable) { enable() }
|
||||||
|
cancelButton { dismiss() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun enable() {
|
private fun enable() {
|
||||||
|
@ -79,9 +79,9 @@ class InputBar @JvmOverloads constructor(
|
|||||||
var voiceMessageDurationMS = 0L
|
var voiceMessageDurationMS = 0L
|
||||||
var voiceRecorderState = VoiceRecorderState.Idle
|
var voiceRecorderState = VoiceRecorderState.Idle
|
||||||
|
|
||||||
private val attachmentsButton = InputBarButton(context, R.drawable.ic_plus_24).apply { contentDescription = context.getString(R.string.AccessibilityId_attachments_button)}
|
private val attachmentsButton = InputBarButton(context, R.drawable.ic_plus_24).apply { contentDescription = context.getString(R.string.AccessibilityId_attachmentsButton)}
|
||||||
val microphoneButton = InputBarButton(context, R.drawable.ic_microphone).apply { contentDescription = context.getString(R.string.AccessibilityId_microphone_button)}
|
val microphoneButton = InputBarButton(context, R.drawable.ic_microphone).apply { contentDescription = context.getString(R.string.AccessibilityId_voiceMessageNew)}
|
||||||
private val sendButton = InputBarButton(context, R.drawable.ic_arrow_up, true).apply { contentDescription = context.getString(R.string.AccessibilityId_send_message_button)}
|
private val sendButton = InputBarButton(context, R.drawable.ic_arrow_up, true).apply { contentDescription = context.getString(R.string.AccessibilityId_send)}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Attachments button
|
// Attachments button
|
||||||
|
@ -19,7 +19,6 @@ import kotlinx.coroutines.isActive
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ViewInputBarRecordingBinding
|
import network.loki.messenger.databinding.ViewInputBarRecordingBinding
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
|
||||||
import org.thoughtcrime.securesms.util.animateSizeChange
|
import org.thoughtcrime.securesms.util.animateSizeChange
|
||||||
import org.thoughtcrime.securesms.util.disableClipping
|
import org.thoughtcrime.securesms.util.disableClipping
|
||||||
import org.thoughtcrime.securesms.util.toPx
|
import org.thoughtcrime.securesms.util.toPx
|
||||||
@ -106,8 +105,7 @@ class InputBarRecordingView : RelativeLayout {
|
|||||||
timerJob = scope.launch {
|
timerJob = scope.launch {
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
val duration = (Date().time - startTimestamp) / 1000L
|
val duration = (Date().time - startTimestamp) / 1000L
|
||||||
binding.recordingViewDurationTextView.text = DateUtils.formatElapsedTime(duration)
|
binding.recordingViewDurationTextView.text = android.text.format.DateUtils.formatElapsedTime(duration)
|
||||||
|
|
||||||
delay(500)
|
delay(500)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,8 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
|
|||||||
val edKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!
|
val edKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!
|
||||||
val blindedPublicKey = openGroup?.publicKey?.let { SodiumUtilities.blindedKeyPair(it, edKeyPair)?.publicKey?.asBytes }
|
val blindedPublicKey = openGroup?.publicKey?.let { SodiumUtilities.blindedKeyPair(it, edKeyPair)?.publicKey?.asBytes }
|
||||||
?.let { AccountId(IdPrefix.BLINDED, it) }?.hexString
|
?.let { AccountId(IdPrefix.BLINDED, it) }?.hexString
|
||||||
|
|
||||||
|
// Embedded function
|
||||||
fun userCanDeleteSelectedItems(): Boolean {
|
fun userCanDeleteSelectedItems(): Boolean {
|
||||||
val allSentByCurrentUser = selectedItems.all { it.isOutgoing }
|
val allSentByCurrentUser = selectedItems.all { it.isOutgoing }
|
||||||
val allReceivedByCurrentUser = selectedItems.all { !it.isOutgoing }
|
val allReceivedByCurrentUser = selectedItems.all { !it.isOutgoing }
|
||||||
@ -47,6 +49,8 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
|
|||||||
if (allSentByCurrentUser) { return true }
|
if (allSentByCurrentUser) { return true }
|
||||||
return OpenGroupManager.isUserModerator(context, openGroup.groupId, userPublicKey, blindedPublicKey)
|
return OpenGroupManager.isUserModerator(context, openGroup.groupId, userPublicKey, blindedPublicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Embedded function
|
||||||
fun userCanBanSelectedUsers(): Boolean {
|
fun userCanBanSelectedUsers(): Boolean {
|
||||||
if (openGroup == null) { return false }
|
if (openGroup == null) { return false }
|
||||||
val anySentByCurrentUser = selectedItems.any { it.isOutgoing }
|
val anySentByCurrentUser = selectedItems.any { it.isOutgoing }
|
||||||
@ -55,6 +59,9 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
|
|||||||
if (selectedUsers.size > 1) { return false }
|
if (selectedUsers.size > 1) { return false }
|
||||||
return OpenGroupManager.isUserModerator(context, openGroup.groupId, userPublicKey, blindedPublicKey)
|
return OpenGroupManager.isUserModerator(context, openGroup.groupId, userPublicKey, blindedPublicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Delete message
|
// Delete message
|
||||||
menu.findItem(R.id.menu_context_delete_message).isVisible = userCanDeleteSelectedItems()
|
menu.findItem(R.id.menu_context_delete_message).isVisible = userCanDeleteSelectedItems()
|
||||||
// Ban user
|
// Ban user
|
||||||
|
@ -16,10 +16,13 @@ import androidx.appcompat.widget.SearchView.OnQueryTextListener
|
|||||||
import androidx.core.content.pm.ShortcutInfoCompat
|
import androidx.core.content.pm.ShortcutInfoCompat
|
||||||
import androidx.core.content.pm.ShortcutManagerCompat
|
import androidx.core.content.pm.ShortcutManagerCompat
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
|
import com.squareup.phrase.Phrase
|
||||||
|
import java.io.IOException
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||||
import org.session.libsession.messaging.sending_receiving.leave
|
import org.session.libsession.messaging.sending_receiving.leave
|
||||||
import org.session.libsession.utilities.GroupUtil.doubleDecodeGroupID
|
import org.session.libsession.utilities.GroupUtil.doubleDecodeGroupID
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsignal.utilities.guava.Optional
|
import org.session.libsignal.utilities.guava.Optional
|
||||||
@ -38,7 +41,6 @@ import org.thoughtcrime.securesms.service.WebRtcCallService
|
|||||||
import org.thoughtcrime.securesms.showMuteDialog
|
import org.thoughtcrime.securesms.showMuteDialog
|
||||||
import org.thoughtcrime.securesms.showSessionDialog
|
import org.thoughtcrime.securesms.showSessionDialog
|
||||||
import org.thoughtcrime.securesms.util.BitmapUtil
|
import org.thoughtcrime.securesms.util.BitmapUtil
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
object ConversationMenuHelper {
|
object ConversationMenuHelper {
|
||||||
|
|
||||||
@ -50,11 +52,11 @@ object ConversationMenuHelper {
|
|||||||
) {
|
) {
|
||||||
// Prepare
|
// Prepare
|
||||||
menu.clear()
|
menu.clear()
|
||||||
val isOpenGroup = thread.isCommunityRecipient
|
val isCommunity = thread.isCommunityRecipient
|
||||||
// Base menu (options that should always be present)
|
// Base menu (options that should always be present)
|
||||||
inflater.inflate(R.menu.menu_conversation, menu)
|
inflater.inflate(R.menu.menu_conversation, menu)
|
||||||
// Expiring messages
|
// Expiring messages
|
||||||
if (!isOpenGroup && (thread.hasApprovedMe() || thread.isClosedGroupRecipient || thread.isLocalNumber)) {
|
if (!isCommunity && (thread.hasApprovedMe() || thread.isClosedGroupRecipient || thread.isLocalNumber)) {
|
||||||
inflater.inflate(R.menu.menu_conversation_expiration, menu)
|
inflater.inflate(R.menu.menu_conversation_expiration, menu)
|
||||||
}
|
}
|
||||||
// One-on-one chat menu allows copying the account id
|
// One-on-one chat menu allows copying the account id
|
||||||
@ -74,7 +76,7 @@ object ConversationMenuHelper {
|
|||||||
inflater.inflate(R.menu.menu_conversation_closed_group, menu)
|
inflater.inflate(R.menu.menu_conversation_closed_group, menu)
|
||||||
}
|
}
|
||||||
// Open group menu
|
// Open group menu
|
||||||
if (isOpenGroup) {
|
if (isCommunity) {
|
||||||
inflater.inflate(R.menu.menu_conversation_open_group, menu)
|
inflater.inflate(R.menu.menu_conversation_open_group, menu)
|
||||||
}
|
}
|
||||||
// Muting
|
// Muting
|
||||||
@ -162,9 +164,9 @@ object ConversationMenuHelper {
|
|||||||
|
|
||||||
if (!TextSecurePreferences.isCallNotificationsEnabled(context)) {
|
if (!TextSecurePreferences.isCallNotificationsEnabled(context)) {
|
||||||
context.showSessionDialog {
|
context.showSessionDialog {
|
||||||
title(R.string.ConversationActivity_call_title)
|
title(R.string.callsPermissionsRequired)
|
||||||
text(R.string.ConversationActivity_call_prompt)
|
text(R.string.callsPermissionsRequiredDescription)
|
||||||
button(R.string.activity_settings_title, R.string.AccessibilityId_settings) {
|
button(R.string.sessionSettings, R.string.AccessibilityId_sessionSettings) {
|
||||||
Intent(context, PrivacySettingsActivity::class.java).let(context::startActivity)
|
Intent(context, PrivacySettingsActivity::class.java).let(context::startActivity)
|
||||||
}
|
}
|
||||||
cancelButton()
|
cancelButton()
|
||||||
@ -178,7 +180,6 @@ object ConversationMenuHelper {
|
|||||||
Intent(context, WebRtcCallActivity::class.java)
|
Intent(context, WebRtcCallActivity::class.java)
|
||||||
.apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK }
|
.apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK }
|
||||||
.let(context::startActivity)
|
.let(context::startActivity)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
@ -215,7 +216,7 @@ object ConversationMenuHelper {
|
|||||||
.setIntent(ShortcutLauncherActivity.createIntent(context, thread.address))
|
.setIntent(ShortcutLauncherActivity.createIntent(context, thread.address))
|
||||||
.build()
|
.build()
|
||||||
if (ShortcutManagerCompat.requestPinShortcut(context, shortcutInfo, null)) {
|
if (ShortcutManagerCompat.requestPinShortcut(context, shortcutInfo, null)) {
|
||||||
Toast.makeText(context, context.resources.getString(R.string.ConversationActivity_added_to_home_screen), Toast.LENGTH_LONG).show()
|
Toast.makeText(context, context.resources.getString(R.string.conversationsAddedToHome), Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.execute()
|
}.execute()
|
||||||
@ -272,15 +273,24 @@ object ConversationMenuHelper {
|
|||||||
val accountID = TextSecurePreferences.getLocalNumber(context)
|
val accountID = TextSecurePreferences.getLocalNumber(context)
|
||||||
val isCurrentUserAdmin = admins.any { it.toString() == accountID }
|
val isCurrentUserAdmin = admins.any { it.toString() == accountID }
|
||||||
val message = if (isCurrentUserAdmin) {
|
val message = if (isCurrentUserAdmin) {
|
||||||
"Because you are the creator of this group it will be deleted for everyone. This cannot be undone."
|
Phrase.from(context, R.string.groupLeaveDescriptionAdmin)
|
||||||
|
.put(GROUP_NAME_KEY, group.title)
|
||||||
|
.format().toString()
|
||||||
} else {
|
} else {
|
||||||
context.resources.getString(R.string.ConversationActivity_are_you_sure_you_want_to_leave_this_group)
|
Phrase.from(context, R.string.groupLeaveDescription)
|
||||||
|
.put(GROUP_NAME_KEY, group.title)
|
||||||
|
.format().toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onLeaveFailed() = Toast.makeText(context, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show()
|
fun onLeaveFailed() {
|
||||||
|
val txt = Phrase.from(context, R.string.groupLeaveErrorFailed)
|
||||||
|
.put(GROUP_NAME_KEY, group.title)
|
||||||
|
.format().toString()
|
||||||
|
Toast.makeText(context, txt, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
context.showSessionDialog {
|
context.showSessionDialog {
|
||||||
title(R.string.ConversationActivity_leave_group)
|
title(R.string.groupLeave)
|
||||||
text(message)
|
text(message)
|
||||||
button(R.string.yes) {
|
button(R.string.yes) {
|
||||||
try {
|
try {
|
||||||
@ -309,7 +319,7 @@ object ConversationMenuHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun mute(context: Context, thread: Recipient) {
|
private fun mute(context: Context, thread: Recipient) {
|
||||||
showMuteDialog(ContextThemeWrapper(context, context.theme)) { until ->
|
showMuteDialog(ContextThemeWrapper(context, context.theme)) { until: Long ->
|
||||||
DatabaseComponent.get(context).recipientDatabase().setMuted(thread, until)
|
DatabaseComponent.get(context).recipientDatabase().setMuted(thread, until)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,17 +8,19 @@ import androidx.core.content.res.ResourcesCompat
|
|||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.squareup.phrase.Phrase
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import javax.inject.Inject
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ViewControlMessageBinding
|
import network.loki.messenger.databinding.ViewControlMessageBinding
|
||||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.messages.ExpirationConfiguration
|
import org.session.libsession.messaging.messages.ExpirationConfiguration
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY
|
||||||
import org.thoughtcrime.securesms.conversation.disappearingmessages.DisappearingMessages
|
import org.thoughtcrime.securesms.conversation.disappearingmessages.DisappearingMessages
|
||||||
import org.thoughtcrime.securesms.conversation.disappearingmessages.expiryMode
|
import org.thoughtcrime.securesms.conversation.disappearingmessages.expiryMode
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ControlMessageView : LinearLayout {
|
class ControlMessageView : LinearLayout {
|
||||||
@ -75,8 +77,10 @@ class ControlMessageView : LinearLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
message.isMessageRequestResponse -> {
|
message.isMessageRequestResponse -> {
|
||||||
binding.textView.text = context.getString(R.string.message_requests_accepted)
|
binding.textView.text = context.getString(R.string.messageRequestsAccepted)
|
||||||
binding.root.contentDescription=context.getString(R.string.AccessibilityId_message_request_config_message)
|
binding.root.contentDescription = Phrase.from(context, R.string.messageRequestYouHaveAccepted)
|
||||||
|
.put(NAME_KEY, message.individualRecipient.name)
|
||||||
|
.format()
|
||||||
}
|
}
|
||||||
message.isCallLog -> {
|
message.isCallLog -> {
|
||||||
val drawable = when {
|
val drawable = when {
|
||||||
|
@ -21,7 +21,7 @@ class DeletedMessageView : LinearLayout {
|
|||||||
// region Updating
|
// region Updating
|
||||||
fun bind(message: MessageRecord, @ColorInt textColor: Int) {
|
fun bind(message: MessageRecord, @ColorInt textColor: Int) {
|
||||||
assert(message.isDeleted)
|
assert(message.isDeleted)
|
||||||
binding.deleteTitleTextView.text = context.getString(R.string.deleted_message)
|
binding.deleteTitleTextView.text = context.resources.getQuantityString(R.plurals.deleteMessageDeleted, 1, 1)
|
||||||
binding.deleteTitleTextView.setTextColor(textColor)
|
binding.deleteTitleTextView.setTextColor(textColor)
|
||||||
binding.deletedMessageViewIconImageView.imageTintList = ColorStateList.valueOf(textColor)
|
binding.deletedMessageViewIconImageView.imageTintList = ColorStateList.valueOf(textColor)
|
||||||
}
|
}
|
||||||
|
@ -12,8 +12,10 @@ import androidx.constraintlayout.widget.ConstraintLayout
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.setPadding
|
import androidx.core.view.setPadding
|
||||||
import com.google.android.flexbox.JustifyContent
|
import com.google.android.flexbox.JustifyContent
|
||||||
|
import com.squareup.phrase.Phrase
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ViewEmojiReactionsBinding
|
import network.loki.messenger.databinding.ViewEmojiReactionsBinding
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.COUNT_KEY
|
||||||
import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber
|
import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber
|
||||||
import org.session.libsession.utilities.ThemeUtil
|
import org.session.libsession.utilities.ThemeUtil
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
import org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||||
@ -199,7 +201,7 @@ class EmojiReactionsView : ConstraintLayout, OnTouchListener {
|
|||||||
} else {
|
} else {
|
||||||
emojiView.visibility = GONE
|
emojiView.visibility = GONE
|
||||||
spacer.visibility = GONE
|
spacer.visibility = GONE
|
||||||
countView.text = context.getString(R.string.ReactionsConversationView_plus, reaction.count)
|
countView.text = Phrase.from(context, R.string.andMore).put(COUNT_KEY, reaction.count.toInt()).format()
|
||||||
}
|
}
|
||||||
if (reaction.userWasSender && !isCompact) {
|
if (reaction.userWasSender && !isCompact) {
|
||||||
root.background = ContextCompat.getDrawable(context, R.drawable.reaction_pill_background_selected)
|
root.background = ContextCompat.getDrawable(context, R.drawable.reaction_pill_background_selected)
|
||||||
|
@ -6,16 +6,16 @@ import android.graphics.Rect
|
|||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import com.bumptech.glide.RequestManager
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ViewLinkPreviewBinding
|
import network.loki.messenger.databinding.ViewLinkPreviewBinding
|
||||||
import org.session.libsession.utilities.getColorFromAttr
|
import org.session.libsession.utilities.getColorFromAttr
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.components.CornerMask
|
import org.thoughtcrime.securesms.components.CornerMask
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ModalUrlBottomSheet
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities
|
import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||||
import com.bumptech.glide.RequestManager
|
|
||||||
import org.thoughtcrime.securesms.mms.ImageSlide
|
import org.thoughtcrime.securesms.mms.ImageSlide
|
||||||
|
|
||||||
class LinkPreviewView : LinearLayout {
|
class LinkPreviewView : LinearLayout {
|
||||||
@ -84,10 +84,11 @@ class LinkPreviewView : LinearLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openURL() {
|
// Method to show the open or copy URL dialog
|
||||||
val url = this.url ?: return
|
private fun openURL() {
|
||||||
val activity = context as AppCompatActivity
|
val url = this.url ?: return Log.w("LinkPreviewView", "Cannot open a null URL")
|
||||||
ModalUrlBottomSheet(url).show(activity.supportFragmentManager, "Open URL Dialog")
|
val activity = context as? ConversationActivityV2
|
||||||
|
activity?.showOpenUrlDialog(url)
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
}
|
}
|
@ -75,13 +75,13 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
|
|||||||
val quoteIsLocalUser = localNumber != null && authorPublicKey == localNumber
|
val quoteIsLocalUser = localNumber != null && authorPublicKey == localNumber
|
||||||
|
|
||||||
val authorDisplayName =
|
val authorDisplayName =
|
||||||
if (quoteIsLocalUser) context.getString(R.string.QuoteView_you)
|
if (quoteIsLocalUser) context.getString(R.string.you)
|
||||||
else author?.displayName(Contact.contextForRecipient(thread)) ?: "${authorPublicKey.take(4)}...${authorPublicKey.takeLast(4)}"
|
else author?.displayName(Contact.contextForRecipient(thread)) ?: "${authorPublicKey.take(4)}...${authorPublicKey.takeLast(4)}"
|
||||||
binding.quoteViewAuthorTextView.text = authorDisplayName
|
binding.quoteViewAuthorTextView.text = authorDisplayName
|
||||||
binding.quoteViewAuthorTextView.setTextColor(getTextColor(isOutgoingMessage))
|
binding.quoteViewAuthorTextView.setTextColor(getTextColor(isOutgoingMessage))
|
||||||
// Body
|
// Body
|
||||||
binding.quoteViewBodyTextView.text = if (isOpenGroupInvitation)
|
binding.quoteViewBodyTextView.text = if (isOpenGroupInvitation)
|
||||||
resources.getString(R.string.open_group_invitation_view__open_group_invitation)
|
resources.getString(R.string.communityInvitation)
|
||||||
else MentionUtilities.highlightMentions(
|
else MentionUtilities.highlightMentions(
|
||||||
text = (body ?: "").toSpannable(),
|
text = (body ?: "").toSpannable(),
|
||||||
isOutgoingMessage = isOutgoingMessage,
|
isOutgoingMessage = isOutgoingMessage,
|
||||||
@ -106,7 +106,7 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
|
|||||||
attachments.audioSlide != null -> {
|
attachments.audioSlide != null -> {
|
||||||
binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone)
|
binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone)
|
||||||
binding.quoteViewAttachmentPreviewImageView.isVisible = true
|
binding.quoteViewAttachmentPreviewImageView.isVisible = true
|
||||||
binding.quoteViewBodyTextView.text = resources.getString(R.string.Slide_audio)
|
binding.quoteViewBodyTextView.text = resources.getString(R.string.audio)
|
||||||
}
|
}
|
||||||
attachments.documentSlide != null -> {
|
attachments.documentSlide != null -> {
|
||||||
binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_document_large_light)
|
binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_document_large_light)
|
||||||
@ -120,7 +120,7 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
|
|||||||
.root.setRoundedCorners(toPx(4, resources))
|
.root.setRoundedCorners(toPx(4, resources))
|
||||||
binding.quoteViewAttachmentThumbnailImageView.root.setImageResource(glide, slide, false)
|
binding.quoteViewAttachmentThumbnailImageView.root.setImageResource(glide, slide, false)
|
||||||
binding.quoteViewAttachmentThumbnailImageView.root.isVisible = true
|
binding.quoteViewAttachmentThumbnailImageView.root.isVisible = true
|
||||||
binding.quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.Slide_video) else resources.getString(R.string.Slide_image)
|
binding.quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.video) else resources.getString(R.string.image)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,12 +5,13 @@ import android.util.AttributeSet
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.squareup.phrase.Phrase
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ViewUntrustedAttachmentBinding
|
import network.loki.messenger.databinding.ViewUntrustedAttachmentBinding
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.FILE_TYPE_KEY
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.conversation.v2.dialogs.DownloadDialog
|
import org.thoughtcrime.securesms.conversation.v2.dialogs.DownloadDialog
|
||||||
import org.thoughtcrime.securesms.util.ActivityDispatcher
|
import org.thoughtcrime.securesms.util.ActivityDispatcher
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class UntrustedAttachmentView: LinearLayout {
|
class UntrustedAttachmentView: LinearLayout {
|
||||||
private val binding: ViewUntrustedAttachmentBinding by lazy { ViewUntrustedAttachmentBinding.bind(this) }
|
private val binding: ViewUntrustedAttachmentBinding by lazy { ViewUntrustedAttachmentBinding.bind(this) }
|
||||||
@ -30,13 +31,17 @@ class UntrustedAttachmentView: LinearLayout {
|
|||||||
// region Updating
|
// region Updating
|
||||||
fun bind(attachmentType: AttachmentType, @ColorInt textColor: Int) {
|
fun bind(attachmentType: AttachmentType, @ColorInt textColor: Int) {
|
||||||
val (iconRes, stringRes) = when (attachmentType) {
|
val (iconRes, stringRes) = when (attachmentType) {
|
||||||
AttachmentType.AUDIO -> R.drawable.ic_microphone to R.string.Slide_audio
|
AttachmentType.AUDIO -> R.drawable.ic_microphone to R.string.audio
|
||||||
AttachmentType.DOCUMENT -> R.drawable.ic_document_large_light to R.string.document
|
AttachmentType.DOCUMENT -> R.drawable.ic_document_large_light to R.string.files
|
||||||
AttachmentType.MEDIA -> R.drawable.ic_image_white_24dp to R.string.media
|
AttachmentType.MEDIA -> R.drawable.ic_image_white_24dp to R.string.media
|
||||||
}
|
}
|
||||||
val iconDrawable = ContextCompat.getDrawable(context,iconRes)!!
|
val iconDrawable = ContextCompat.getDrawable(context,iconRes)!!
|
||||||
iconDrawable.mutate().setTint(textColor)
|
iconDrawable.mutate().setTint(textColor)
|
||||||
val text = context.getString(R.string.UntrustedAttachmentView_download_attachment, context.getString(stringRes).toLowerCase(Locale.ROOT))
|
|
||||||
|
val text = Phrase.from(context, R.string.attachmentsTapToDownload)
|
||||||
|
.put(FILE_TYPE_KEY, context.getString(stringRes))
|
||||||
|
.format()
|
||||||
|
binding.untrustedAttachmentTitle.text = text
|
||||||
|
|
||||||
binding.untrustedAttachmentIcon.setImageDrawable(iconDrawable)
|
binding.untrustedAttachmentIcon.setImageDrawable(iconDrawable)
|
||||||
binding.untrustedAttachmentTitle.text = text
|
binding.untrustedAttachmentTitle.text = text
|
||||||
|
@ -12,34 +12,29 @@ import android.util.AttributeSet
|
|||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.graphics.ColorUtils
|
import androidx.core.graphics.ColorUtils
|
||||||
import androidx.core.text.getSpans
|
import androidx.core.text.getSpans
|
||||||
import androidx.core.text.toSpannable
|
import androidx.core.text.toSpannable
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.RequestManager
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ViewVisibleMessageContentBinding
|
import network.loki.messenger.databinding.ViewVisibleMessageContentBinding
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||||
import org.session.libsession.utilities.ThemeUtil
|
import org.session.libsession.utilities.ThemeUtil
|
||||||
import org.session.libsession.utilities.getColorFromAttr
|
import org.session.libsession.utilities.getColorFromAttr
|
||||||
import org.session.libsession.utilities.modifyLayoutParams
|
import org.session.libsession.utilities.modifyLayoutParams
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ModalUrlBottomSheet
|
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
|
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.ModalURLSpan
|
import org.thoughtcrime.securesms.conversation.v2.utilities.ModalURLSpan
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans
|
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord
|
import org.thoughtcrime.securesms.database.model.SmsMessageRecord
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.bumptech.glide.RequestManager
|
|
||||||
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
||||||
import org.thoughtcrime.securesms.util.SearchUtil
|
import org.thoughtcrime.securesms.util.SearchUtil
|
||||||
import org.thoughtcrime.securesms.util.getAccentColor
|
import org.thoughtcrime.securesms.util.getAccentColor
|
||||||
@ -117,7 +112,7 @@ class VisibleMessageContentView : ConstraintLayout {
|
|||||||
binding.quoteView.root.isVisible = true
|
binding.quoteView.root.isVisible = true
|
||||||
val quote = message.quote!!
|
val quote = message.quote!!
|
||||||
val quoteText = if (quote.isOriginalMissing) {
|
val quoteText = if (quote.isOriginalMissing) {
|
||||||
context.getString(R.string.QuoteView_original_missing)
|
context.getString(R.string.messageErrorOriginal)
|
||||||
} else {
|
} else {
|
||||||
quote.text
|
quote.text
|
||||||
}
|
}
|
||||||
@ -292,8 +287,8 @@ class VisibleMessageContentView : ConstraintLayout {
|
|||||||
body.getSpans<URLSpan>(0, body.length).toList().forEach { urlSpan ->
|
body.getSpans<URLSpan>(0, body.length).toList().forEach { urlSpan ->
|
||||||
val updatedUrl = urlSpan.url.let { it.toHttpUrlOrNull().toString() }
|
val updatedUrl = urlSpan.url.let { it.toHttpUrlOrNull().toString() }
|
||||||
val replacementSpan = ModalURLSpan(updatedUrl) { url ->
|
val replacementSpan = ModalURLSpan(updatedUrl) { url ->
|
||||||
val activity = context as AppCompatActivity
|
val activity = context as? ConversationActivityV2
|
||||||
ModalUrlBottomSheet(url).show(activity.supportFragmentManager, "Open URL Dialog")
|
activity?.showOpenUrlDialog(url)
|
||||||
}
|
}
|
||||||
val start = body.getSpanStart(urlSpan)
|
val start = body.getSpanStart(urlSpan)
|
||||||
val end = body.getSpanEnd(urlSpan)
|
val end = body.getSpanEnd(urlSpan)
|
||||||
|
@ -27,6 +27,13 @@ import androidx.core.os.bundleOf
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.marginBottom
|
import androidx.core.view.marginBottom
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
import kotlin.math.sqrt
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ViewEmojiReactionsBinding
|
import network.loki.messenger.databinding.ViewEmojiReactionsBinding
|
||||||
import network.loki.messenger.databinding.ViewVisibleMessageBinding
|
import network.loki.messenger.databinding.ViewVisibleMessageBinding
|
||||||
@ -58,13 +65,6 @@ import org.thoughtcrime.securesms.util.DateUtils
|
|||||||
import org.thoughtcrime.securesms.util.disableClipping
|
import org.thoughtcrime.securesms.util.disableClipping
|
||||||
import org.thoughtcrime.securesms.util.toDp
|
import org.thoughtcrime.securesms.util.toDp
|
||||||
import org.thoughtcrime.securesms.util.toPx
|
import org.thoughtcrime.securesms.util.toPx
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlin.math.abs
|
|
||||||
import kotlin.math.min
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
import kotlin.math.sqrt
|
|
||||||
|
|
||||||
private const val TAG = "VisibleMessageView"
|
private const val TAG = "VisibleMessageView"
|
||||||
|
|
||||||
@ -269,8 +269,7 @@ class VisibleMessageView : FrameLayout {
|
|||||||
// Method to display or hide the status of a message.
|
// Method to display or hide the status of a message.
|
||||||
// Note: Although most commonly used to display the delivery status of a message, we also use the
|
// Note: Although most commonly used to display the delivery status of a message, we also use the
|
||||||
// message status area to display the disappearing messages state - so in this latter case we'll
|
// message status area to display the disappearing messages state - so in this latter case we'll
|
||||||
// be displaying the "Sent" and the animating clock icon for outgoing messages or "Read" and the
|
// be displaying either "Sent" or "Read" and the animating clock icon.
|
||||||
// animated clock icon for incoming messages.
|
|
||||||
private fun showStatusMessage(message: MessageRecord) {
|
private fun showStatusMessage(message: MessageRecord) {
|
||||||
// We'll start by hiding everything and then only make visible what we need
|
// We'll start by hiding everything and then only make visible what we need
|
||||||
binding.messageStatusTextView.isVisible = false
|
binding.messageStatusTextView.isVisible = false
|
||||||
@ -384,37 +383,47 @@ class VisibleMessageView : FrameLayout {
|
|||||||
message.isFailed ->
|
message.isFailed ->
|
||||||
MessageStatusInfo(R.drawable.ic_delivery_status_failed,
|
MessageStatusInfo(R.drawable.ic_delivery_status_failed,
|
||||||
getThemedColor(context, R.attr.danger),
|
getThemedColor(context, R.attr.danger),
|
||||||
R.string.delivery_status_failed
|
R.string.messageStatusFailedToSend
|
||||||
)
|
)
|
||||||
message.isSyncFailed ->
|
message.isSyncFailed ->
|
||||||
MessageStatusInfo(
|
MessageStatusInfo(
|
||||||
R.drawable.ic_delivery_status_failed,
|
R.drawable.ic_delivery_status_failed,
|
||||||
context.getColor(R.color.accent_orange),
|
context.getColor(R.color.accent_orange),
|
||||||
R.string.delivery_status_sync_failed
|
R.string.messageStatusFailedToSync
|
||||||
)
|
)
|
||||||
message.isPending ->
|
message.isPending ->
|
||||||
MessageStatusInfo(
|
// Non-mms messages display 'Sending'..
|
||||||
R.drawable.ic_delivery_status_sending,
|
if (!message.isMms) {
|
||||||
context.getColorFromAttr(R.attr.message_status_color),
|
MessageStatusInfo(
|
||||||
R.string.delivery_status_sending
|
R.drawable.ic_delivery_status_sending,
|
||||||
)
|
context.getColorFromAttr(R.attr.message_status_color),
|
||||||
|
R.string.sending
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// ..and Mms messages display 'Uploading'.
|
||||||
|
MessageStatusInfo(
|
||||||
|
R.drawable.ic_delivery_status_sending,
|
||||||
|
context.getColorFromAttr(R.attr.message_status_color),
|
||||||
|
R.string.messageStatusUploading
|
||||||
|
)
|
||||||
|
}
|
||||||
message.isSyncing || message.isResyncing ->
|
message.isSyncing || message.isResyncing ->
|
||||||
MessageStatusInfo(
|
MessageStatusInfo(
|
||||||
R.drawable.ic_delivery_status_sending,
|
R.drawable.ic_delivery_status_sending,
|
||||||
context.getColorFromAttr(R.attr.message_status_color),
|
context.getColorFromAttr(R.attr.message_status_color),
|
||||||
R.string.delivery_status_sending // We COULD tell the user that we're `syncing` (R.string.delivery_status_syncing) but it will likely make more sense to them if we say "Sending"
|
R.string.messageStatusSyncing
|
||||||
)
|
)
|
||||||
message.isRead || message.isIncoming ->
|
message.isRead || message.isIncoming ->
|
||||||
MessageStatusInfo(
|
MessageStatusInfo(
|
||||||
R.drawable.ic_delivery_status_read,
|
R.drawable.ic_delivery_status_read,
|
||||||
context.getColorFromAttr(R.attr.message_status_color),
|
context.getColorFromAttr(R.attr.message_status_color),
|
||||||
R.string.delivery_status_read
|
R.string.read
|
||||||
)
|
)
|
||||||
message.isSent ->
|
message.isSent ->
|
||||||
MessageStatusInfo(
|
MessageStatusInfo(
|
||||||
R.drawable.ic_delivery_status_sent,
|
R.drawable.ic_delivery_status_sent,
|
||||||
context.getColorFromAttr(R.attr.message_status_color),
|
context.getColorFromAttr(R.attr.message_status_color),
|
||||||
R.string.delivery_status_sent
|
R.string.disappearingMessagesSent
|
||||||
)
|
)
|
||||||
else -> {
|
else -> {
|
||||||
// The message isn't one we care about for message statuses we display to the user (i.e.,
|
// The message isn't one we care about for message statuses we display to the user (i.e.,
|
||||||
|
@ -5,9 +5,11 @@ import android.util.AttributeSet
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import com.squareup.phrase.Phrase
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ViewSearchBottomBarBinding
|
import network.loki.messenger.databinding.ViewSearchBottomBarBinding
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.COUNT_KEY
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.TOTAL_COUNT_KEY
|
||||||
|
|
||||||
class SearchBottomBar : LinearLayout {
|
class SearchBottomBar : LinearLayout {
|
||||||
private lateinit var binding: ViewSearchBottomBarBinding
|
private lateinit var binding: ViewSearchBottomBarBinding
|
||||||
@ -35,7 +37,7 @@ class SearchBottomBar : LinearLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
searchPosition.text = resources.getString(R.string.ConversationActivity_search_position, position + 1, count)
|
searchPosition.text = resources.getQuantityString(R.plurals.searchMatches, count, position + 1, count)
|
||||||
} else {
|
} else {
|
||||||
searchPosition.text = ""
|
searchPosition.text = ""
|
||||||
}
|
}
|
||||||
|
@ -94,6 +94,8 @@ class SearchViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public fun getActiveQuery() = activeQuery
|
||||||
|
|
||||||
class SearchResult(private val results: CursorList<MessageResult?>, val position: Int) : Closeable {
|
class SearchResult(private val results: CursorList<MessageResult?>, val position: Int) : Closeable {
|
||||||
|
|
||||||
fun getResults(): List<MessageResult?> {
|
fun getResults(): List<MessageResult?> {
|
||||||
|
@ -16,6 +16,9 @@
|
|||||||
*/
|
*/
|
||||||
package org.thoughtcrime.securesms.conversation.v2.utilities;
|
package org.thoughtcrime.securesms.conversation.v2.utilities;
|
||||||
|
|
||||||
|
import static com.google.android.gms.common.util.CollectionUtils.listOf;
|
||||||
|
import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
@ -30,10 +33,14 @@ import android.provider.OpenableColumns;
|
|||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import com.squareup.phrase.Phrase;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import network.loki.messenger.R;
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
import org.session.libsignal.utilities.ListenableFuture;
|
import org.session.libsignal.utilities.ListenableFuture;
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
@ -55,17 +62,13 @@ import org.thoughtcrime.securesms.permissions.Permissions;
|
|||||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
public class AttachmentManager {
|
public class AttachmentManager {
|
||||||
|
|
||||||
private final static String TAG = AttachmentManager.class.getSimpleName();
|
private final static String TAG = AttachmentManager.class.getSimpleName();
|
||||||
|
|
||||||
|
// Max attachment size is 10MB, above which we display a warning toast rather than sending the msg
|
||||||
|
private final long MAX_ATTACHMENTS_FILE_SIZE_BYTES = 10 * 1024 * 1024;
|
||||||
|
|
||||||
private final @NonNull Context context;
|
private final @NonNull Context context;
|
||||||
private final @NonNull AttachmentListener attachmentListener;
|
private final @NonNull AttachmentListener attachmentListener;
|
||||||
|
|
||||||
@ -252,13 +255,31 @@ public class AttachmentManager {
|
|||||||
} else {
|
} else {
|
||||||
builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE);
|
builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE);
|
||||||
}
|
}
|
||||||
builder.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
|
|
||||||
.withRationaleDialog(activity.getString(R.string.ConversationActivity_to_send_photos_and_video_allow_signal_access_to_storage), R.drawable.ic_baseline_photo_library_24)
|
Context c = activity.getApplicationContext();
|
||||||
|
|
||||||
|
String needStoragePermissionTxt = Phrase.from(c, R.string.permissionsStorageSend).put(APP_NAME_KEY, c.getString(R.string.app_name)).format().toString();
|
||||||
|
|
||||||
|
String storagePermissionDeniedTxt = Phrase.from(c, R.string.permissionsStorageSaveDenied)
|
||||||
|
.put(APP_NAME_KEY, c.getString(R.string.app_name))
|
||||||
|
.format().toString();
|
||||||
|
|
||||||
|
builder.withPermanentDenialDialog(storagePermissionDeniedTxt)
|
||||||
|
.withRationaleDialog(needStoragePermissionTxt, R.drawable.ic_baseline_photo_library_24)
|
||||||
.onAllGranted(() -> selectMediaType(activity, "*/*", null, requestCode)) // Note: We can use startActivityForResult w/ the ACTION_OPEN_DOCUMENT or ACTION_OPEN_DOCUMENT_TREE intent if we need to modernise this.
|
.onAllGranted(() -> selectMediaType(activity, "*/*", null, requestCode)) // Note: We can use startActivityForResult w/ the ACTION_OPEN_DOCUMENT or ACTION_OPEN_DOCUMENT_TREE intent if we need to modernise this.
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body) {
|
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body) {
|
||||||
|
|
||||||
|
Context c = activity.getApplicationContext();
|
||||||
|
String needStoragePermissionTxt = Phrase.from(c, R.string.permissionsStorageSend)
|
||||||
|
.put(APP_NAME_KEY, c.getString(R.string.app_name))
|
||||||
|
.format().toString();
|
||||||
|
String cameraPermissionDeniedTxt = Phrase.from(c, R.string.cameraGrantAccessDenied)
|
||||||
|
.put(APP_NAME_KEY, c.getString(R.string.app_name))
|
||||||
|
.format().toString();
|
||||||
|
|
||||||
Permissions.PermissionsBuilder builder = Permissions.with(activity);
|
Permissions.PermissionsBuilder builder = Permissions.with(activity);
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
builder = builder.request(Manifest.permission.READ_MEDIA_VIDEO)
|
builder = builder.request(Manifest.permission.READ_MEDIA_VIDEO)
|
||||||
@ -266,8 +287,8 @@ public class AttachmentManager {
|
|||||||
} else {
|
} else {
|
||||||
builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE);
|
builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE);
|
||||||
}
|
}
|
||||||
builder.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
|
builder.withPermanentDenialDialog(cameraPermissionDeniedTxt)
|
||||||
.withRationaleDialog(activity.getString(R.string.ConversationActivity_to_send_photos_and_video_allow_signal_access_to_storage), R.drawable.ic_baseline_photo_library_24)
|
.withRationaleDialog(needStoragePermissionTxt, R.drawable.ic_baseline_photo_library_24)
|
||||||
.onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode))
|
.onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode))
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
@ -291,10 +312,19 @@ public class AttachmentManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void capturePhoto(Activity activity, int requestCode, Recipient recipient) {
|
public void capturePhoto(Activity activity, int requestCode, Recipient recipient) {
|
||||||
|
|
||||||
|
String cameraPermissionDeniedTxt = Phrase.from(context, R.string.cameraGrantAccessDenied)
|
||||||
|
.put(APP_NAME_KEY, context.getString(R.string.app_name))
|
||||||
|
.format().toString();
|
||||||
|
|
||||||
|
String requireCameraPermissionTxt = Phrase.from(context, R.string.cameraGrantAccessDescription)
|
||||||
|
.put(APP_NAME_KEY, context.getString(R.string.app_name))
|
||||||
|
.format().toString();
|
||||||
|
|
||||||
Permissions.with(activity)
|
Permissions.with(activity)
|
||||||
.request(Manifest.permission.CAMERA)
|
.request(Manifest.permission.CAMERA)
|
||||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_camera_permission_in_order_to_take_photos_but_it_has_been_permanently_denied))
|
.withPermanentDenialDialog(cameraPermissionDeniedTxt)
|
||||||
.withRationaleDialog(activity.getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera),R.drawable.ic_baseline_photo_camera_24)
|
.withRationaleDialog(requireCameraPermissionTxt, R.drawable.ic_baseline_photo_camera_24)
|
||||||
.onAllGranted(() -> {
|
.onAllGranted(() -> {
|
||||||
Intent captureIntent = MediaSendActivity.buildCameraIntent(activity, recipient);
|
Intent captureIntent = MediaSendActivity.buildCameraIntent(activity, recipient);
|
||||||
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
|
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
|
||||||
@ -326,7 +356,7 @@ public class AttachmentManager {
|
|||||||
activity.startActivityForResult(intent, requestCode);
|
activity.startActivityForResult(intent, requestCode);
|
||||||
} catch (ActivityNotFoundException anfe) {
|
} catch (ActivityNotFoundException anfe) {
|
||||||
Log.w(TAG, "couldn't complete ACTION_GET_CONTENT intent, no activity found. falling back.");
|
Log.w(TAG, "couldn't complete ACTION_GET_CONTENT intent, no activity found. falling back.");
|
||||||
Toast.makeText(activity, R.string.AttachmentManager_cant_open_media_selection, Toast.LENGTH_LONG).show();
|
Toast.makeText(activity, R.string.attachmentsErrorNoApp, Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -334,9 +364,21 @@ public class AttachmentManager {
|
|||||||
final @Nullable Slide slide,
|
final @Nullable Slide slide,
|
||||||
final @NonNull MediaConstraints constraints)
|
final @NonNull MediaConstraints constraints)
|
||||||
{
|
{
|
||||||
return slide == null ||
|
// Null attachment? Not satisfied.
|
||||||
constraints.isSatisfied(context, slide.asAttachment()) ||
|
if (slide == null) return false;
|
||||||
constraints.canResize(slide.asAttachment());
|
|
||||||
|
// Attachments are excessively large? Not satisfied.
|
||||||
|
// Note: This file size test must come BEFORE the `constraints.isSatisfied` check below because
|
||||||
|
// it is a more specific type of check.
|
||||||
|
if (slide.asAttachment().getSize() > MAX_ATTACHMENTS_FILE_SIZE_BYTES) {
|
||||||
|
Toast.makeText(context, R.string.attachmentsErrorSize, Toast.LENGTH_SHORT).show();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise we return whether our constraints are satisfied OR if we can resize the attachment
|
||||||
|
// (in the case of one or more images) - either one will be acceptable, but if both aren't then
|
||||||
|
// we fail the constraint test.
|
||||||
|
return constraints.isSatisfied(context, slide.asAttachment()) || constraints.canResize(slide.asAttachment());
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface AttachmentListener {
|
public interface AttachmentListener {
|
||||||
|
@ -54,13 +54,14 @@ object MentionUtilities {
|
|||||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||||
val openGroup by lazy { DatabaseComponent.get(context).storage().getOpenGroup(threadID) }
|
val openGroup by lazy { DatabaseComponent.get(context).storage().getOpenGroup(threadID) }
|
||||||
|
|
||||||
// format the mention text
|
// Format the mention text
|
||||||
if (matcher.find(startIndex)) {
|
if (matcher.find(startIndex)) {
|
||||||
while (true) {
|
while (true) {
|
||||||
val publicKey = text.subSequence(matcher.start() + 1, matcher.end()).toString() // +1 to get rid of the @
|
val publicKey = text.subSequence(matcher.start() + 1, matcher.end()).toString() // +1 to get rid of the @
|
||||||
|
|
||||||
val isYou = isYou(publicKey, userPublicKey, openGroup)
|
val isYou = isYou(publicKey, userPublicKey, openGroup)
|
||||||
val userDisplayName: String? = if (isYou) {
|
val userDisplayName: String? = if (isYou) {
|
||||||
context.getString(R.string.MessageRecord_you)
|
context.getString(R.string.you)
|
||||||
} else {
|
} else {
|
||||||
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithAccountID(publicKey)
|
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithAccountID(publicKey)
|
||||||
@Suppress("NAME_SHADOWING") val context = if (openGroup != null) Contact.ContactContext.OPEN_GROUP else Contact.ContactContext.REGULAR
|
@Suppress("NAME_SHADOWING") val context = if (openGroup != null) Contact.ContactContext.OPEN_GROUP else Contact.ContactContext.REGULAR
|
||||||
|
@ -8,7 +8,7 @@ import org.thoughtcrime.securesms.showSessionDialog
|
|||||||
object NotificationUtils {
|
object NotificationUtils {
|
||||||
fun showNotifyDialog(context: Context, thread: Recipient, notifyTypeHandler: (Int)->Unit) {
|
fun showNotifyDialog(context: Context, thread: Recipient, notifyTypeHandler: (Int)->Unit) {
|
||||||
context.showSessionDialog {
|
context.showSessionDialog {
|
||||||
title(R.string.RecipientPreferenceActivity_notification_settings)
|
title(R.string.sessionNotifications)
|
||||||
singleChoiceItems(
|
singleChoiceItems(
|
||||||
context.resources.getStringArray(R.array.notify_types),
|
context.resources.getStringArray(R.array.notify_types),
|
||||||
thread.notifyType
|
thread.notifyType
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.v2.utilities
|
package org.thoughtcrime.securesms.conversation.v2.utilities
|
||||||
|
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
|
import android.graphics.Typeface
|
||||||
import android.text.Layout
|
import android.text.Layout
|
||||||
|
import android.text.SpannableString
|
||||||
|
import android.text.Spanned
|
||||||
import android.text.StaticLayout
|
import android.text.StaticLayout
|
||||||
import android.text.TextPaint
|
import android.text.TextPaint
|
||||||
|
import android.text.style.StyleSpan
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.text.getSpans
|
import androidx.core.text.getSpans
|
||||||
|
@ -3,14 +3,8 @@ package org.thoughtcrime.securesms.database;
|
|||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@ -24,10 +18,10 @@ public class DraftDatabase extends Database {
|
|||||||
public static final String DRAFT_VALUE = "value";
|
public static final String DRAFT_VALUE = "value";
|
||||||
|
|
||||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
|
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
|
||||||
THREAD_ID + " INTEGER, " + DRAFT_TYPE + " TEXT, " + DRAFT_VALUE + " TEXT);";
|
THREAD_ID + " INTEGER, " + DRAFT_TYPE + " TEXT, " + DRAFT_VALUE + " TEXT);";
|
||||||
|
|
||||||
public static final String[] CREATE_INDEXS = {
|
public static final String[] CREATE_INDEXS = {
|
||||||
"CREATE INDEX IF NOT EXISTS draft_thread_index ON " + TABLE_NAME + " (" + THREAD_ID + ");",
|
"CREATE INDEX IF NOT EXISTS draft_thread_index ON " + TABLE_NAME + " (" + THREAD_ID + ");",
|
||||||
};
|
};
|
||||||
|
|
||||||
public DraftDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
public DraftDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||||
@ -59,8 +53,8 @@ public class DraftDatabase extends Database {
|
|||||||
|
|
||||||
for (long threadId : threadIds) {
|
for (long threadId : threadIds) {
|
||||||
where.append(" OR ")
|
where.append(" OR ")
|
||||||
.append(THREAD_ID)
|
.append(THREAD_ID)
|
||||||
.append(" = ?");
|
.append(" = ?");
|
||||||
|
|
||||||
arguments.add(String.valueOf(threadId));
|
arguments.add(String.valueOf(threadId));
|
||||||
}
|
}
|
||||||
@ -95,12 +89,10 @@ public class DraftDatabase extends Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Class to save drafts of text (only) messages if the user is in the middle of writing a message
|
||||||
|
// and then the app loses focus or is closed.
|
||||||
public static class Draft {
|
public static class Draft {
|
||||||
public static final String TEXT = "text";
|
public static final String TEXT = "text";
|
||||||
public static final String IMAGE = "image";
|
|
||||||
public static final String VIDEO = "video";
|
|
||||||
public static final String AUDIO = "audio";
|
|
||||||
public static final String QUOTE = "quote";
|
|
||||||
|
|
||||||
private final String type;
|
private final String type;
|
||||||
private final String value;
|
private final String value;
|
||||||
@ -117,48 +109,10 @@ public class DraftDatabase extends Database {
|
|||||||
public String getValue() {
|
public String getValue() {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
String getSnippet(Context context) {
|
|
||||||
switch (type) {
|
|
||||||
case TEXT: return value;
|
|
||||||
case IMAGE: return context.getString(R.string.DraftDatabase_Draft_image_snippet);
|
|
||||||
case VIDEO: return context.getString(R.string.DraftDatabase_Draft_video_snippet);
|
|
||||||
case AUDIO: return context.getString(R.string.DraftDatabase_Draft_audio_snippet);
|
|
||||||
case QUOTE: return context.getString(R.string.DraftDatabase_Draft_quote_snippet);
|
|
||||||
default: return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Drafts extends LinkedList<Draft> {
|
public static class Drafts extends LinkedList<Draft> {
|
||||||
private Draft getDraftOfType(String type) {
|
// We don't do anything with drafts of a given type anymore (image, audio etc.) - we store TEXT
|
||||||
for (Draft draft : this) {
|
// drafts, and any files or audio get sent to the recipient when added as a message.
|
||||||
if (type.equals(draft.getType())) {
|
|
||||||
return draft;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSnippet(Context context) {
|
|
||||||
Draft textDraft = getDraftOfType(Draft.TEXT);
|
|
||||||
if (textDraft != null) {
|
|
||||||
return textDraft.getSnippet(context);
|
|
||||||
} else if (size() > 0) {
|
|
||||||
return get(0).getSnippet(context);
|
|
||||||
} else {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public @Nullable Uri getUriSnippet() {
|
|
||||||
Draft imageDraft = getDraftOfType(Draft.IMAGE);
|
|
||||||
|
|
||||||
if (imageDraft != null && imageDraft.getValue() != null) {
|
|
||||||
return Uri.parse(imageDraft.getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -633,7 +633,11 @@ open class Storage(
|
|||||||
// Notify the user
|
// Notify the user
|
||||||
val threadID = getOrCreateThreadIdFor(Address.fromSerialized(groupId))
|
val threadID = getOrCreateThreadIdFor(Address.fromSerialized(groupId))
|
||||||
threadDb.setDate(threadID, formationTimestamp)
|
threadDb.setDate(threadID, formationTimestamp)
|
||||||
insertOutgoingInfoMessage(context, groupId, SignalServiceGroup.Type.CREATION, title, members.map { it.serialize() }, admins.map { it.serialize() }, threadID, formationTimestamp)
|
|
||||||
|
// Note: Commenting out this line prevents the timestamp of room creation being added to a new closed group,
|
||||||
|
// which in turn allows us to show the `groupNoMessages` control message text.
|
||||||
|
//insertOutgoingInfoMessage(context, groupId, SignalServiceGroup.Type.CREATION, title, members.map { it.serialize() }, admins.map { it.serialize() }, threadID, formationTimestamp)
|
||||||
|
|
||||||
// Don't create config group here, it's from a config update
|
// Don't create config group here, it's from a config update
|
||||||
// Start polling
|
// Start polling
|
||||||
ClosedGroupPollerV2.shared.startPolling(group.accountId)
|
ClosedGroupPollerV2.shared.startPolling(group.accountId)
|
||||||
|
@ -808,8 +808,8 @@ public class ThreadDatabase extends Database {
|
|||||||
private @NonNull String getFormattedBodyFor(@NonNull MessageRecord messageRecord) {
|
private @NonNull String getFormattedBodyFor(@NonNull MessageRecord messageRecord) {
|
||||||
if (messageRecord.isMms()) {
|
if (messageRecord.isMms()) {
|
||||||
MmsMessageRecord record = (MmsMessageRecord) messageRecord;
|
MmsMessageRecord record = (MmsMessageRecord) messageRecord;
|
||||||
if (record.getSharedContacts().size() > 0) {
|
if (!record.getSharedContacts().isEmpty()) {
|
||||||
Contact contact = ((MmsMessageRecord) messageRecord).getSharedContacts().get(0);
|
Contact contact = ((MmsMessageRecord)messageRecord).getSharedContacts().get(0);
|
||||||
return ContactUtil.getStringSummary(context, contact).toString();
|
return ContactUtil.getStringSummary(context, contact).toString();
|
||||||
}
|
}
|
||||||
String attachmentString = record.getSlideDeck().getBody();
|
String attachmentString = record.getSlideDeck().getBody();
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
package org.thoughtcrime.securesms.database.helpers;
|
package org.thoughtcrime.securesms.database.helpers;
|
||||||
|
|
||||||
|
import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
|
||||||
|
|
||||||
import android.app.NotificationChannel;
|
import android.app.NotificationChannel;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
|
import com.squareup.phrase.Phrase;
|
||||||
|
import java.io.File;
|
||||||
import net.zetetic.database.sqlcipher.SQLiteConnection;
|
import net.zetetic.database.sqlcipher.SQLiteConnection;
|
||||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
import net.zetetic.database.sqlcipher.SQLiteDatabaseHook;
|
import net.zetetic.database.sqlcipher.SQLiteDatabaseHook;
|
||||||
import net.zetetic.database.sqlcipher.SQLiteOpenHelper;
|
import net.zetetic.database.sqlcipher.SQLiteOpenHelper;
|
||||||
|
import network.loki.messenger.R;
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
|
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
|
||||||
@ -39,13 +41,8 @@ import org.thoughtcrime.securesms.database.SessionContactDatabase;
|
|||||||
import org.thoughtcrime.securesms.database.SessionJobDatabase;
|
import org.thoughtcrime.securesms.database.SessionJobDatabase;
|
||||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
|
||||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities;
|
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@ -250,18 +247,22 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
|
|
||||||
// Notify the user of the issue so they know they can downgrade until the issue is fixed
|
// Notify the user of the issue so they know they can downgrade until the issue is fixed
|
||||||
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||||||
String channelId = context.getString(R.string.NotificationChannel_failures);
|
String channelId = context.getString(R.string.failures);
|
||||||
|
|
||||||
NotificationChannel channel = new NotificationChannel(channelId, channelId, NotificationManager.IMPORTANCE_HIGH);
|
NotificationChannel channel = new NotificationChannel(channelId, channelId, NotificationManager.IMPORTANCE_HIGH);
|
||||||
channel.enableVibration(true);
|
channel.enableVibration(true);
|
||||||
notificationManager.createNotificationChannel(channel);
|
notificationManager.createNotificationChannel(channel);
|
||||||
|
|
||||||
|
CharSequence errorTxt = Phrase.from(context, R.string.databaseErrorGeneric)
|
||||||
|
.put(APP_NAME_KEY, R.string.app_name)
|
||||||
|
.format();
|
||||||
|
|
||||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId)
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId)
|
||||||
.setSmallIcon(R.drawable.ic_notification)
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
.setColor(context.getResources().getColor(R.color.textsecure_primary))
|
.setColor(context.getResources().getColor(R.color.textsecure_primary))
|
||||||
.setCategory(NotificationCompat.CATEGORY_ERROR)
|
.setCategory(NotificationCompat.CATEGORY_ERROR)
|
||||||
.setContentTitle(context.getString(R.string.ErrorNotifier_migration))
|
.setContentTitle(context.getString(R.string.errorDatabase))
|
||||||
.setContentText(context.getString(R.string.ErrorNotifier_migration_downgrade))
|
.setContentText(errorTxt)
|
||||||
.setAutoCancel(true);
|
.setAutoCancel(true);
|
||||||
|
|
||||||
notificationManager.notify(5874, builder.build());
|
notificationManager.notify(5874, builder.build());
|
||||||
|
@ -14,6 +14,8 @@ import org.session.libsession.utilities.Address;
|
|||||||
import org.session.libsession.utilities.recipients.Recipient;
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
||||||
|
import org.thoughtcrime.securesms.util.DateUtils;
|
||||||
|
import org.thoughtcrime.securesms.util.RelativeDay;
|
||||||
|
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -88,16 +90,18 @@ public class BucketedThreadMediaLoader extends AsyncTaskLoader<BucketedThreadMed
|
|||||||
|
|
||||||
private final TimeBucket[] TIME_SECTIONS;
|
private final TimeBucket[] TIME_SECTIONS;
|
||||||
|
|
||||||
public BucketedThreadMedia(@NonNull Context context) {
|
public BucketedThreadMedia(@NonNull Context context) {
|
||||||
this.TODAY = new TimeBucket(context.getString(R.string.BucketedThreadMedia_Today), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -1), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, 1000));
|
String localisedTodayString = DateUtils.INSTANCE.getLocalisedRelativeDayString(RelativeDay.TODAY);
|
||||||
this.YESTERDAY = new TimeBucket(context.getString(R.string.BucketedThreadMedia_Yesterday), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -2), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -1));
|
String localisedYesterdayString = DateUtils.INSTANCE.getLocalisedRelativeDayString(RelativeDay.YESTERDAY);
|
||||||
this.THIS_WEEK = new TimeBucket(context.getString(R.string.BucketedThreadMedia_This_week), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -7), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -2));
|
|
||||||
this.THIS_MONTH = new TimeBucket(context.getString(R.string.BucketedThreadMedia_This_month), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -30), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -7));
|
this.TODAY = new TimeBucket(localisedTodayString, TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -1), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, 1000));
|
||||||
this.TIME_SECTIONS = new TimeBucket[]{TODAY, YESTERDAY, THIS_WEEK, THIS_MONTH};
|
this.YESTERDAY = new TimeBucket(localisedYesterdayString, TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -2), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -1));
|
||||||
this.OLDER = new MonthBuckets();
|
this.THIS_WEEK = new TimeBucket(context.getString(R.string.attachmentsThisWeek), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -7), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -2));
|
||||||
|
this.THIS_MONTH = new TimeBucket(context.getString(R.string.attachmentsThisMonth), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -30), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -7));
|
||||||
|
this.TIME_SECTIONS = new TimeBucket[] { TODAY, YESTERDAY, THIS_WEEK, THIS_MONTH };
|
||||||
|
this.OLDER = new MonthBuckets();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void add(MediaDatabase.MediaRecord mediaRecord) {
|
public void add(MediaDatabase.MediaRecord mediaRecord) {
|
||||||
for (TimeBucket timeSection : TIME_SECTIONS) {
|
for (TimeBucket timeSection : TIME_SECTIONS) {
|
||||||
if (timeSection.inRange(mediaRecord.getDate())) {
|
if (timeSection.inRange(mediaRecord.getDate())) {
|
||||||
|
@ -77,14 +77,6 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SpannableString getDisplayBody(@NonNull Context context) {
|
public SpannableString getDisplayBody(@NonNull Context context) {
|
||||||
if (MmsDatabase.Types.isFailedDecryptType(type)) {
|
|
||||||
return emphasisAdded(context.getString(R.string.MmsMessageRecord_bad_encrypted_mms_message));
|
|
||||||
} else if (MmsDatabase.Types.isDuplicateMessageType(type)) {
|
|
||||||
return emphasisAdded(context.getString(R.string.SmsMessageRecord_duplicate_message));
|
|
||||||
} else if (MmsDatabase.Types.isNoRemoteSessionType(type)) {
|
|
||||||
return emphasisAdded(context.getString(R.string.MmsMessageRecord_mms_message_encrypted_for_non_existing_session));
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.getDisplayBody(context);
|
return super.getDisplayBody(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,15 +57,7 @@ public class SmsMessageRecord extends MessageRecord {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SpannableString getDisplayBody(@NonNull Context context) {
|
public SpannableString getDisplayBody(@NonNull Context context) {
|
||||||
if (SmsDatabase.Types.isFailedDecryptType(type)) {
|
return super.getDisplayBody(context);
|
||||||
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_bad_encrypted_message));
|
|
||||||
} else if (SmsDatabase.Types.isDuplicateMessageType(type)) {
|
|
||||||
return emphasisAdded(context.getString(R.string.SmsMessageRecord_duplicate_message));
|
|
||||||
} else if (SmsDatabase.Types.isNoRemoteSessionType(type)) {
|
|
||||||
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session));
|
|
||||||
} else {
|
|
||||||
return super.getDisplayBody(context);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -17,21 +17,25 @@
|
|||||||
*/
|
*/
|
||||||
package org.thoughtcrime.securesms.database.model;
|
package org.thoughtcrime.securesms.database.model;
|
||||||
|
|
||||||
|
import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
|
||||||
|
import static org.session.libsession.utilities.StringSubstitutionConstants.DISAPPEARING_MESSAGES_TYPE_KEY;
|
||||||
|
import static org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY;
|
||||||
|
import static org.session.libsession.utilities.StringSubstitutionConstants.TIME_KEY;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.text.Spannable;
|
import android.text.Spannable;
|
||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.style.StyleSpan;
|
import android.text.style.StyleSpan;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import com.squareup.phrase.Phrase;
|
||||||
import org.session.libsession.utilities.ExpirationUtil;
|
import org.session.libsession.utilities.ExpirationUtil;
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -42,146 +46,179 @@ import network.loki.messenger.R;
|
|||||||
*/
|
*/
|
||||||
public class ThreadRecord extends DisplayRecord {
|
public class ThreadRecord extends DisplayRecord {
|
||||||
|
|
||||||
private @Nullable final Uri snippetUri;
|
private @Nullable final Uri snippetUri;
|
||||||
public @Nullable final MessageRecord lastMessage;
|
public @Nullable final MessageRecord lastMessage;
|
||||||
private final long count;
|
private final long count;
|
||||||
private final int unreadCount;
|
private final int unreadCount;
|
||||||
private final int unreadMentionCount;
|
private final int unreadMentionCount;
|
||||||
private final int distributionType;
|
private final int distributionType;
|
||||||
private final boolean archived;
|
private final boolean archived;
|
||||||
private final long expiresIn;
|
private final long expiresIn;
|
||||||
private final long lastSeen;
|
private final long lastSeen;
|
||||||
private final boolean pinned;
|
private final boolean pinned;
|
||||||
private final int initialRecipientHash;
|
private final int initialRecipientHash;
|
||||||
|
private final long dateSent;
|
||||||
|
|
||||||
public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri,
|
public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri,
|
||||||
@Nullable MessageRecord lastMessage, @NonNull Recipient recipient, long date, long count, int unreadCount,
|
@Nullable MessageRecord lastMessage, @NonNull Recipient recipient, long date, long count, int unreadCount,
|
||||||
int unreadMentionCount, long threadId, int deliveryReceiptCount, int status,
|
int unreadMentionCount, long threadId, int deliveryReceiptCount, int status,
|
||||||
long snippetType, int distributionType, boolean archived, long expiresIn,
|
long snippetType, int distributionType, boolean archived, long expiresIn,
|
||||||
long lastSeen, int readReceiptCount, boolean pinned)
|
long lastSeen, int readReceiptCount, boolean pinned)
|
||||||
{
|
{
|
||||||
super(body, recipient, date, date, threadId, status, deliveryReceiptCount, snippetType, readReceiptCount);
|
super(body, recipient, date, date, threadId, status, deliveryReceiptCount, snippetType, readReceiptCount);
|
||||||
this.snippetUri = snippetUri;
|
this.snippetUri = snippetUri;
|
||||||
this.lastMessage = lastMessage;
|
this.lastMessage = lastMessage;
|
||||||
this.count = count;
|
this.count = count;
|
||||||
this.unreadCount = unreadCount;
|
this.unreadCount = unreadCount;
|
||||||
this.unreadMentionCount = unreadMentionCount;
|
this.unreadMentionCount = unreadMentionCount;
|
||||||
this.distributionType = distributionType;
|
this.distributionType = distributionType;
|
||||||
this.archived = archived;
|
this.archived = archived;
|
||||||
this.expiresIn = expiresIn;
|
this.expiresIn = expiresIn;
|
||||||
this.lastSeen = lastSeen;
|
this.lastSeen = lastSeen;
|
||||||
this.pinned = pinned;
|
this.pinned = pinned;
|
||||||
this.initialRecipientHash = recipient.hashCode();
|
this.initialRecipientHash = recipient.hashCode();
|
||||||
}
|
this.dateSent = date;
|
||||||
|
|
||||||
public @Nullable Uri getSnippetUri() {
|
|
||||||
return snippetUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SpannableString getDisplayBody(@NonNull Context context) {
|
|
||||||
if (isGroupUpdateMessage()) {
|
|
||||||
return emphasisAdded(context.getString(R.string.ThreadRecord_group_updated));
|
|
||||||
} else if (isOpenGroupInvitation()) {
|
|
||||||
return emphasisAdded(context.getString(R.string.ThreadRecord_open_group_invitation));
|
|
||||||
} else if (SmsDatabase.Types.isFailedDecryptType(type)) {
|
|
||||||
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_bad_encrypted_message));
|
|
||||||
} else if (SmsDatabase.Types.isNoRemoteSessionType(type)) {
|
|
||||||
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session));
|
|
||||||
} else if (SmsDatabase.Types.isEndSessionType(type)) {
|
|
||||||
return emphasisAdded(context.getString(R.string.ThreadRecord_secure_session_reset));
|
|
||||||
} else if (MmsSmsColumns.Types.isLegacyType(type)) {
|
|
||||||
return emphasisAdded(context.getString(R.string.MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported));
|
|
||||||
} else if (MmsSmsColumns.Types.isDraftMessageType(type)) {
|
|
||||||
String draftText = context.getString(R.string.ThreadRecord_draft);
|
|
||||||
return emphasisAdded(draftText + " " + getBody(), 0, draftText.length());
|
|
||||||
} else if (SmsDatabase.Types.isOutgoingCall(type)) {
|
|
||||||
return emphasisAdded(context.getString(network.loki.messenger.R.string.ThreadRecord_called));
|
|
||||||
} else if (SmsDatabase.Types.isIncomingCall(type)) {
|
|
||||||
return emphasisAdded(context.getString(network.loki.messenger.R.string.ThreadRecord_called_you));
|
|
||||||
} else if (SmsDatabase.Types.isMissedCall(type)) {
|
|
||||||
return emphasisAdded(context.getString(network.loki.messenger.R.string.ThreadRecord_missed_call));
|
|
||||||
} else if (SmsDatabase.Types.isJoinedType(type)) {
|
|
||||||
return emphasisAdded(context.getString(R.string.ThreadRecord_s_is_on_signal, getRecipient().toShortString()));
|
|
||||||
} else if (SmsDatabase.Types.isExpirationTimerUpdate(type)) {
|
|
||||||
int seconds = (int) (getExpiresIn() / 1000);
|
|
||||||
if (seconds <= 0) {
|
|
||||||
return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_messages_disabled));
|
|
||||||
}
|
|
||||||
String time = ExpirationUtil.getExpirationDisplayValue(context, seconds);
|
|
||||||
return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_message_time_updated_to_s, time));
|
|
||||||
} else if (MmsSmsColumns.Types.isMediaSavedExtraction(type)) {
|
|
||||||
return emphasisAdded(context.getString(R.string.ThreadRecord_media_saved_by_s, getRecipient().toShortString()));
|
|
||||||
} else if (MmsSmsColumns.Types.isScreenshotExtraction(type)) {
|
|
||||||
return emphasisAdded(context.getString(R.string.ThreadRecord_s_took_a_screenshot, getRecipient().toShortString()));
|
|
||||||
} else if (SmsDatabase.Types.isIdentityUpdate(type)) {
|
|
||||||
if (getRecipient().isGroupRecipient()) return emphasisAdded(context.getString(R.string.ThreadRecord_safety_number_changed));
|
|
||||||
else return emphasisAdded(context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, getRecipient().toShortString()));
|
|
||||||
} else if (SmsDatabase.Types.isIdentityVerified(type)) {
|
|
||||||
return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_verified));
|
|
||||||
} else if (SmsDatabase.Types.isIdentityDefault(type)) {
|
|
||||||
return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_unverified));
|
|
||||||
} else if (MmsSmsColumns.Types.isMessageRequestResponse(type)) {
|
|
||||||
return emphasisAdded(context.getString(R.string.message_requests_accepted));
|
|
||||||
} else if (getCount() == 0) {
|
|
||||||
return new SpannableString(context.getString(R.string.ThreadRecord_empty_message));
|
|
||||||
} else {
|
|
||||||
if (TextUtils.isEmpty(getBody())) {
|
|
||||||
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_media_message)));
|
|
||||||
} else {
|
|
||||||
return new SpannableString(getBody());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private SpannableString emphasisAdded(String sequence) {
|
public @Nullable Uri getSnippetUri() {
|
||||||
return emphasisAdded(sequence, 0, sequence.length());
|
return snippetUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
private SpannableString emphasisAdded(String sequence, int start, int end) {
|
private String getName() {
|
||||||
SpannableString spannable = new SpannableString(sequence);
|
String name = getRecipient().getName();
|
||||||
spannable.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC),
|
if (name == null) {
|
||||||
start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
Log.w("ThreadRecord", "Got a null name - using: Unknown");
|
||||||
return spannable;
|
name = "Unknown";
|
||||||
}
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
public long getCount() {
|
private String getDisappearingMsgExpiryTypeString(Context context) {
|
||||||
return count;
|
MessageRecord lm = this.lastMessage;
|
||||||
}
|
if (lm == null) {
|
||||||
|
Log.w("ThreadRecord", "Could not get last message to determine disappearing msg type.");
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
long expireStarted = lm.getExpireStarted();
|
||||||
|
|
||||||
public int getUnreadCount() {
|
// Note: This works because expireStarted is 0 for messages which are 'Disappear after read'
|
||||||
return unreadCount;
|
// while it's a touch higher than the sent timestamp for "Disappear after send". We could then
|
||||||
}
|
// use `expireStarted == 0`, but that's not how it's done in UpdateMessageBuilder so to keep
|
||||||
|
// things the same I'll assume there's a reason for this and follow suit.
|
||||||
|
// Also: `this.lastMessage.getExpiresIn()` is available.
|
||||||
|
if (expireStarted >= dateSent) {
|
||||||
|
return context.getString(R.string.disappearingMessagesSent);
|
||||||
|
}
|
||||||
|
return context.getString(R.string.read);
|
||||||
|
}
|
||||||
|
|
||||||
public int getUnreadMentionCount() {
|
@Override
|
||||||
return unreadMentionCount;
|
public SpannableString getDisplayBody(@NonNull Context context) {
|
||||||
}
|
if (isGroupUpdateMessage()) {
|
||||||
|
return emphasisAdded(context.getString(R.string.groupUpdated));
|
||||||
|
} else if (isOpenGroupInvitation()) {
|
||||||
|
return emphasisAdded(context.getString(R.string.communityInvitation));
|
||||||
|
} else if (MmsSmsColumns.Types.isLegacyType(type)) {
|
||||||
|
String txt = Phrase.from(context, R.string.messageErrorOld)
|
||||||
|
.put(APP_NAME_KEY, context.getString(R.string.app_name))
|
||||||
|
.format().toString();
|
||||||
|
return emphasisAdded(txt);
|
||||||
|
} else if (MmsSmsColumns.Types.isDraftMessageType(type)) {
|
||||||
|
String draftText = context.getString(R.string.draft);
|
||||||
|
return emphasisAdded(draftText + " " + getBody(), 0, draftText.length());
|
||||||
|
} else if (SmsDatabase.Types.isOutgoingCall(type)) {
|
||||||
|
String txt = Phrase.from(context, R.string.callsYouCalled)
|
||||||
|
.put(NAME_KEY, getName())
|
||||||
|
.format().toString();
|
||||||
|
return emphasisAdded(txt);
|
||||||
|
} else if (SmsDatabase.Types.isIncomingCall(type)) {
|
||||||
|
String txt = Phrase.from(context, R.string.callsCalledYou)
|
||||||
|
.put(NAME_KEY, getName())
|
||||||
|
.format().toString();
|
||||||
|
return emphasisAdded(txt);
|
||||||
|
} else if (SmsDatabase.Types.isMissedCall(type)) {
|
||||||
|
String txt = Phrase.from(context, R.string.callsMissedCallFrom)
|
||||||
|
.put(NAME_KEY, getName())
|
||||||
|
.format().toString();
|
||||||
|
return emphasisAdded(txt);
|
||||||
|
} else if (SmsDatabase.Types.isExpirationTimerUpdate(type)) {
|
||||||
|
int seconds = (int) (getExpiresIn() / 1000);
|
||||||
|
if (seconds <= 0) {
|
||||||
|
String txt = Phrase.from(context, R.string.disappearingMessagesTurnedOff)
|
||||||
|
.put(NAME_KEY, getName())
|
||||||
|
.format().toString();
|
||||||
|
return emphasisAdded(txt);
|
||||||
|
}
|
||||||
|
|
||||||
public long getDate() {
|
// Implied that disappearing messages is enabled..
|
||||||
return getDateReceived();
|
String time = ExpirationUtil.getExpirationDisplayValue(context, seconds);
|
||||||
}
|
String disappearAfterWhat = getDisappearingMsgExpiryTypeString(context); // Disappear after send or read?
|
||||||
|
String txt = Phrase.from(context, R.string.disappearingMessagesSet)
|
||||||
|
.put(NAME_KEY, getName())
|
||||||
|
.put(TIME_KEY, time)
|
||||||
|
.put(DISAPPEARING_MESSAGES_TYPE_KEY, disappearAfterWhat)
|
||||||
|
.format().toString();
|
||||||
|
return emphasisAdded(txt);
|
||||||
|
|
||||||
public boolean isArchived() {
|
} else if (MmsSmsColumns.Types.isMediaSavedExtraction(type)) {
|
||||||
return archived;
|
String txt = Phrase.from(context, R.string.attachmentsMediaSaved)
|
||||||
}
|
.put(NAME_KEY, getName())
|
||||||
|
.format().toString();
|
||||||
|
return emphasisAdded(txt);
|
||||||
|
|
||||||
public int getDistributionType() {
|
} else if (MmsSmsColumns.Types.isScreenshotExtraction(type)) {
|
||||||
return distributionType;
|
String txt = Phrase.from(context, R.string.screenshotTaken)
|
||||||
}
|
.put(NAME_KEY, getName())
|
||||||
|
.format().toString();
|
||||||
|
return emphasisAdded(txt);
|
||||||
|
|
||||||
public long getExpiresIn() {
|
} else if (MmsSmsColumns.Types.isMessageRequestResponse(type)) {
|
||||||
return expiresIn;
|
return emphasisAdded(context.getString(R.string.messageRequestsAccepted));
|
||||||
}
|
} else if (getCount() == 0) {
|
||||||
|
return new SpannableString(context.getString(R.string.messageEmpty));
|
||||||
|
} else {
|
||||||
|
// This block hits when we receive a media message from an unaccepted contact - however,
|
||||||
|
// unaccepted contacts aren't allowed to send us media - so we'll return an empty string
|
||||||
|
// if it's JUST an image, or the body text that accompanied the image should any exist.
|
||||||
|
// We could return null here - but then we have to find all the usages of this
|
||||||
|
// `getDisplayBody` method and make sure it doesn't fall over if it has a null result.
|
||||||
|
if (TextUtils.isEmpty(getBody())) {
|
||||||
|
return new SpannableString("");
|
||||||
|
// Old behaviour was: return new SpannableString(emphasisAdded(context.getString(R.string.mediaMessage)));
|
||||||
|
} else {
|
||||||
|
return new SpannableString(getBody());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public long getLastSeen() {
|
private SpannableString emphasisAdded(String sequence) {
|
||||||
return lastSeen;
|
return emphasisAdded(sequence, 0, sequence.length());
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPinned() {
|
private SpannableString emphasisAdded(String sequence, int start, int end) {
|
||||||
return pinned;
|
SpannableString spannable = new SpannableString(sequence);
|
||||||
}
|
spannable.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC),
|
||||||
|
start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
return spannable;
|
||||||
|
}
|
||||||
|
|
||||||
public int getInitialRecipientHash() {
|
public long getCount() { return count; }
|
||||||
return initialRecipientHash;
|
|
||||||
}
|
public int getUnreadCount() { return unreadCount; }
|
||||||
|
|
||||||
|
public int getUnreadMentionCount() { return unreadMentionCount; }
|
||||||
|
|
||||||
|
public long getDate() { return getDateReceived(); }
|
||||||
|
|
||||||
|
public boolean isArchived() { return archived; }
|
||||||
|
|
||||||
|
public int getDistributionType() { return distributionType; }
|
||||||
|
|
||||||
|
public long getExpiresIn() { return expiresIn; }
|
||||||
|
|
||||||
|
public long getLastSeen() { return lastSeen; }
|
||||||
|
|
||||||
|
public boolean isPinned() { return pinned; }
|
||||||
|
|
||||||
|
public int getInitialRecipientHash() { return initialRecipientHash; }
|
||||||
}
|
}
|
||||||
|
@ -15,8 +15,7 @@ enum class EmojiCategory(val priority: Int, val key: String, @AttrRes val icon:
|
|||||||
PLACES(4, "Places", R.attr.emoji_category_places),
|
PLACES(4, "Places", R.attr.emoji_category_places),
|
||||||
OBJECTS(5, "Objects", R.attr.emoji_category_objects),
|
OBJECTS(5, "Objects", R.attr.emoji_category_objects),
|
||||||
SYMBOLS(6, "Symbols", R.attr.emoji_category_symbol),
|
SYMBOLS(6, "Symbols", R.attr.emoji_category_symbol),
|
||||||
FLAGS(7, "Flags", R.attr.emoji_category_flags),
|
FLAGS(7, "Flags", R.attr.emoji_category_flags);
|
||||||
EMOTICONS(8, "Emoticons", R.attr.emoji_category_emoticons);
|
|
||||||
|
|
||||||
@StringRes
|
@StringRes
|
||||||
fun getCategoryLabel(): Int {
|
fun getCategoryLabel(): Int {
|
||||||
@ -31,15 +30,14 @@ enum class EmojiCategory(val priority: Int, val key: String, @AttrRes val icon:
|
|||||||
@StringRes
|
@StringRes
|
||||||
fun getCategoryLabel(@AttrRes iconAttr: Int): Int {
|
fun getCategoryLabel(@AttrRes iconAttr: Int): Int {
|
||||||
return when (iconAttr) {
|
return when (iconAttr) {
|
||||||
R.attr.emoji_category_people -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__smileys_and_people
|
R.attr.emoji_category_people -> R.string.emojiCategorySmileys
|
||||||
R.attr.emoji_category_nature -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__nature
|
R.attr.emoji_category_nature -> R.string.emojiCategoryAnimals
|
||||||
R.attr.emoji_category_foods -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__food
|
R.attr.emoji_category_foods -> R.string.emojiCategoryFood
|
||||||
R.attr.emoji_category_activity -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__activities
|
R.attr.emoji_category_activity -> R.string.emojiCategoryActivities
|
||||||
R.attr.emoji_category_places -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__places
|
R.attr.emoji_category_places -> R.string.emojiCategoryTravel
|
||||||
R.attr.emoji_category_objects -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__objects
|
R.attr.emoji_category_objects -> R.string.emojiCategoryObjects
|
||||||
R.attr.emoji_category_symbol -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__symbols
|
R.attr.emoji_category_symbol -> R.string.emojiCategorySymbols
|
||||||
R.attr.emoji_category_flags -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__flags
|
R.attr.emoji_category_flags -> R.string.emojiCategoryFlags
|
||||||
R.attr.emoji_category_emoticons -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__emoticons
|
|
||||||
else -> throw AssertionError()
|
else -> throw AssertionError()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,10 +110,12 @@ class EmojiSource(
|
|||||||
val parsedData: ParsedEmojiData = EmojiJsonParser.parse(it, ::getAssetsUri).getOrThrow()
|
val parsedData: ParsedEmojiData = EmojiJsonParser.parse(it, ::getAssetsUri).getOrThrow()
|
||||||
return EmojiSource(
|
return EmojiSource(
|
||||||
ScreenDensity.xhdpiRelativeDensityScaleFactor("xhdpi"),
|
ScreenDensity.xhdpiRelativeDensityScaleFactor("xhdpi"),
|
||||||
|
|
||||||
parsedData.copy(
|
parsedData.copy(
|
||||||
displayPages = parsedData.displayPages + PAGE_EMOTICONS,
|
displayPages = parsedData.displayPages,
|
||||||
dataPages = parsedData.dataPages + PAGE_EMOTICONS
|
dataPages = parsedData.dataPages
|
||||||
)
|
)
|
||||||
|
|
||||||
) { uri: Uri -> EmojiPage.Asset(uri) }
|
) { uri: Uri -> EmojiPage.Asset(uri) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -137,25 +139,3 @@ data class ObsoleteEmoji(val obsolete: String, val replaceWith: String)
|
|||||||
data class EmojiMetrics(val rawHeight: Int, val rawWidth: Int, val perRow: Int)
|
data class EmojiMetrics(val rawHeight: Int, val rawWidth: Int, val perRow: Int)
|
||||||
|
|
||||||
private fun getAssetsUri(name: String, format: String): Uri = Uri.parse("file:///android_asset/emoji/$name.$format")
|
private fun getAssetsUri(name: String, format: String): Uri = Uri.parse("file:///android_asset/emoji/$name.$format")
|
||||||
|
|
||||||
private val PAGE_EMOTICONS: EmojiPageModel = StaticEmojiPageModel(
|
|
||||||
EmojiCategory.EMOTICONS,
|
|
||||||
arrayOf(
|
|
||||||
":-)", ";-)", "(-:", ":->", ":-D", "\\o/",
|
|
||||||
":-P", "B-)", ":-$", ":-*", "O:-)", "=-O",
|
|
||||||
"O_O", "O_o", "o_O", ":O", ":-!", ":-x",
|
|
||||||
":-|", ":-\\", ":-(", ":'(", ":-[", ">:-(",
|
|
||||||
"^.^", "^_^", "\\(\u02c6\u02da\u02c6)/",
|
|
||||||
"\u30fd(\u00b0\u25c7\u00b0 )\u30ce", "\u00af\\(\u00b0_o)/\u00af",
|
|
||||||
"\u00af\\_(\u30c4)_/\u00af", "(\u00ac_\u00ac)",
|
|
||||||
"(>_<)", "(\u2565\ufe4f\u2565)", "(\u261e\uff9f\u30ee\uff9f)\u261e",
|
|
||||||
"\u261c(\uff9f\u30ee\uff9f\u261c)", "\u261c(\u2312\u25bd\u2312)\u261e",
|
|
||||||
"(\u256f\u00b0\u25a1\u00b0)\u256f\ufe35", "\u253b\u2501\u253b",
|
|
||||||
"\u252c\u2500\u252c", "\u30ce(\u00b0\u2013\u00b0\u30ce)",
|
|
||||||
"(^._.^)\uff89", "\u0e05^\u2022\ufecc\u2022^\u0e05",
|
|
||||||
"\u0295\u2022\u1d25\u2022\u0294", "(\u2022_\u2022)",
|
|
||||||
" \u25a0-\u25a0\u00ac <(\u2022_\u2022) ", "(\u25a0_\u25a0\u00ac)",
|
|
||||||
"\u01aa(\u0693\u05f2)\u200e\u01aa\u200b\u200b"
|
|
||||||
),
|
|
||||||
null
|
|
||||||
)
|
|
||||||
|
@ -19,6 +19,7 @@ import androidx.viewpager.widget.ViewPager;
|
|||||||
import com.google.android.material.tabs.TabLayout;
|
import com.google.android.material.tabs.TabLayout;
|
||||||
|
|
||||||
import org.session.libsession.utilities.MediaTypes;
|
import org.session.libsession.utilities.MediaTypes;
|
||||||
|
import org.session.libsession.utilities.NonTranslatableStringConstants;
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||||
@ -120,7 +121,7 @@ public class GiphyActivity extends PassphraseRequiredActionBarActivity
|
|||||||
|
|
||||||
protected void onPostExecute(@Nullable Uri uri) {
|
protected void onPostExecute(@Nullable Uri uri) {
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
Toast.makeText(GiphyActivity.this, R.string.GiphyActivity_error_while_retrieving_full_resolution_gif, Toast.LENGTH_LONG).show();
|
Toast.makeText(GiphyActivity.this, R.string.errorUnknown, Toast.LENGTH_LONG).show();
|
||||||
} else if (viewHolder == finishingImage) {
|
} else if (viewHolder == finishingImage) {
|
||||||
Intent intent = new Intent();
|
Intent intent = new Intent();
|
||||||
intent.setData(uri);
|
intent.setData(uri);
|
||||||
@ -165,8 +166,8 @@ public class GiphyActivity extends PassphraseRequiredActionBarActivity
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CharSequence getPageTitle(int position) {
|
public CharSequence getPageTitle(int position) {
|
||||||
if (position == 0) return context.getString(R.string.GiphyFragmentPagerAdapter_gifs);
|
if (position == 0) return NonTranslatableStringConstants.GIF;
|
||||||
else return context.getString(R.string.GiphyFragmentPagerAdapter_stickers);
|
else return context.getString(R.string.stickers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,17 +76,20 @@ class CreateGroupFragment : Fragment() {
|
|||||||
if (isLoading) return@setOnClickListener
|
if (isLoading) return@setOnClickListener
|
||||||
val name = binding.nameEditText.text.trim()
|
val name = binding.nameEditText.text.trim()
|
||||||
if (name.isEmpty()) {
|
if (name.isEmpty()) {
|
||||||
return@setOnClickListener Toast.makeText(context, R.string.activity_create_closed_group_group_name_missing_error, Toast.LENGTH_LONG).show()
|
return@setOnClickListener Toast.makeText(context, R.string.groupNameEnterPlease, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Limit the group name length if it exceeds the limit
|
||||||
if (name.length > resources.getInteger(R.integer.max_group_and_community_name_length_chars)) {
|
if (name.length > resources.getInteger(R.integer.max_group_and_community_name_length_chars)) {
|
||||||
return@setOnClickListener Toast.makeText(context, R.string.activity_create_closed_group_group_name_too_long_error, Toast.LENGTH_LONG).show()
|
return@setOnClickListener Toast.makeText(context, R.string.groupNameEnterShorter, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
val selectedMembers = adapter.selectedMembers
|
val selectedMembers = adapter.selectedMembers
|
||||||
if (selectedMembers.isEmpty()) {
|
if (selectedMembers.isEmpty()) {
|
||||||
return@setOnClickListener Toast.makeText(context, R.string.activity_create_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show()
|
return@setOnClickListener Toast.makeText(context, R.string.groupCreateErrorNoMembers, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
if (selectedMembers.count() >= groupSizeLimit) { // Minus one because we're going to include self later
|
if (selectedMembers.count() >= groupSizeLimit) { // Minus one because we're going to include self later
|
||||||
return@setOnClickListener Toast.makeText(context, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show()
|
return@setOnClickListener Toast.makeText(context, R.string.groupAddMemberMaximum, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
val userPublicKey = TextSecurePreferences.getLocalNumber(requireContext())!!
|
val userPublicKey = TextSecurePreferences.getLocalNumber(requireContext())!!
|
||||||
isLoading = true
|
isLoading = true
|
||||||
|
@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.groups
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.SpannableString
|
||||||
|
import android.text.style.StyleSpan
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@ -16,7 +18,10 @@ import androidx.loader.app.LoaderManager
|
|||||||
import androidx.loader.content.Loader
|
import androidx.loader.content.Loader
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.squareup.phrase.Phrase
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import java.io.IOException
|
||||||
|
import javax.inject.Inject
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import nl.komponents.kovenant.Promise
|
import nl.komponents.kovenant.Promise
|
||||||
import nl.komponents.kovenant.task
|
import nl.komponents.kovenant.task
|
||||||
@ -26,6 +31,7 @@ import org.session.libsession.messaging.sending_receiving.MessageSender
|
|||||||
import org.session.libsession.messaging.sending_receiving.groupSizeLimit
|
import org.session.libsession.messaging.sending_receiving.groupSizeLimit
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.COUNT_KEY
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.ThemeUtil
|
import org.session.libsession.utilities.ThemeUtil
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
@ -40,8 +46,6 @@ import org.thoughtcrime.securesms.groups.ClosedGroupManager.updateLegacyGroup
|
|||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import org.thoughtcrime.securesms.util.fadeIn
|
import org.thoughtcrime.securesms.util.fadeIn
|
||||||
import org.thoughtcrime.securesms.util.fadeOut
|
import org.thoughtcrime.securesms.util.fadeOut
|
||||||
import java.io.IOException
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
|
class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
|
||||||
@ -107,17 +111,17 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
groupID = intent.getStringExtra(groupIDKey)!!
|
groupID = intent.getStringExtra(groupIDKey)!!
|
||||||
val groupInfo = DatabaseComponent.get(this).groupDatabase().getGroup(groupID).get()
|
val groupInfo = DatabaseComponent.get(this).groupDatabase().getGroup(groupID).get()
|
||||||
originalName = groupInfo.title
|
originalName = groupInfo.title
|
||||||
isSelfAdmin = groupInfo.admins.any{ it.serialize() == TextSecurePreferences.getLocalNumber(this) }
|
isSelfAdmin = groupInfo.admins.any { it.serialize() == TextSecurePreferences.getLocalNumber(this) }
|
||||||
|
|
||||||
name = originalName
|
name = originalName
|
||||||
|
|
||||||
mainContentContainer = findViewById(R.id.mainContentContainer)
|
mainContentContainer = findViewById(R.id.mainContentContainer)
|
||||||
cntGroupNameEdit = findViewById(R.id.cntGroupNameEdit)
|
cntGroupNameEdit = findViewById(R.id.cntGroupNameEdit)
|
||||||
cntGroupNameDisplay = findViewById(R.id.cntGroupNameDisplay)
|
cntGroupNameDisplay = findViewById(R.id.cntGroupNameDisplay)
|
||||||
edtGroupName = findViewById(R.id.edtGroupName)
|
edtGroupName = findViewById(R.id.edtGroupName)
|
||||||
emptyStateContainer = findViewById(R.id.emptyStateContainer)
|
emptyStateContainer = findViewById(R.id.emptyStateContainer)
|
||||||
lblGroupNameDisplay = findViewById(R.id.lblGroupNameDisplay)
|
lblGroupNameDisplay = findViewById(R.id.lblGroupNameDisplay)
|
||||||
loaderContainer = findViewById(R.id.loaderContainer)
|
loaderContainer = findViewById(R.id.loaderContainer)
|
||||||
|
|
||||||
findViewById<View>(R.id.addMembersClosedGroupButton).setOnClickListener {
|
findViewById<View>(R.id.addMembersClosedGroupButton).setOnClickListener {
|
||||||
onAddMembersClick()
|
onAddMembersClick()
|
||||||
@ -129,7 +133,19 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lblGroupNameDisplay.text = originalName
|
lblGroupNameDisplay.text = originalName
|
||||||
cntGroupNameDisplay.setOnClickListener { isEditingName = true }
|
|
||||||
|
// Only allow admins to click on the name of closed groups to edit them..
|
||||||
|
if (isSelfAdmin) {
|
||||||
|
cntGroupNameDisplay.setOnClickListener { isEditingName = true }
|
||||||
|
}
|
||||||
|
else // ..and also hide the edit `drawableEnd` for non-admins.
|
||||||
|
{
|
||||||
|
// Note: compoundDrawables returns 4 drawables (drawablesStart/Top/End/Bottom) -
|
||||||
|
// so the `drawableEnd` component is at index 2, which we replace with null.
|
||||||
|
val cd = lblGroupNameDisplay.compoundDrawables
|
||||||
|
lblGroupNameDisplay.setCompoundDrawables(cd[0], cd[1], null, cd[3])
|
||||||
|
}
|
||||||
|
|
||||||
findViewById<View>(R.id.btnCancelGroupNameEdit).setOnClickListener { isEditingName = false }
|
findViewById<View>(R.id.btnCancelGroupNameEdit).setOnClickListener { isEditingName = false }
|
||||||
findViewById<View>(R.id.btnSaveGroupNameEdit).setOnClickListener { saveName() }
|
findViewById<View>(R.id.btnSaveGroupNameEdit).setOnClickListener { saveName() }
|
||||||
edtGroupName.setImeActionLabel(getString(R.string.save), EditorInfo.IME_ACTION_DONE)
|
edtGroupName.setImeActionLabel(getString(R.string.save), EditorInfo.IME_ACTION_DONE)
|
||||||
@ -245,10 +261,10 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
private fun saveName() {
|
private fun saveName() {
|
||||||
val name = edtGroupName.text.toString().trim()
|
val name = edtGroupName.text.toString().trim()
|
||||||
if (name.isEmpty()) {
|
if (name.isEmpty()) {
|
||||||
return Toast.makeText(this, R.string.activity_edit_closed_group_group_name_missing_error, Toast.LENGTH_SHORT).show()
|
return Toast.makeText(this, R.string.groupNameEnterPlease, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
if (name.length >= 64) {
|
if (name.length >= 64) {
|
||||||
return Toast.makeText(this, R.string.activity_edit_closed_group_group_name_too_long_error, Toast.LENGTH_SHORT).show()
|
return Toast.makeText(this, R.string.groupNameEnterShorter, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
this.name = name
|
this.name = name
|
||||||
lblGroupNameDisplay.text = name
|
lblGroupNameDisplay.text = name
|
||||||
@ -283,20 +299,22 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (members.isEmpty()) {
|
if (members.isEmpty()) {
|
||||||
return Toast.makeText(this, R.string.activity_edit_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show()
|
return Toast.makeText(this, R.string.groupCreateErrorNoMembers, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
val maxGroupMembers = if (isClosedGroup) groupSizeLimit else legacyGroupSizeLimit
|
val maxGroupMembers = if (isClosedGroup) groupSizeLimit else legacyGroupSizeLimit
|
||||||
if (members.size >= maxGroupMembers) {
|
if (members.size >= maxGroupMembers) {
|
||||||
return Toast.makeText(this, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show()
|
return Toast.makeText(this, R.string.groupAddMemberMaximum, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
val userPublicKey = TextSecurePreferences.getLocalNumber(this)!!
|
val userPublicKey = TextSecurePreferences.getLocalNumber(this)!!
|
||||||
val userAsRecipient = Recipient.from(this, Address.fromSerialized(userPublicKey), false)
|
val userAsRecipient = Recipient.from(this, Address.fromSerialized(userPublicKey), false)
|
||||||
|
|
||||||
|
// There's presently no way in the UI to get into the state whereby you could remove yourself from the group when removing any other members
|
||||||
|
// (you can't unselect yourself - the only way to leave is to "Leave Group" from the menu) - but it's possible that this was not always
|
||||||
|
// the case - so we can leave this in as defensive code in-case something goes screwy.
|
||||||
if (!members.contains(userAsRecipient) && !members.map { it.address.toString() }.containsAll(originalMembers.minus(userPublicKey))) {
|
if (!members.contains(userAsRecipient) && !members.map { it.address.toString() }.containsAll(originalMembers.minus(userPublicKey))) {
|
||||||
val message = "Can't leave while adding or removing other members."
|
return Log.w("EditClosedGroup", "Can't leave group while adding or removing other members.")
|
||||||
return Toast.makeText(this@EditClosedGroupActivity, message, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isClosedGroup) {
|
if (isClosedGroup) {
|
||||||
|
@ -12,6 +12,7 @@ import android.widget.Toast
|
|||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
|
import com.squareup.phrase.Phrase
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -22,6 +23,7 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
|
|||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsession.utilities.OpenGroupUrlParser
|
import org.session.libsession.utilities.OpenGroupUrlParser
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.conversation.start.StartConversationDelegate
|
import org.thoughtcrime.securesms.conversation.start.StartConversationDelegate
|
||||||
@ -47,6 +49,7 @@ class JoinCommunityFragment : Fragment() {
|
|||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
binding.backButton.setOnClickListener { delegate.onDialogBackPressed() }
|
binding.backButton.setOnClickListener { delegate.onDialogBackPressed() }
|
||||||
binding.closeButton.setOnClickListener { delegate.onDialogClosePressed() }
|
binding.closeButton.setOnClickListener { delegate.onDialogClosePressed() }
|
||||||
|
|
||||||
fun showLoader() {
|
fun showLoader() {
|
||||||
binding.loader.visibility = View.VISIBLE
|
binding.loader.visibility = View.VISIBLE
|
||||||
binding.loader.animate().setDuration(150).alpha(1.0f).start()
|
binding.loader.animate().setDuration(150).alpha(1.0f).start()
|
||||||
@ -61,18 +64,23 @@ class JoinCommunityFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fun joinCommunityIfPossible(url: String) {
|
fun joinCommunityIfPossible(url: String) {
|
||||||
val openGroup = try {
|
val openGroup = try {
|
||||||
OpenGroupUrlParser.parseUrl(url)
|
OpenGroupUrlParser.parseUrl(url)
|
||||||
} catch (e: OpenGroupUrlParser.Error) {
|
} catch (e: OpenGroupUrlParser.Error) {
|
||||||
when (e) {
|
when (e) {
|
||||||
is OpenGroupUrlParser.Error.MalformedURL -> return Toast.makeText(activity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
|
is OpenGroupUrlParser.Error.MalformedURL, OpenGroupUrlParser.Error.NoRoom -> {
|
||||||
is OpenGroupUrlParser.Error.InvalidPublicKey -> return Toast.makeText(activity, R.string.invalid_public_key, Toast.LENGTH_SHORT).show()
|
return Toast.makeText(activity, context?.resources?.getString(R.string.communityJoinError), Toast.LENGTH_SHORT).show()
|
||||||
is OpenGroupUrlParser.Error.NoPublicKey -> return Toast.makeText(activity, R.string.invalid_public_key, Toast.LENGTH_SHORT).show()
|
}
|
||||||
is OpenGroupUrlParser.Error.NoRoom -> return Toast.makeText(activity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
|
is OpenGroupUrlParser.Error.InvalidPublicKey, OpenGroupUrlParser.Error.NoPublicKey -> {
|
||||||
|
return Toast.makeText(activity, R.string.communityEnterUrlErrorInvalidDescription, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoader()
|
showLoader()
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val sanitizedServer = openGroup.server.removeSuffix("/")
|
val sanitizedServer = openGroup.server.removeSuffix("/")
|
||||||
@ -90,10 +98,11 @@ class JoinCommunityFragment : Fragment() {
|
|||||||
delegate.onDialogClosePressed()
|
delegate.onDialogClosePressed()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("Loki", "Couldn't join open group.", e)
|
Log.e("Loki", "Couldn't join community.", e)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
hideLoader()
|
hideLoader()
|
||||||
Toast.makeText(activity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
|
val txt = Phrase.from(context, R.string.groupErrorJoin).put(GROUP_NAME_KEY, url).format().toString()
|
||||||
|
Toast.makeText(activity, txt, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
@ -107,8 +116,8 @@ class JoinCommunityFragment : Fragment() {
|
|||||||
)
|
)
|
||||||
val mediator = TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, pos ->
|
val mediator = TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, pos ->
|
||||||
tab.text = when (pos) {
|
tab.text = when (pos) {
|
||||||
0 -> getString(R.string.activity_join_public_chat_enter_community_url_tab_title)
|
0 -> getString(R.string.communityUrl)
|
||||||
1 -> getString(R.string.activity_join_public_chat_scan_qr_code_tab_title)
|
1 -> getString(R.string.qrScan)
|
||||||
else -> throw IllegalStateException()
|
else -> throw IllegalStateException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
package org.thoughtcrime.securesms.groups
|
package org.thoughtcrime.securesms.groups
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import okhttp3.HttpUrl
|
import com.squareup.phrase.Phrase
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import network.loki.messenger.R
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.open_groups.GroupMemberRole
|
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||||
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller
|
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.COMMUNITY_NAME_KEY
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||||
import java.util.concurrent.Executors
|
|
||||||
|
|
||||||
object OpenGroupManager {
|
object OpenGroupManager {
|
||||||
private val executorService = Executors.newScheduledThreadPool(4)
|
private val executorService = Executors.newScheduledThreadPool(4)
|
||||||
@ -111,35 +113,43 @@ object OpenGroupManager {
|
|||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
fun delete(server: String, room: String, context: Context) {
|
fun delete(server: String, room: String, context: Context) {
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
try {
|
||||||
val configFactory = MessagingModuleConfiguration.shared.configFactory
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val threadDB = DatabaseComponent.get(context).threadDatabase()
|
val configFactory = MessagingModuleConfiguration.shared.configFactory
|
||||||
val openGroupID = "${server.removeSuffix("/")}.$room"
|
val threadDB = DatabaseComponent.get(context).threadDatabase()
|
||||||
val threadID = GroupManager.getOpenGroupThreadID(openGroupID, context)
|
val openGroupID = "${server.removeSuffix("/")}.$room"
|
||||||
val recipient = threadDB.getRecipientForThreadId(threadID) ?: return
|
val threadID = GroupManager.getOpenGroupThreadID(openGroupID, context)
|
||||||
threadDB.setThreadArchived(threadID)
|
val recipient = threadDB.getRecipientForThreadId(threadID) ?: return
|
||||||
val groupID = recipient.address.serialize()
|
threadDB.setThreadArchived(threadID)
|
||||||
// Stop the poller if needed
|
val groupID = recipient.address.serialize()
|
||||||
val openGroups = storage.getAllOpenGroups().filter { it.value.server == server }
|
// Stop the poller if needed
|
||||||
if (openGroups.isNotEmpty()) {
|
val openGroups = storage.getAllOpenGroups().filter { it.value.server == server }
|
||||||
synchronized(pollUpdaterLock) {
|
if (openGroups.isNotEmpty()) {
|
||||||
val poller = pollers[server]
|
synchronized(pollUpdaterLock) {
|
||||||
poller?.stop()
|
val poller = pollers[server]
|
||||||
pollers.remove(server)
|
poller?.stop()
|
||||||
|
pollers.remove(server)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
configFactory.userGroups?.eraseCommunity(server, room)
|
||||||
|
configFactory.convoVolatile?.eraseCommunity(server, room)
|
||||||
|
// Delete
|
||||||
|
storage.removeLastDeletionServerID(room, server)
|
||||||
|
storage.removeLastMessageServerID(room, server)
|
||||||
|
storage.removeLastInboxMessageId(server)
|
||||||
|
storage.removeLastOutboxMessageId(server)
|
||||||
|
val lokiThreadDB = DatabaseComponent.get(context).lokiThreadDatabase()
|
||||||
|
lokiThreadDB.removeOpenGroupChat(threadID)
|
||||||
|
storage.deleteConversation(threadID) // Must be invoked on a background thread
|
||||||
|
GroupManager.deleteGroup(groupID, context) // Must be invoked on a background thread
|
||||||
|
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
|
||||||
|
}
|
||||||
|
catch (e: Exception) {
|
||||||
|
Log.e("Loki", "Failed to leave (delete) community", e)
|
||||||
|
val serverAndRoom = "$server.$room"
|
||||||
|
val txt = Phrase.from(context, R.string.communityLeaveError).put(COMMUNITY_NAME_KEY, serverAndRoom).format().toString()
|
||||||
|
Toast.makeText(context, txt, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
configFactory.userGroups?.eraseCommunity(server, room)
|
|
||||||
configFactory.convoVolatile?.eraseCommunity(server, room)
|
|
||||||
// Delete
|
|
||||||
storage.removeLastDeletionServerID(room, server)
|
|
||||||
storage.removeLastMessageServerID(room, server)
|
|
||||||
storage.removeLastInboxMessageId(server)
|
|
||||||
storage.removeLastOutboxMessageId(server)
|
|
||||||
val lokiThreadDB = DatabaseComponent.get(context).lokiThreadDatabase()
|
|
||||||
lokiThreadDB.removeOpenGroupChat(threadID)
|
|
||||||
storage.deleteConversation(threadID) // Must be invoked on a background thread
|
|
||||||
GroupManager.deleteGroup(groupID, context) // Must be invoked on a background thread
|
|
||||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
|
@ -136,7 +136,7 @@ class ConversationView : LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getTitle(recipient: Recipient): String? = when {
|
private fun getTitle(recipient: Recipient): String? = when {
|
||||||
recipient.isLocalNumber -> context.getString(R.string.note_to_self)
|
recipient.isLocalNumber -> context.getString(R.string.noteToSelf)
|
||||||
else -> recipient.toShortString() // Internally uses the Contact API
|
else -> recipient.toShortString() // Internally uses the Contact API
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,7 +147,7 @@ class ConversationView : LinearLayout {
|
|||||||
|
|
||||||
private fun ThreadRecord.getSnippetPrefix(): CharSequence? = when {
|
private fun ThreadRecord.getSnippetPrefix(): CharSequence? = when {
|
||||||
recipient.isLocalNumber || lastMessage?.isControlMessage == true -> null
|
recipient.isLocalNumber || lastMessage?.isControlMessage == true -> null
|
||||||
lastMessage?.isOutgoing == true -> resources.getString(R.string.MessageRecord_you)
|
lastMessage?.isOutgoing == true -> resources.getString(R.string.you)
|
||||||
else -> lastMessage?.individualRecipient?.toShortString()
|
else -> lastMessage?.individualRecipient?.toShortString()
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
@ -9,20 +9,25 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.squareup.phrase.Phrase
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.utilities.NonTranslatableStringConstants.WAVING_HAND_EMOJI
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.EMOJI_KEY
|
||||||
import org.thoughtcrime.securesms.ui.Divider
|
import org.thoughtcrime.securesms.ui.Divider
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.LocalColors
|
||||||
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
|
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.LocalType
|
||||||
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
|
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
|
||||||
import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider
|
import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider
|
||||||
import org.thoughtcrime.securesms.ui.theme.ThemeColors
|
import org.thoughtcrime.securesms.ui.theme.ThemeColors
|
||||||
import org.thoughtcrime.securesms.ui.theme.LocalColors
|
|
||||||
import org.thoughtcrime.securesms.ui.theme.LocalType
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun EmptyView(newAccount: Boolean) {
|
internal fun EmptyView(newAccount: Boolean) {
|
||||||
@ -44,7 +49,13 @@ internal fun EmptyView(newAccount: Boolean) {
|
|||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.welcome_to_session),
|
stringResource(R.string.onboardingBubbleWelcomeToSession).let { txt ->
|
||||||
|
val c = LocalContext.current
|
||||||
|
Phrase.from(txt)
|
||||||
|
.put(APP_NAME_KEY, c.getString(R.string.app_name))
|
||||||
|
.put(EMOJI_KEY, WAVING_HAND_EMOJI)
|
||||||
|
.format().toString()
|
||||||
|
},
|
||||||
style = LocalType.current.base,
|
style = LocalType.current.base,
|
||||||
color = LocalColors.current.primary,
|
color = LocalColors.current.primary,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
|
@ -8,8 +8,11 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.format.DateUtils
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
|
import androidx.annotation.PluralsRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
@ -17,6 +20,7 @@ import androidx.lifecycle.Lifecycle
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.squareup.phrase.Phrase
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
@ -42,6 +46,9 @@ import org.session.libsession.utilities.recipients.Recipient
|
|||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.session.libsignal.utilities.ThreadUtils
|
import org.session.libsignal.utilities.ThreadUtils
|
||||||
import org.session.libsignal.utilities.toHexString
|
import org.session.libsignal.utilities.toHexString
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.COUNT_KEY
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.conversation.start.StartConversationFragment
|
import org.thoughtcrime.securesms.conversation.start.StartConversationFragment
|
||||||
@ -73,13 +80,22 @@ import org.thoughtcrime.securesms.showSessionDialog
|
|||||||
import org.thoughtcrime.securesms.ui.setThemedContent
|
import org.thoughtcrime.securesms.ui.setThemedContent
|
||||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||||
import org.thoughtcrime.securesms.util.IP2Country
|
import org.thoughtcrime.securesms.util.IP2Country
|
||||||
|
import org.thoughtcrime.securesms.util.RelativeDay
|
||||||
import org.thoughtcrime.securesms.util.disableClipping
|
import org.thoughtcrime.securesms.util.disableClipping
|
||||||
import org.thoughtcrime.securesms.util.push
|
import org.thoughtcrime.securesms.util.push
|
||||||
import org.thoughtcrime.securesms.util.show
|
import org.thoughtcrime.securesms.util.show
|
||||||
import org.thoughtcrime.securesms.util.start
|
import org.thoughtcrime.securesms.util.start
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.time.Duration.Companion.days
|
||||||
|
import kotlin.time.Duration.Companion.hours
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
// Intent extra keys so we know where we came from
|
||||||
private const val NEW_ACCOUNT = "HomeActivity_NEW_ACCOUNT"
|
private const val NEW_ACCOUNT = "HomeActivity_NEW_ACCOUNT"
|
||||||
private const val FROM_ONBOARDING = "HomeActivity_FROM_ONBOARDING"
|
private const val FROM_ONBOARDING = "HomeActivity_FROM_ONBOARDING"
|
||||||
|
|
||||||
@ -88,6 +104,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
ConversationClickListener,
|
ConversationClickListener,
|
||||||
GlobalSearchInputLayout.GlobalSearchInputLayoutListener {
|
GlobalSearchInputLayout.GlobalSearchInputLayoutListener {
|
||||||
|
|
||||||
|
private val TAG = "HomeActivity"
|
||||||
|
|
||||||
private lateinit var binding: ActivityHomeBinding
|
private lateinit var binding: ActivityHomeBinding
|
||||||
private lateinit var glide: RequestManager
|
private lateinit var glide: RequestManager
|
||||||
|
|
||||||
@ -244,17 +262,17 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
globalSearchViewModel.result.map { result ->
|
globalSearchViewModel.result.map { result ->
|
||||||
result.query to when {
|
result.query to when {
|
||||||
result.query.isEmpty() -> buildList {
|
result.query.isEmpty() -> buildList {
|
||||||
add(GlobalSearchAdapter.Model.Header(R.string.contacts))
|
add(GlobalSearchAdapter.Model.Header(R.string.contactContacts))
|
||||||
add(GlobalSearchAdapter.Model.SavedMessages(publicKey))
|
add(GlobalSearchAdapter.Model.SavedMessages(publicKey))
|
||||||
addAll(result.groupedContacts)
|
addAll(result.groupedContacts)
|
||||||
}
|
}
|
||||||
else -> buildList {
|
else -> buildList {
|
||||||
result.contactAndGroupList.takeUnless { it.isEmpty() }?.let {
|
result.contactAndGroupList.takeUnless { it.isEmpty() }?.let {
|
||||||
add(GlobalSearchAdapter.Model.Header(R.string.conversations))
|
add(GlobalSearchAdapter.Model.Header(R.string.sessionConversations))
|
||||||
addAll(it)
|
addAll(it)
|
||||||
}
|
}
|
||||||
result.messageResults.takeUnless { it.isEmpty() }?.let {
|
result.messageResults.takeUnless { it.isEmpty() }?.let {
|
||||||
add(GlobalSearchAdapter.Model.Header(R.string.global_search_messages))
|
add(GlobalSearchAdapter.Model.Header(R.string.messages))
|
||||||
addAll(it)
|
addAll(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -427,7 +445,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
val clip = ClipData.newPlainText("Account ID", thread.recipient.address.toString())
|
val clip = ClipData.newPlainText("Account ID", thread.recipient.address.toString())
|
||||||
val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
manager.setPrimaryClip(clip)
|
manager.setPrimaryClip(clip)
|
||||||
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.copied, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
else if (thread.recipient.isCommunityRecipient) {
|
else if (thread.recipient.isCommunityRecipient) {
|
||||||
val threadId = threadDb.getThreadIdIfExistsFor(thread.recipient)
|
val threadId = threadDb.getThreadIdIfExistsFor(thread.recipient)
|
||||||
@ -436,7 +454,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
val clip = ClipData.newPlainText("Community URL", openGroup.joinURL)
|
val clip = ClipData.newPlainText("Community URL", openGroup.joinURL)
|
||||||
val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
manager.setPrimaryClip(clip)
|
manager.setPrimaryClip(clip)
|
||||||
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.copied, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bottomSheet.onBlockTapped = {
|
bottomSheet.onBlockTapped = {
|
||||||
@ -482,9 +500,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
|
|
||||||
private fun blockConversation(thread: ThreadRecord) {
|
private fun blockConversation(thread: ThreadRecord) {
|
||||||
showSessionDialog {
|
showSessionDialog {
|
||||||
title(R.string.RecipientPreferenceActivity_block_this_contact_question)
|
title(R.string.block)
|
||||||
text(R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact)
|
text(Phrase.from(context, R.string.blockDescription)
|
||||||
button(R.string.RecipientPreferenceActivity_block) {
|
.put(NAME_KEY, thread.recipient.name)
|
||||||
|
.format())
|
||||||
|
dangerButton(R.string.block, R.string.AccessibilityId_blockConfirm) {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
storage.setBlocked(listOf(thread.recipient), true)
|
storage.setBlocked(listOf(thread.recipient), true)
|
||||||
|
|
||||||
@ -492,6 +512,9 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Block confirmation toast added as per SS-64
|
||||||
|
val txt = Phrase.from(context, R.string.blockBlockedUser).put(NAME_KEY, thread.recipient.name).format().toString()
|
||||||
|
Toast.makeText(context, txt, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
cancelButton()
|
cancelButton()
|
||||||
}
|
}
|
||||||
@ -499,12 +522,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
|
|
||||||
private fun unblockConversation(thread: ThreadRecord) {
|
private fun unblockConversation(thread: ThreadRecord) {
|
||||||
showSessionDialog {
|
showSessionDialog {
|
||||||
title(R.string.RecipientPreferenceActivity_unblock_this_contact_question)
|
title(R.string.blockUnblock)
|
||||||
text(R.string.RecipientPreferenceActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact)
|
text(Phrase.from(context, R.string.blockUnblockName).put(NAME_KEY, thread.recipient.name).format())
|
||||||
button(R.string.RecipientPreferenceActivity_unblock) {
|
dangerButton(R.string.blockUnblock, R.string.AccessibilityId_unblockConfirm) {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
storage.setBlocked(listOf(thread.recipient), false)
|
storage.setBlocked(listOf(thread.recipient), false)
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
@ -559,18 +581,42 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
private fun deleteConversation(thread: ThreadRecord) {
|
private fun deleteConversation(thread: ThreadRecord) {
|
||||||
val threadID = thread.threadId
|
val threadID = thread.threadId
|
||||||
val recipient = thread.recipient
|
val recipient = thread.recipient
|
||||||
val message = if (recipient.isGroupRecipient) {
|
val title: String
|
||||||
|
val message: CharSequence
|
||||||
|
|
||||||
|
if (recipient.isGroupRecipient) {
|
||||||
val group = groupDatabase.getGroup(recipient.address.toString()).orNull()
|
val group = groupDatabase.getGroup(recipient.address.toString()).orNull()
|
||||||
|
|
||||||
|
// If you are an admin of this group you can delete it
|
||||||
if (group != null && group.admins.map { it.toString() }.contains(textSecurePreferences.getLocalNumber())) {
|
if (group != null && group.admins.map { it.toString() }.contains(textSecurePreferences.getLocalNumber())) {
|
||||||
getString(R.string.admin_group_leave_warning)
|
title = getString(R.string.groupDelete)
|
||||||
|
message = Phrase.from(this.applicationContext, R.string.groupDeleteDescription)
|
||||||
|
.put(GROUP_NAME_KEY, group.title)
|
||||||
|
.format()
|
||||||
} else {
|
} else {
|
||||||
resources.getString(R.string.activity_home_leave_group_dialog_message)
|
// Otherwise this is either a community, or it's a group you're not an admin of
|
||||||
|
title = if (recipient.isCommunityRecipient) getString(R.string.communityLeave) else getString(R.string.groupLeave)
|
||||||
|
message = Phrase.from(this.applicationContext, R.string.groupLeaveDescription)
|
||||||
|
.put(GROUP_NAME_KEY, group.title)
|
||||||
|
.format()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
resources.getString(R.string.activity_home_delete_conversation_dialog_message)
|
// If this is a 1-on-1 conversation
|
||||||
|
if (recipient.name != null) {
|
||||||
|
title = getString(R.string.conversationsDelete)
|
||||||
|
message = Phrase.from(this.applicationContext, R.string.conversationsDeleteDescription)
|
||||||
|
.put(NAME_KEY, recipient.name)
|
||||||
|
.format()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If not group-related and we don't have a recipient name then this must be our Note to Self conversation
|
||||||
|
title = getString(R.string.noteToSelf)
|
||||||
|
message = getString(R.string.clearMessagesNoteToSelfDescription)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showSessionDialog {
|
showSessionDialog {
|
||||||
|
title(title)
|
||||||
text(message)
|
text(message)
|
||||||
button(R.string.yes) {
|
button(R.string.yes) {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
@ -583,7 +629,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
GroupUtil.doubleDecodeGroupID(recipient.address.toString()).toHexString()
|
GroupUtil.doubleDecodeGroupID(recipient.address.toString()).toHexString()
|
||||||
.takeIf(DatabaseComponent.get(context).lokiAPIDatabase()::isClosedGroup)
|
.takeIf(DatabaseComponent.get(context).lokiAPIDatabase()::isClosedGroup)
|
||||||
?.let { MessageSender.explicitLeave(it, false) }
|
?.let { MessageSender.explicitLeave(it, false) }
|
||||||
} catch (_: IOException) {
|
} catch (ioe: IOException) {
|
||||||
|
Log.w(TAG, "Got an IOException while sending leave group message")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Delete the conversation
|
// Delete the conversation
|
||||||
@ -597,8 +644,9 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
}
|
}
|
||||||
// Update the badge count
|
// Update the badge count
|
||||||
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context)
|
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context)
|
||||||
|
|
||||||
// Notify the user
|
// Notify the user
|
||||||
val toastMessage = if (recipient.isGroupRecipient) R.string.MessageRecord_left_group else R.string.activity_home_conversation_deleted_message
|
val toastMessage = if (recipient.isGroupRecipient) R.string.groupMemberYouLeft else R.string.conversationsDeleted
|
||||||
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show()
|
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -618,7 +666,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
|
|
||||||
private fun hideMessageRequests() {
|
private fun hideMessageRequests() {
|
||||||
showSessionDialog {
|
showSessionDialog {
|
||||||
text(getString(R.string.hide_message_requests))
|
text(getString(R.string.hide))
|
||||||
button(R.string.yes) {
|
button(R.string.yes) {
|
||||||
textSecurePreferences.setHasHiddenMessageRequests()
|
textSecurePreferences.setHasHiddenMessageRequests()
|
||||||
homeViewModel.tryReload()
|
homeViewModel.tryReload()
|
||||||
|
@ -27,9 +27,12 @@ import kotlinx.coroutines.withContext
|
|||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ActivityPathBinding
|
import network.loki.messenger.databinding.ActivityPathBinding
|
||||||
import org.session.libsession.snode.OnionRequestAPI
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
|
import org.session.libsession.utilities.NonTranslatableStringConstants.APP_NAME
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
|
||||||
import org.session.libsession.utilities.getColorFromAttr
|
import org.session.libsession.utilities.getColorFromAttr
|
||||||
import org.session.libsignal.utilities.Snode
|
import org.session.libsignal.utilities.Snode
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
|
import org.thoughtcrime.securesms.ui.getSubbedString
|
||||||
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
||||||
import org.thoughtcrime.securesms.util.IP2Country
|
import org.thoughtcrime.securesms.util.IP2Country
|
||||||
import org.thoughtcrime.securesms.util.PathDotView
|
import org.thoughtcrime.securesms.util.PathDotView
|
||||||
@ -49,7 +52,12 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
super.onCreate(savedInstanceState, isReady)
|
super.onCreate(savedInstanceState, isReady)
|
||||||
binding = ActivityPathBinding.inflate(layoutInflater)
|
binding = ActivityPathBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
supportActionBar!!.title = resources.getString(R.string.activity_path_title)
|
supportActionBar!!.title = resources.getString(R.string.onionRoutingPath)
|
||||||
|
|
||||||
|
// Substitute "Session" into the path description. Note: This is a non-translatable string.
|
||||||
|
val txt = applicationContext.getSubbedString(R.string.onionRoutingPathDescription,APP_NAME_KEY to APP_NAME)
|
||||||
|
binding.pathDescription.text = txt
|
||||||
|
|
||||||
binding.pathRowsContainer.disableClipping()
|
binding.pathRowsContainer.disableClipping()
|
||||||
binding.learnMoreButton.setOnClickListener { learnMore() }
|
binding.learnMoreButton.setOnClickListener { learnMore() }
|
||||||
update(false)
|
update(false)
|
||||||
@ -98,6 +106,7 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
|
|
||||||
private fun update(isAnimated: Boolean) {
|
private fun update(isAnimated: Boolean) {
|
||||||
binding.pathRowsContainer.removeAllViews()
|
binding.pathRowsContainer.removeAllViews()
|
||||||
|
|
||||||
if (OnionRequestAPI.paths.isNotEmpty()) {
|
if (OnionRequestAPI.paths.isNotEmpty()) {
|
||||||
val path = OnionRequestAPI.paths.firstOrNull() ?: return finish()
|
val path = OnionRequestAPI.paths.firstOrNull() ?: return finish()
|
||||||
val dotAnimationRepeatInterval = path.count().toLong() * 1000 + 1000
|
val dotAnimationRepeatInterval = path.count().toLong() * 1000 + 1000
|
||||||
@ -105,8 +114,8 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
val isGuardSnode = (OnionRequestAPI.guardSnodes.contains(snode))
|
val isGuardSnode = (OnionRequestAPI.guardSnodes.contains(snode))
|
||||||
getPathRow(snode, LineView.Location.Middle, index.toLong() * 1000 + 2000, dotAnimationRepeatInterval, isGuardSnode)
|
getPathRow(snode, LineView.Location.Middle, index.toLong() * 1000 + 2000, dotAnimationRepeatInterval, isGuardSnode)
|
||||||
}
|
}
|
||||||
val youRow = getPathRow(resources.getString(R.string.activity_path_device_row_title), null, LineView.Location.Top, 1000, dotAnimationRepeatInterval)
|
val youRow = getPathRow(resources.getString(R.string.onionRoutingPath), null, LineView.Location.Top, 1000, dotAnimationRepeatInterval)
|
||||||
val destinationRow = getPathRow(resources.getString(R.string.activity_path_destination_row_title), null, LineView.Location.Bottom, path.count().toLong() * 1000 + 2000, dotAnimationRepeatInterval)
|
val destinationRow = getPathRow(resources.getString(R.string.onionRoutingPathDestination), null, LineView.Location.Bottom, path.count().toLong() * 1000 + 2000, dotAnimationRepeatInterval)
|
||||||
val rows = listOf( youRow ) + pathRows + listOf( destinationRow )
|
val rows = listOf( youRow ) + pathRows + listOf( destinationRow )
|
||||||
for (row in rows) {
|
for (row in rows) {
|
||||||
binding.pathRowsContainer.addView(row)
|
binding.pathRowsContainer.addView(row)
|
||||||
@ -162,11 +171,11 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getPathRow(snode: Snode, location: LineView.Location, dotAnimationStartDelay: Long, dotAnimationRepeatInterval: Long, isGuardSnode: Boolean): LinearLayout {
|
private fun getPathRow(snode: Snode, location: LineView.Location, dotAnimationStartDelay: Long, dotAnimationRepeatInterval: Long, isGuardSnode: Boolean): LinearLayout {
|
||||||
val title = if (isGuardSnode) resources.getString(R.string.activity_path_guard_node_row_title) else resources.getString(R.string.activity_path_service_node_row_title)
|
val title = if (isGuardSnode) resources.getString(R.string.onionRoutingPathEntryNode) else resources.getString(R.string.onionRoutingPathServiceNode)
|
||||||
val subtitle = if (IP2Country.isInitialized) {
|
val subtitle = if (IP2Country.isInitialized) {
|
||||||
IP2Country.shared.countryNamesCache[snode.ip] ?: resources.getString(R.string.activity_path_resolving_progress)
|
IP2Country.shared.countryNamesCache[snode.ip] ?: resources.getString(R.string.resolving)
|
||||||
} else {
|
} else {
|
||||||
resources.getString(R.string.activity_path_resolving_progress)
|
resources.getString(R.string.resolving)
|
||||||
}
|
}
|
||||||
return getPathRow(title, subtitle, location, dotAnimationStartDelay, dotAnimationRepeatInterval)
|
return getPathRow(title, subtitle, location, dotAnimationStartDelay, dotAnimationRepeatInterval)
|
||||||
}
|
}
|
||||||
@ -179,7 +188,7 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Toast.makeText(this, R.string.invalid_url, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.communityEnterUrlErrorInvalid, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
@ -250,13 +259,11 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
|
|
||||||
override fun onAttachedToWindow() {
|
override fun onAttachedToWindow() {
|
||||||
super.onAttachedToWindow()
|
super.onAttachedToWindow()
|
||||||
|
|
||||||
startAnimation()
|
startAnimation()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDetachedFromWindow() {
|
override fun onDetachedFromWindow() {
|
||||||
super.onDetachedFromWindow()
|
super.onDetachedFromWindow()
|
||||||
|
|
||||||
stopAnimation()
|
stopAnimation()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,15 +18,15 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
|
|
||||||
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
|
|
||||||
import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider
|
|
||||||
import org.thoughtcrime.securesms.ui.SessionShieldIcon
|
import org.thoughtcrime.securesms.ui.SessionShieldIcon
|
||||||
import org.thoughtcrime.securesms.ui.theme.ThemeColors
|
|
||||||
import org.thoughtcrime.securesms.ui.theme.LocalColors
|
|
||||||
import org.thoughtcrime.securesms.ui.components.SlimPrimaryOutlineButton
|
import org.thoughtcrime.securesms.ui.components.SlimPrimaryOutlineButton
|
||||||
import org.thoughtcrime.securesms.ui.contentDescription
|
import org.thoughtcrime.securesms.ui.contentDescription
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.LocalColors
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
|
||||||
import org.thoughtcrime.securesms.ui.theme.LocalType
|
import org.thoughtcrime.securesms.ui.theme.LocalType
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.ThemeColors
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SeedReminder(startRecoveryPasswordActivity: () -> Unit) {
|
internal fun SeedReminder(startRecoveryPasswordActivity: () -> Unit) {
|
||||||
@ -49,23 +49,23 @@ internal fun SeedReminder(startRecoveryPasswordActivity: () -> Unit) {
|
|||||||
Column(Modifier.weight(1f)) {
|
Column(Modifier.weight(1f)) {
|
||||||
Row {
|
Row {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.save_your_recovery_password),
|
stringResource(R.string.recoveryPasswordBannerTitle),
|
||||||
style = LocalType.current.h8
|
style = LocalType.current.h8
|
||||||
)
|
)
|
||||||
Spacer(Modifier.requiredWidth(LocalDimensions.current.xxsSpacing))
|
Spacer(Modifier.requiredWidth(LocalDimensions.current.xxsSpacing))
|
||||||
SessionShieldIcon()
|
SessionShieldIcon()
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.save_your_recovery_password_to_make_sure_you_don_t_lose_access_to_your_account),
|
stringResource(R.string.recoveryPasswordBannerDescription),
|
||||||
style = LocalType.current.small
|
style = LocalType.current.small
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.width(LocalDimensions.current.xsSpacing))
|
Spacer(Modifier.width(LocalDimensions.current.xsSpacing))
|
||||||
SlimPrimaryOutlineButton(
|
SlimPrimaryOutlineButton(
|
||||||
text = stringResource(R.string.continue_2),
|
text = stringResource(R.string.theContinue),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.CenterVertically)
|
.align(Alignment.CenterVertically)
|
||||||
.contentDescription(R.string.AccessibilityId_reveal_recovery_phrase_button),
|
.contentDescription(R.string.AccessibilityId_recoveryPasswordBanner),
|
||||||
onClick = startRecoveryPasswordActivity
|
onClick = startRecoveryPasswordActivity
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -78,6 +78,6 @@ private fun PreviewSeedReminder(
|
|||||||
@PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors
|
@PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors
|
||||||
) {
|
) {
|
||||||
PreviewTheme(colors) {
|
PreviewTheme(colors) {
|
||||||
SeedReminder {}
|
SeedReminder { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -100,7 +100,7 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
|
|||||||
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
val clip = ClipData.newPlainText("Account ID", publicKey)
|
val clip = ClipData.newPlainText("Account ID", publicKey)
|
||||||
clipboard.setPrimaryClip(clip)
|
clipboard.setPrimaryClip(clip)
|
||||||
Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT)
|
Toast.makeText(requireContext(), R.string.copied, Toast.LENGTH_SHORT)
|
||||||
.show()
|
.show()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
@ -6,12 +6,14 @@ import android.text.SpannableStringBuilder
|
|||||||
import android.text.style.StyleSpan
|
import android.text.style.StyleSpan
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import java.util.Locale
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsession.utilities.truncateIdForDisplay
|
import org.session.libsession.utilities.truncateIdForDisplay
|
||||||
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.ContentView
|
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.ContentView
|
||||||
|
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Contact as ContactModel
|
||||||
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.GroupConversation
|
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.GroupConversation
|
||||||
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Header
|
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Header
|
||||||
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Message
|
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Message
|
||||||
@ -19,9 +21,6 @@ import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.SavedMes
|
|||||||
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.SubHeader
|
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.SubHeader
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
import org.thoughtcrime.securesms.util.SearchUtil
|
import org.thoughtcrime.securesms.util.SearchUtil
|
||||||
import java.util.Locale
|
|
||||||
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Contact as ContactModel
|
|
||||||
|
|
||||||
|
|
||||||
class GlobalSearchDiff(
|
class GlobalSearchDiff(
|
||||||
private val oldQuery: String?,
|
private val oldQuery: String?,
|
||||||
@ -78,8 +77,8 @@ fun ContentView.bindQuery(query: String, model: GlobalSearchAdapter.Model) {
|
|||||||
}
|
}
|
||||||
binding.searchResultSubtitle.text = getHighlight(query, membersString)
|
binding.searchResultSubtitle.text = getHighlight(query, membersString)
|
||||||
}
|
}
|
||||||
is Header, // do nothing for header
|
is Header, // do nothing for header
|
||||||
is SubHeader, // do nothing for subheader
|
is SubHeader, // do nothing for subheader
|
||||||
is SavedMessages -> Unit // do nothing for saved messages (displays note to self)
|
is SavedMessages -> Unit // do nothing for saved messages (displays note to self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -112,7 +111,7 @@ fun ContentView.bindModel(query: String?, model: ContactModel) = binding.run {
|
|||||||
searchResultSubtitle.text = null
|
searchResultSubtitle.text = null
|
||||||
val recipient = Recipient.from(root.context, Address.fromSerialized(model.contact.accountID), false)
|
val recipient = Recipient.from(root.context, Address.fromSerialized(model.contact.accountID), false)
|
||||||
searchResultProfilePicture.update(recipient)
|
searchResultProfilePicture.update(recipient)
|
||||||
val nameString = if (model.isSelf) root.context.getString(R.string.note_to_self)
|
val nameString = if (model.isSelf) root.context.getString(R.string.noteToSelf)
|
||||||
else model.contact.getSearchName()
|
else model.contact.getSearchName()
|
||||||
searchResultTitle.text = getHighlight(query, nameString)
|
searchResultTitle.text = getHighlight(query, nameString)
|
||||||
}
|
}
|
||||||
@ -120,7 +119,7 @@ fun ContentView.bindModel(query: String?, model: ContactModel) = binding.run {
|
|||||||
fun ContentView.bindModel(model: SavedMessages) {
|
fun ContentView.bindModel(model: SavedMessages) {
|
||||||
binding.searchResultSubtitle.isVisible = false
|
binding.searchResultSubtitle.isVisible = false
|
||||||
binding.searchResultTimestamp.isVisible = false
|
binding.searchResultTimestamp.isVisible = false
|
||||||
binding.searchResultTitle.setText(R.string.note_to_self)
|
binding.searchResultTitle.setText(R.string.noteToSelf)
|
||||||
binding.searchResultProfilePicture.update(Address.fromSerialized(model.currentUserPublicKey))
|
binding.searchResultProfilePicture.update(Address.fromSerialized(model.currentUserPublicKey))
|
||||||
binding.searchResultProfilePicture.isVisible = true
|
binding.searchResultProfilePicture.isVisible = true
|
||||||
}
|
}
|
||||||
@ -128,11 +127,13 @@ fun ContentView.bindModel(model: SavedMessages) {
|
|||||||
fun ContentView.bindModel(query: String?, model: Message) = binding.apply {
|
fun ContentView.bindModel(query: String?, model: Message) = binding.apply {
|
||||||
searchResultProfilePicture.isVisible = true
|
searchResultProfilePicture.isVisible = true
|
||||||
searchResultTimestamp.isVisible = true
|
searchResultTimestamp.isVisible = true
|
||||||
|
|
||||||
// val hasUnreads = model.unread > 0
|
// val hasUnreads = model.unread > 0
|
||||||
// unreadCountIndicator.isVisible = hasUnreads
|
// unreadCountIndicator.isVisible = hasUnreads
|
||||||
// if (hasUnreads) {
|
// if (hasUnreads) {
|
||||||
// unreadCountTextView.text = model.unread.toString()
|
// unreadCountTextView.text = model.unread.toString()
|
||||||
// }
|
// }
|
||||||
|
|
||||||
searchResultTimestamp.text = DateUtils.getDisplayFormattedTimeSpanString(root.context, Locale.getDefault(), model.messageResult.sentTimestampMs)
|
searchResultTimestamp.text = DateUtils.getDisplayFormattedTimeSpanString(root.context, Locale.getDefault(), model.messageResult.sentTimestampMs)
|
||||||
searchResultProfilePicture.update(model.messageResult.conversationRecipient)
|
searchResultProfilePicture.update(model.messageResult.conversationRecipient)
|
||||||
val textSpannable = SpannableStringBuilder()
|
val textSpannable = SpannableStringBuilder()
|
||||||
@ -146,7 +147,7 @@ fun ContentView.bindModel(query: String?, model: Message) = binding.apply {
|
|||||||
model.messageResult.bodySnippet
|
model.messageResult.bodySnippet
|
||||||
))
|
))
|
||||||
searchResultSubtitle.text = textSpannable
|
searchResultSubtitle.text = textSpannable
|
||||||
searchResultTitle.text = if (model.isSelf) root.context.getString(R.string.note_to_self)
|
searchResultTitle.text = if (model.isSelf) root.context.getString(R.string.noteToSelf)
|
||||||
else model.messageResult.conversationRecipient.getSearchName()
|
else model.messageResult.conversationRecipient.getSearchName()
|
||||||
searchResultSubtitle.isVisible = true
|
searchResultSubtitle.isVisible = true
|
||||||
}
|
}
|
||||||
|
@ -1,216 +1,233 @@
|
|||||||
package org.thoughtcrime.securesms.linkpreview;
|
package org.thoughtcrime.securesms.linkpreview;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
import android.os.Build;
|
||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.style.URLSpan;
|
import android.text.style.URLSpan;
|
||||||
import android.text.util.Linkify;
|
import android.text.util.Linkify;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
import java.text.ParseException;
|
||||||
import org.thoughtcrime.securesms.util.DateUtils;
|
import java.text.SimpleDateFormat;
|
||||||
import org.session.libsignal.utilities.guava.Optional;
|
|
||||||
|
|
||||||
import org.session.libsession.utilities.Util;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import okhttp3.HttpUrl;
|
import okhttp3.HttpUrl;
|
||||||
|
import org.session.libsession.utilities.Util;
|
||||||
|
import org.session.libsignal.utilities.Log;
|
||||||
|
import org.session.libsignal.utilities.guava.Optional;
|
||||||
|
|
||||||
public final class LinkPreviewUtil {
|
public final class LinkPreviewUtil {
|
||||||
|
|
||||||
private static final Pattern DOMAIN_PATTERN = Pattern.compile("^(https?://)?([^/]+).*$", Pattern.CASE_INSENSITIVE);
|
private static final Pattern DOMAIN_PATTERN = Pattern.compile("^(https?://)?([^/]+).*$", Pattern.CASE_INSENSITIVE);
|
||||||
private static final Pattern ALL_ASCII_PATTERN = Pattern.compile("^[\\x00-\\x7F]*$", Pattern.CASE_INSENSITIVE);
|
private static final Pattern ALL_ASCII_PATTERN = Pattern.compile("^[\\x00-\\x7F]*$", Pattern.CASE_INSENSITIVE);
|
||||||
private static final Pattern ALL_NON_ASCII_PATTERN = Pattern.compile("^[^\\x00-\\x7F]*$", Pattern.CASE_INSENSITIVE);
|
private static final Pattern ALL_NON_ASCII_PATTERN = Pattern.compile("^[^\\x00-\\x7F]*$", Pattern.CASE_INSENSITIVE);
|
||||||
private static final Pattern OPEN_GRAPH_TAG_PATTERN = Pattern.compile("<\\s*meta[^>]*property\\s*=\\s*\"\\s*og:([^\"]+)\"[^>]*/?\\s*>", Pattern.CASE_INSENSITIVE);
|
private static final Pattern OPEN_GRAPH_TAG_PATTERN = Pattern.compile("<\\s*meta[^>]*property\\s*=\\s*\"\\s*og:([^\"]+)\"[^>]*/?\\s*>", Pattern.CASE_INSENSITIVE);
|
||||||
private static final Pattern ARTICLE_TAG_PATTERN = Pattern.compile("<\\s*meta[^>]*property\\s*=\\s*\"\\s*article:([^\"]+)\"[^>]*/?\\s*>", Pattern.CASE_INSENSITIVE);
|
private static final Pattern ARTICLE_TAG_PATTERN = Pattern.compile("<\\s*meta[^>]*property\\s*=\\s*\"\\s*article:([^\"]+)\"[^>]*/?\\s*>", Pattern.CASE_INSENSITIVE);
|
||||||
private static final Pattern OPEN_GRAPH_CONTENT_PATTERN = Pattern.compile("content\\s*=\\s*\"([^\"]*)\"", Pattern.CASE_INSENSITIVE);
|
private static final Pattern OPEN_GRAPH_CONTENT_PATTERN = Pattern.compile("content\\s*=\\s*\"([^\"]*)\"", Pattern.CASE_INSENSITIVE);
|
||||||
private static final Pattern TITLE_PATTERN = Pattern.compile("<\\s*title[^>]*>(.*)<\\s*/title[^>]*>", Pattern.CASE_INSENSITIVE);
|
private static final Pattern TITLE_PATTERN = Pattern.compile("<\\s*title[^>]*>(.*)<\\s*/title[^>]*>", Pattern.CASE_INSENSITIVE);
|
||||||
private static final Pattern FAVICON_PATTERN = Pattern.compile("<\\s*link[^>]*rel\\s*=\\s*\".*icon.*\"[^>]*>", Pattern.CASE_INSENSITIVE);
|
private static final Pattern FAVICON_PATTERN = Pattern.compile("<\\s*link[^>]*rel\\s*=\\s*\".*icon.*\"[^>]*>", Pattern.CASE_INSENSITIVE);
|
||||||
private static final Pattern FAVICON_HREF_PATTERN = Pattern.compile("href\\s*=\\s*\"([^\"]*)\"", Pattern.CASE_INSENSITIVE);
|
private static final Pattern FAVICON_HREF_PATTERN = Pattern.compile("href\\s*=\\s*\"([^\"]*)\"", Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return All whitelisted URLs in the source text.
|
* @return All whitelisted URLs in the source text.
|
||||||
*/
|
*/
|
||||||
public static @NonNull List<Link> findWhitelistedUrls(@NonNull String text) {
|
public static @NonNull List<Link> findWhitelistedUrls(@NonNull String text) {
|
||||||
SpannableString spannable = new SpannableString(text);
|
SpannableString spannable = new SpannableString(text);
|
||||||
boolean found = Linkify.addLinks(spannable, Linkify.WEB_URLS);
|
boolean found = Linkify.addLinks(spannable, Linkify.WEB_URLS);
|
||||||
|
|
||||||
if (!found) {
|
if (!found) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
|
||||||
|
|
||||||
return Stream.of(spannable.getSpans(0, spannable.length(), URLSpan.class))
|
|
||||||
.map(span -> new Link(span.getURL(), spannable.getSpanStart(span)))
|
|
||||||
.filter(link -> isValidLinkUrl(link.getUrl()))
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return True if the host is valid.
|
|
||||||
*/
|
|
||||||
public static boolean isValidLinkUrl(@Nullable String linkUrl) {
|
|
||||||
if (linkUrl == null) return false;
|
|
||||||
|
|
||||||
HttpUrl url = HttpUrl.parse(linkUrl);
|
|
||||||
return url != null &&
|
|
||||||
!TextUtils.isEmpty(url.scheme()) &&
|
|
||||||
"https".equals(url.scheme()) &&
|
|
||||||
isLegalUrl(linkUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return True if the top-level domain is valid.
|
|
||||||
*/
|
|
||||||
public static boolean isValidMediaUrl(@Nullable String mediaUrl) {
|
|
||||||
if (mediaUrl == null) return false;
|
|
||||||
|
|
||||||
HttpUrl url = HttpUrl.parse(mediaUrl);
|
|
||||||
return url != null &&
|
|
||||||
!TextUtils.isEmpty(url.scheme()) &&
|
|
||||||
"https".equals(url.scheme()) &&
|
|
||||||
isLegalUrl(mediaUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isLegalUrl(@NonNull String url) {
|
|
||||||
Matcher matcher = DOMAIN_PATTERN.matcher(url);
|
|
||||||
|
|
||||||
if (matcher.matches()) {
|
|
||||||
String domain = matcher.group(2);
|
|
||||||
String cleanedDomain = domain.replaceAll("\\.", "");
|
|
||||||
|
|
||||||
return ALL_ASCII_PATTERN.matcher(cleanedDomain).matches() ||
|
|
||||||
ALL_NON_ASCII_PATTERN.matcher(cleanedDomain).matches();
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isValidMimeType(@NonNull String url) {
|
|
||||||
String[] validMimeType = {".jpg", ".png", ".gif", ".jpeg"};
|
|
||||||
if (url.contains(".")) {
|
|
||||||
for (String mimeType : validMimeType) {
|
|
||||||
if (url.contains(mimeType)) {
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static @NonNull OpenGraph parseOpenGraphFields(@Nullable String html) {
|
return Stream.of(spannable.getSpans(0, spannable.length(), URLSpan.class))
|
||||||
return parseOpenGraphFields(html, text -> Html.fromHtml(text).toString());
|
.map(span -> new Link(span.getURL(), spannable.getSpanStart(span)))
|
||||||
}
|
.filter(link -> isValidLinkUrl(link.getUrl()))
|
||||||
|
.toList();
|
||||||
static @NonNull OpenGraph parseOpenGraphFields(@Nullable String html, @NonNull HtmlDecoder htmlDecoder) {
|
|
||||||
if (html == null) {
|
|
||||||
return new OpenGraph(Collections.emptyMap(), null, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, String> openGraphTags = new HashMap<>();
|
/**
|
||||||
Matcher openGraphMatcher = OPEN_GRAPH_TAG_PATTERN.matcher(html);
|
* @return True if the host is valid.
|
||||||
|
*/
|
||||||
|
public static boolean isValidLinkUrl(@Nullable String linkUrl) {
|
||||||
|
if (linkUrl == null) return false;
|
||||||
|
|
||||||
while (openGraphMatcher.find()) {
|
HttpUrl url = HttpUrl.parse(linkUrl);
|
||||||
String tag = openGraphMatcher.group();
|
return url != null &&
|
||||||
String property = openGraphMatcher.groupCount() > 0 ? openGraphMatcher.group(1) : null;
|
!TextUtils.isEmpty(url.scheme()) &&
|
||||||
|
"https".equals(url.scheme()) &&
|
||||||
|
isLegalUrl(linkUrl);
|
||||||
|
}
|
||||||
|
|
||||||
if (property != null) {
|
/**
|
||||||
Matcher contentMatcher = OPEN_GRAPH_CONTENT_PATTERN.matcher(tag);
|
* @return True if the top-level domain is valid.
|
||||||
if (contentMatcher.find() && contentMatcher.groupCount() > 0) {
|
*/
|
||||||
String content = htmlDecoder.fromEncoded(contentMatcher.group(1));
|
public static boolean isValidMediaUrl(@Nullable String mediaUrl) {
|
||||||
openGraphTags.put(property.toLowerCase(), content);
|
if (mediaUrl == null) return false;
|
||||||
|
|
||||||
|
HttpUrl url = HttpUrl.parse(mediaUrl);
|
||||||
|
return url != null &&
|
||||||
|
!TextUtils.isEmpty(url.scheme()) &&
|
||||||
|
"https".equals(url.scheme()) &&
|
||||||
|
isLegalUrl(mediaUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isLegalUrl(@NonNull String url) {
|
||||||
|
Matcher matcher = DOMAIN_PATTERN.matcher(url);
|
||||||
|
|
||||||
|
if (matcher.matches()) {
|
||||||
|
String domain = matcher.group(2);
|
||||||
|
String cleanedDomain = domain.replaceAll("\\.", "");
|
||||||
|
|
||||||
|
return ALL_ASCII_PATTERN.matcher(cleanedDomain).matches() ||
|
||||||
|
ALL_NON_ASCII_PATTERN.matcher(cleanedDomain).matches();
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Matcher articleMatcher = ARTICLE_TAG_PATTERN.matcher(html);
|
public static boolean isValidMimeType(@NonNull String url) {
|
||||||
|
String[] validMimeType = {".jpg", ".png", ".gif", ".jpeg"};
|
||||||
while (articleMatcher.find()) {
|
if (url.contains(".")) {
|
||||||
String tag = articleMatcher.group();
|
for (String mimeType : validMimeType) {
|
||||||
String property = articleMatcher.groupCount() > 0 ? articleMatcher.group(1) : null;
|
if (url.contains(mimeType)) {
|
||||||
|
return true;
|
||||||
if (property != null) {
|
}
|
||||||
Matcher contentMatcher = OPEN_GRAPH_CONTENT_PATTERN.matcher(tag);
|
}
|
||||||
if (contentMatcher.find() && contentMatcher.groupCount() > 0) {
|
return false;
|
||||||
String content = htmlDecoder.fromEncoded(contentMatcher.group(1));
|
|
||||||
openGraphTags.put(property.toLowerCase(), content);
|
|
||||||
}
|
}
|
||||||
}
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
String htmlTitle = "";
|
public static @NonNull OpenGraph parseOpenGraphFields(@Nullable String html) {
|
||||||
String faviconUrl = "";
|
return parseOpenGraphFields(html, text -> Html.fromHtml(text).toString());
|
||||||
|
|
||||||
Matcher titleMatcher = TITLE_PATTERN.matcher(html);
|
|
||||||
if (titleMatcher.find() && titleMatcher.groupCount() > 0) {
|
|
||||||
htmlTitle = htmlDecoder.fromEncoded(titleMatcher.group(1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Matcher faviconMatcher = FAVICON_PATTERN.matcher(html);
|
static @NonNull OpenGraph parseOpenGraphFields(@Nullable String html, @NonNull HtmlDecoder htmlDecoder) {
|
||||||
if (faviconMatcher.find()) {
|
if (html == null) {
|
||||||
Matcher faviconHrefMatcher = FAVICON_HREF_PATTERN.matcher(faviconMatcher.group());
|
return new OpenGraph(Collections.emptyMap(), null, null);
|
||||||
if (faviconHrefMatcher.find() && faviconHrefMatcher.groupCount() > 0) {
|
}
|
||||||
faviconUrl = faviconHrefMatcher.group(1);
|
|
||||||
}
|
Map<String, String> openGraphTags = new HashMap<>();
|
||||||
|
Matcher openGraphMatcher = OPEN_GRAPH_TAG_PATTERN.matcher(html);
|
||||||
|
|
||||||
|
while (openGraphMatcher.find()) {
|
||||||
|
String tag = openGraphMatcher.group();
|
||||||
|
String property = openGraphMatcher.groupCount() > 0 ? openGraphMatcher.group(1) : null;
|
||||||
|
|
||||||
|
if (property != null) {
|
||||||
|
Matcher contentMatcher = OPEN_GRAPH_CONTENT_PATTERN.matcher(tag);
|
||||||
|
if (contentMatcher.find() && contentMatcher.groupCount() > 0) {
|
||||||
|
String content = htmlDecoder.fromEncoded(contentMatcher.group(1));
|
||||||
|
openGraphTags.put(property.toLowerCase(), content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Matcher articleMatcher = ARTICLE_TAG_PATTERN.matcher(html);
|
||||||
|
|
||||||
|
while (articleMatcher.find()) {
|
||||||
|
String tag = articleMatcher.group();
|
||||||
|
String property = articleMatcher.groupCount() > 0 ? articleMatcher.group(1) : null;
|
||||||
|
|
||||||
|
if (property != null) {
|
||||||
|
Matcher contentMatcher = OPEN_GRAPH_CONTENT_PATTERN.matcher(tag);
|
||||||
|
if (contentMatcher.find() && contentMatcher.groupCount() > 0) {
|
||||||
|
String content = htmlDecoder.fromEncoded(contentMatcher.group(1));
|
||||||
|
openGraphTags.put(property.toLowerCase(), content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String htmlTitle = "";
|
||||||
|
String faviconUrl = "";
|
||||||
|
|
||||||
|
Matcher titleMatcher = TITLE_PATTERN.matcher(html);
|
||||||
|
if (titleMatcher.find() && titleMatcher.groupCount() > 0) {
|
||||||
|
htmlTitle = htmlDecoder.fromEncoded(titleMatcher.group(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
Matcher faviconMatcher = FAVICON_PATTERN.matcher(html);
|
||||||
|
if (faviconMatcher.find()) {
|
||||||
|
Matcher faviconHrefMatcher = FAVICON_HREF_PATTERN.matcher(faviconMatcher.group());
|
||||||
|
if (faviconHrefMatcher.find() && faviconHrefMatcher.groupCount() > 0) {
|
||||||
|
faviconUrl = faviconHrefMatcher.group(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new OpenGraph(openGraphTags, htmlTitle, faviconUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new OpenGraph(openGraphTags, htmlTitle, faviconUrl);
|
public static final class OpenGraph {
|
||||||
}
|
|
||||||
|
|
||||||
public static final class OpenGraph {
|
private final Map<String, String> values;
|
||||||
|
|
||||||
private final Map<String, String> values;
|
private final @Nullable String htmlTitle;
|
||||||
|
private final @Nullable String faviconUrl;
|
||||||
|
|
||||||
private final @Nullable String htmlTitle;
|
private static final String KEY_TITLE = "title";
|
||||||
private final @Nullable String faviconUrl;
|
private static final String KEY_DESCRIPTION_URL = "description";
|
||||||
|
private static final String KEY_IMAGE_URL = "image";
|
||||||
|
private static final String KEY_PUBLISHED_TIME_1 = "published_time";
|
||||||
|
private static final String KEY_PUBLISHED_TIME_2 = "article:published_time";
|
||||||
|
private static final String KEY_MODIFIED_TIME_1 = "modified_time";
|
||||||
|
private static final String KEY_MODIFIED_TIME_2 = "article:modified_time";
|
||||||
|
|
||||||
private static final String KEY_TITLE = "title";
|
public OpenGraph(@NonNull Map<String, String> values, @Nullable String htmlTitle, @Nullable String faviconUrl) {
|
||||||
private static final String KEY_DESCRIPTION_URL = "description";
|
this.values = values;
|
||||||
private static final String KEY_IMAGE_URL = "image";
|
this.htmlTitle = htmlTitle;
|
||||||
private static final String KEY_PUBLISHED_TIME_1 = "published_time";
|
this.faviconUrl = faviconUrl;
|
||||||
private static final String KEY_PUBLISHED_TIME_2 = "article:published_time";
|
}
|
||||||
private static final String KEY_MODIFIED_TIME_1 = "modified_time";
|
|
||||||
private static final String KEY_MODIFIED_TIME_2 = "article:modified_time";
|
|
||||||
|
|
||||||
public OpenGraph(@NonNull Map<String, String> values, @Nullable String htmlTitle, @Nullable String faviconUrl) {
|
public @NonNull Optional<String> getTitle() {
|
||||||
this.values = values;
|
return Optional.of(Util.getFirstNonEmpty(values.get(KEY_TITLE), htmlTitle));
|
||||||
this.htmlTitle = htmlTitle;
|
}
|
||||||
this.faviconUrl = faviconUrl;
|
|
||||||
|
public @NonNull Optional<String> getImageUrl() {
|
||||||
|
return Optional.of(Util.getFirstNonEmpty(values.get(KEY_IMAGE_URL), faviconUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long parseISO8601(String date) {
|
||||||
|
|
||||||
|
if (date == null || date.isEmpty()) { return -1L; }
|
||||||
|
|
||||||
|
SimpleDateFormat format;
|
||||||
|
if (Build.VERSION.SDK_INT >= 24) {
|
||||||
|
format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", Locale.getDefault());
|
||||||
|
} else {
|
||||||
|
format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return format.parse(date).getTime();
|
||||||
|
} catch (ParseException pe) {
|
||||||
|
Log.w("OpenGraph", "Failed to parse date.", pe);
|
||||||
|
return -1L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ObsoleteSdkInt")
|
||||||
|
public long getDate() {
|
||||||
|
return Stream.of(values.get(KEY_PUBLISHED_TIME_1),
|
||||||
|
values.get(KEY_PUBLISHED_TIME_2),
|
||||||
|
values.get(KEY_MODIFIED_TIME_1),
|
||||||
|
values.get(KEY_MODIFIED_TIME_2))
|
||||||
|
.map(OpenGraph::parseISO8601)
|
||||||
|
.filter(time -> time > 0)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(0L);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NonNull Optional<String> getTitle() {
|
public interface HtmlDecoder {
|
||||||
return Optional.of(Util.getFirstNonEmpty(values.get(KEY_TITLE), htmlTitle));
|
@NonNull String fromEncoded(@NonNull String html);
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NonNull Optional<String> getImageUrl() {
|
}
|
||||||
return Optional.of(Util.getFirstNonEmpty(values.get(KEY_IMAGE_URL), faviconUrl));
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("ObsoleteSdkInt")
|
|
||||||
public long getDate() {
|
|
||||||
return Stream.of(values.get(KEY_PUBLISHED_TIME_1),
|
|
||||||
values.get(KEY_PUBLISHED_TIME_2),
|
|
||||||
values.get(KEY_MODIFIED_TIME_1),
|
|
||||||
values.get(KEY_MODIFIED_TIME_2))
|
|
||||||
.map(DateUtils::parseIso8601)
|
|
||||||
.filter(time -> time > 0)
|
|
||||||
.findFirst()
|
|
||||||
.orElse(0L);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface HtmlDecoder {
|
|
||||||
@NonNull String fromEncoded(@NonNull String html);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -44,7 +44,7 @@ fun DocumentsPage(
|
|||||||
content.isEmpty() -> {
|
content.isEmpty() -> {
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.media_overview_documents_fragment__no_documents_found),
|
text = stringResource(R.string.attachmentsFilesEmpty),
|
||||||
style = LocalType.current.base,
|
style = LocalType.current.base,
|
||||||
color = LocalColors.current.text
|
color = LocalColors.current.text
|
||||||
)
|
)
|
||||||
|
@ -35,10 +35,10 @@ class FixedTimeBuckets(
|
|||||||
@StringRes
|
@StringRes
|
||||||
fun getBucketText(time: ZonedDateTime): Int? {
|
fun getBucketText(time: ZonedDateTime): Int? {
|
||||||
return when {
|
return when {
|
||||||
time >= startOfToday -> R.string.BucketedThreadMedia_Today
|
time >= startOfToday -> R.string.BucketedThreadMedia_Today
|
||||||
time >= startOfYesterday -> R.string.BucketedThreadMedia_Yesterday
|
time >= startOfYesterday -> R.string.BucketedThreadMedia_Yesterday
|
||||||
time >= startOfThisWeek -> R.string.BucketedThreadMedia_This_week
|
time >= startOfThisWeek -> R.string.attachmentsThisWeek
|
||||||
time >= startOfThisMonth -> R.string.BucketedThreadMedia_This_month
|
time >= startOfThisMonth -> R.string.attachmentsThisMonth
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@ -68,7 +69,7 @@ fun MediaOverviewScreen(
|
|||||||
} else {
|
} else {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission,
|
R.string.cameraGrantAccessDenied,
|
||||||
Toast.LENGTH_LONG
|
Toast.LENGTH_LONG
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
@ -101,31 +102,18 @@ fun MediaOverviewScreen(
|
|||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
R.string.ConversationItem_unable_to_open_media,
|
R.string.attachmentsErrorOpen,
|
||||||
Toast.LENGTH_LONG
|
Toast.LENGTH_LONG
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is MediaOverviewEvent.ShowSaveAttachmentError -> {
|
is MediaOverviewEvent.ShowSaveAttachmentError -> {
|
||||||
val message = context.resources.getQuantityText(
|
Toast.makeText(context, R.string.attachmentsSaveError, Toast.LENGTH_LONG).show()
|
||||||
R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card,
|
|
||||||
event.errorCount
|
|
||||||
)
|
|
||||||
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is MediaOverviewEvent.ShowSaveAttachmentSuccess -> {
|
is MediaOverviewEvent.ShowSaveAttachmentSuccess -> {
|
||||||
val message = if (event.directory.isNotBlank()) {
|
Toast.makeText(context, R.string.saved, Toast.LENGTH_LONG).show()
|
||||||
context.resources.getString(
|
|
||||||
R.string.SaveAttachmentTask_saved_to,
|
|
||||||
event.directory
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
context.resources.getString(R.string.SaveAttachmentTask_saved)
|
|
||||||
}
|
|
||||||
|
|
||||||
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -241,15 +229,11 @@ private fun SaveAttachmentWarningDialog(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
title = context.getString(R.string.ConversationFragment_save_to_sd_card),
|
title = context.getString(R.string.warning),
|
||||||
text = context.resources.getQuantityString(
|
text = context.resources.getString(R.string.attachmentsWarning),
|
||||||
R.plurals.ConversationFragment_saving_n_media_to_storage_warning,
|
|
||||||
numSelected,
|
|
||||||
numSelected
|
|
||||||
),
|
|
||||||
buttons = listOf(
|
buttons = listOf(
|
||||||
DialogButtonModel(GetString(R.string.save), onClick = onAccepted),
|
DialogButtonModel(GetString(R.string.save), GetString(R.string.AccessibilityId_save), color = LocalColors.current.danger, onClick = onAccepted),
|
||||||
DialogButtonModel(GetString(android.R.string.cancel), dismissOnClick = true)
|
DialogButtonModel(GetString(android.R.string.cancel), GetString(R.string.AccessibilityId_cancel), dismissOnClick = true)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -305,7 +289,6 @@ private fun ActionProgressDialog(
|
|||||||
|
|
||||||
private val MediaOverviewTab.titleResId: Int
|
private val MediaOverviewTab.titleResId: Int
|
||||||
get() = when (this) {
|
get() = when (this) {
|
||||||
MediaOverviewTab.Media -> R.string.MediaOverviewActivity_Media
|
MediaOverviewTab.Media -> R.string.media
|
||||||
MediaOverviewTab.Documents -> R.string.MediaOverviewActivity_Documents
|
MediaOverviewTab.Documents -> R.string.document
|
||||||
}
|
}
|
||||||
|
|
@ -50,7 +50,7 @@ fun MediaOverviewTopAppBar(
|
|||||||
IconButton(onClick = onSelectAllClicked) {
|
IconButton(onClick = onSelectAllClicked) {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(R.drawable.ic_baseline_select_all_24),
|
painterResource(R.drawable.ic_baseline_select_all_24),
|
||||||
contentDescription = stringResource(R.string.MediaOverviewActivity_Select_all),
|
contentDescription = stringResource(R.string.selectAll),
|
||||||
tint = LocalColors.current.text,
|
tint = LocalColors.current.text,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -234,11 +234,7 @@ class MediaOverviewViewModel(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val selectedMedia = selectedMedia.toList()
|
val selectedMedia = selectedMedia.toList()
|
||||||
|
|
||||||
mutableShowingActionProgress.value = application.resources.getQuantityString(
|
mutableShowingActionProgress.value = application.resources.getString(R.string.saving)
|
||||||
R.plurals.ConversationFragment_saving_n_attachments,
|
|
||||||
selectedMedia.size,
|
|
||||||
selectedMedia.size,
|
|
||||||
)
|
|
||||||
|
|
||||||
val attachments = selectedMedia
|
val attachments = selectedMedia
|
||||||
.asSequence()
|
.asSequence()
|
||||||
@ -308,7 +304,7 @@ class MediaOverviewViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
mutableShowingActionProgress.value = application.getString(R.string.MediaOverviewActivity_Media_delete_progress_message)
|
mutableShowingActionProgress.value = application.getString(R.string.deleting)
|
||||||
|
|
||||||
// Delete the selected media items, and retrieve the thread ID for the address if any
|
// Delete the selected media items, and retrieve the thread ID for the address if any
|
||||||
val threadId = withContext(Dispatchers.Default) {
|
val threadId = withContext(Dispatchers.Default) {
|
||||||
|
@ -64,7 +64,7 @@ fun MediaPage(
|
|||||||
state.isEmpty() -> {
|
state.isEmpty() -> {
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.media_overview_activity__no_media),
|
text = stringResource(R.string.attachmentsMediaEmpty),
|
||||||
style = LocalType.current.base,
|
style = LocalType.current.base,
|
||||||
color = LocalColors.current.text
|
color = LocalColors.current.text
|
||||||
)
|
)
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user