mirror of
https://github.com/oxen-io/session-android.git
synced 2025-03-28 17:02:14 +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 {
|
||||
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")
|
||||
ksp("com.google.dagger:hilt-compiler:$daggerHiltVersion")
|
||||
ksp("androidx.hilt:hilt-compiler:$jetpackHiltVersion")
|
||||
|
||||
implementation "androidx.appcompat:appcompat:$appcompatVersion"
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||
implementation "com.google.android.material:material:$materialVersion"
|
||||
@ -249,12 +251,15 @@ dependencies {
|
||||
implementation 'androidx.fragment:fragment-ktx:1.5.3'
|
||||
implementation "androidx.core:core-ktx:$coreVersion"
|
||||
implementation "androidx.work:work-runtime-ktx:2.7.1"
|
||||
|
||||
playImplementation ("com.google.firebase:firebase-messaging:24.0.0") {
|
||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
||||
}
|
||||
|
||||
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-ui:1.4.0'
|
||||
implementation 'org.conscrypt:conscrypt-android:2.5.2'
|
||||
@ -268,7 +273,6 @@ dependencies {
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.1.3'
|
||||
implementation "com.github.bumptech.glide:glide:$glideVersion"
|
||||
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.pnikosis:materialish-progress:1.5'
|
||||
implementation 'org.greenrobot:eventbus:3.0.0'
|
||||
|
@ -2,8 +2,6 @@ package network.loki.messenger
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Instrumentation
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
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.isRoot
|
||||
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.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
@ -25,6 +21,7 @@ import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.uiautomator.By
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import com.adevinta.android.barista.interaction.PermissionGranter
|
||||
import com.bumptech.glide.Glide
|
||||
import network.loki.messenger.util.InputBarButtonDrawableMatcher.Companion.inputButtonWithDrawable
|
||||
import org.hamcrest.Matcher
|
||||
import org.hamcrest.Matchers.allOf
|
||||
@ -36,11 +33,9 @@ import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
||||
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.input_bar.InputBar
|
||||
import org.thoughtcrime.securesms.home.HomeActivity
|
||||
import com.bumptech.glide.Glide
|
||||
|
||||
/**
|
||||
* Currently not used as part of our CI/Deployment processes !!!!
|
||||
@ -62,7 +57,6 @@ class HomeActivityTests {
|
||||
@Before
|
||||
fun setUp() {
|
||||
InstrumentationRegistry.getInstrumentation().addMonitor(activityMonitor)
|
||||
|
||||
}
|
||||
|
||||
@After
|
||||
@ -96,10 +90,10 @@ class HomeActivityTests {
|
||||
device.pressKeyCode(67)
|
||||
|
||||
// Continue with display name
|
||||
objectFromDesc(R.string.continue_2).click()
|
||||
objectFromDesc(R.string.theContinue).click()
|
||||
|
||||
// Continue with default push notification setting
|
||||
objectFromDesc(R.string.continue_2).click()
|
||||
objectFromDesc(R.string.theContinue).click()
|
||||
|
||||
// PN select
|
||||
if (hasViewedSeed) {
|
||||
@ -110,7 +104,6 @@ class HomeActivityTests {
|
||||
PermissionGranter.allowPermissionsIfNeeded(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
|
||||
|
||||
/* private fun goToMyChat() {
|
||||
onView(withId(R.id.newConversationButton)).perform(ViewActions.click())
|
||||
onView(withId(R.id.createPrivateChatButton)).perform(ViewActions.click())
|
||||
@ -131,7 +124,7 @@ class HomeActivityTests {
|
||||
@Test
|
||||
fun testLaunches_dismiss_seedView() {
|
||||
setupLoggedInState()
|
||||
objectFromDesc(R.string.continue_2).click()
|
||||
objectFromDesc(R.string.theContinue).click()
|
||||
objectFromDesc(R.string.copy).click()
|
||||
pressBack()
|
||||
onView(withId(R.id.seedReminderView)).check(matches(not(isDisplayed())))
|
||||
@ -182,6 +175,7 @@ class HomeActivityTests {
|
||||
onView(withText(dialogPromptText)).check(matches(isDisplayed()))
|
||||
}*/
|
||||
|
||||
|
||||
/**
|
||||
* 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_MICROPHONE"/>
|
||||
<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_MEDIA_IMAGES"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
|
||||
@ -79,7 +79,7 @@
|
||||
android:networkSecurityConfig="@xml/network_security_configuration"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Session.DayNight"
|
||||
tools:replace="android:allowBackup">
|
||||
tools:replace="android:allowBackup,android:label" >
|
||||
|
||||
<!-- Disable all analytics -->
|
||||
|
||||
@ -130,12 +130,12 @@
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/activity_message_requests_title"
|
||||
android:label="@string/sessionMessageRequests"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.preferences.SettingsActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:label="@string/activity_settings_title" />
|
||||
android:label="@string/sessionSettings" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.debugmenu.DebugActivity"
|
||||
android:screenOrientation="portrait"
|
||||
@ -151,11 +151,11 @@
|
||||
android:name="org.thoughtcrime.securesms.preferences.BlockedContactsActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar"
|
||||
android:label="@string/blocked_contacts_title"
|
||||
android:label="@string/conversationsBlockedContacts"
|
||||
/>
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.groups.EditClosedGroupActivity"
|
||||
android:label="@string/activity_edit_closed_group_title"
|
||||
android:label="@string/groupEdit"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.recoverypassword.RecoveryPasswordActivity"
|
||||
@ -165,7 +165,7 @@
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.preferences.PrivacySettingsActivity"
|
||||
android:label="@string/activity_privacy_settings_title"
|
||||
android:label="@string/sessionPrivacy"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.preferences.NotificationSettingsActivity"
|
||||
@ -175,7 +175,7 @@
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.preferences.HelpSettingsActivity"
|
||||
android:label="@string/activity_help_settings_title"
|
||||
android:label="@string/sessionHelp"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity android:name="org.thoughtcrime.securesms.preferences.appearance.AppearanceSettingsActivity"
|
||||
android:screenOrientation="portrait"/>
|
||||
@ -268,18 +268,10 @@
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.MediaPreviewActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:label="@string/AndroidManifest__media_preview"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.Session.DayNight.NoActionBar"
|
||||
android:launchMode="singleTask"
|
||||
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
|
||||
android:name="org.thoughtcrime.securesms.DummyActivity"
|
||||
android:allowTaskReparenting="true"
|
||||
|
@ -8,19 +8,8 @@ class DeleteMediaDialog {
|
||||
@JvmStatic
|
||||
fun show(context: Context, recordCount: Int, doDelete: Runnable) = context.showSessionDialog {
|
||||
iconAttribute(R.attr.dialog_alert_icon)
|
||||
title(
|
||||
context.resources.getQuantityString(
|
||||
R.plurals.MediaOverviewActivity_Media_delete_confirm_title,
|
||||
recordCount,
|
||||
recordCount
|
||||
)
|
||||
)
|
||||
text(
|
||||
context.resources.getQuantityString(R.plurals.MediaOverviewActivity_Media_delete_confirm_message,
|
||||
recordCount,
|
||||
recordCount
|
||||
)
|
||||
)
|
||||
title(context.resources.getQuantityString(R.plurals.deleteMessage, recordCount, recordCount))
|
||||
text(context.resources.getString(R.string.deleteMessageDescriptionEveryone))
|
||||
button(R.string.delete) { doDelete.run() }
|
||||
cancelButton()
|
||||
}
|
||||
|
@ -9,9 +9,9 @@ class DeleteMediaPreviewDialog {
|
||||
fun show(context: Context, doDelete: Runnable) {
|
||||
context.showSessionDialog {
|
||||
iconAttribute(R.attr.dialog_alert_icon)
|
||||
title(R.string.MediaPreviewActivity_media_delete_confirmation_title)
|
||||
text(R.string.MediaPreviewActivity_media_delete_confirmation_message)
|
||||
button(R.string.delete) { doDelete.run() }
|
||||
title(context.resources.getQuantityString(R.plurals.deleteMessage, 1, 1))
|
||||
text(R.string.deleteMessageDescriptionEveryone)
|
||||
dangerButton(R.string.delete) { doDelete.run() }
|
||||
cancelButton()
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,8 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@ -23,8 +25,8 @@ import android.database.Cursor;
|
||||
import android.database.CursorIndexOutOfBoundsException;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
@ -42,7 +44,6 @@ import android.view.WindowInsetsController;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
@ -54,10 +55,14 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
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.sending_receiving.MessageSender;
|
||||
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.util.AttachmentUtil;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import kotlin.Unit;
|
||||
import network.loki.messenger.R;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
|
||||
/**
|
||||
* Activity for displaying media attachments in-app
|
||||
@ -242,12 +240,12 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
CharSequence relativeTimeSpan;
|
||||
|
||||
if (mediaItem.date > 0) {
|
||||
relativeTimeSpan = DateUtils.getDisplayFormattedTimeSpanString(this, Locale.getDefault(), mediaItem.date);
|
||||
relativeTimeSpan = DateUtils.INSTANCE.getDisplayFormattedTimeSpanString(this, Locale.getDefault(), mediaItem.date);
|
||||
} 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 getSupportActionBar().setTitle("");
|
||||
|
||||
@ -258,7 +256,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
initializeMedia();
|
||||
}
|
||||
|
||||
@ -291,7 +288,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
captionContainer = findViewById(R.id.media_preview_caption_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.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setHomeButtonEnabled(true);
|
||||
@ -361,7 +358,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
private void initializeMedia() {
|
||||
if (!isContentTypeSupported(initialMediaType)) {
|
||||
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();
|
||||
}
|
||||
|
||||
@ -411,12 +408,19 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
MediaItem mediaItem = getCurrentMediaItem();
|
||||
if (mediaItem == null) return;
|
||||
|
||||
SaveAttachmentTask.showWarningDialog(this, 1, () -> {
|
||||
SaveAttachmentTask.showOneTimeWarningDialogOrSave(this, 1, () -> {
|
||||
Permissions.with(this)
|
||||
.request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.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))
|
||||
.onAnyDenied(() -> Toast.makeText(this, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
|
||||
.withPermanentDenialDialog(Phrase.from(getApplicationContext(), R.string.permissionsStorageSaveDenied)
|
||||
.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(() -> {
|
||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this);
|
||||
long saveDate = (mediaItem.date > 0) ? mediaItem.date : SnodeAPI.getNowWithOffset();
|
||||
@ -482,6 +486,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
super.onOptionsItemSelected(item);
|
||||
|
||||
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__forward: forward(); 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);
|
||||
}
|
||||
|
||||
if (item == 0) {
|
||||
viewPagerListener.onPageSelected(0);
|
||||
}
|
||||
if (item == 0) { viewPagerListener.onPageSelected(0); }
|
||||
}
|
||||
|
||||
@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 {
|
||||
|
||||
@ -575,13 +576,11 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
|
||||
@Override
|
||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
|
||||
|
||||
/* Do nothing */
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state) {
|
||||
|
||||
}
|
||||
public void onPageScrollStateChanged(int state) { /* Do nothing */ }
|
||||
}
|
||||
|
||||
private static class SingleItemPagerAdapter extends MediaItemAdapter {
|
||||
@ -646,9 +645,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pause(int position) {
|
||||
|
||||
}
|
||||
public void pause(int position) { /* Do nothing */ }
|
||||
|
||||
@Override
|
||||
public @Nullable View getPlaybackControls(int position) {
|
||||
|
@ -4,24 +4,45 @@ import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
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 kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
fun showMuteDialog(
|
||||
context: Context,
|
||||
onMuteDuration: (Long) -> Unit
|
||||
): AlertDialog = context.showSessionDialog {
|
||||
title(R.string.MuteDialog_mute_notifications)
|
||||
items(Option.values().map { it.stringRes }.map(context::getString).toTypedArray()) {
|
||||
onMuteDuration(Option.values()[it].getTime())
|
||||
title(R.string.notificationsMute)
|
||||
|
||||
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) {
|
||||
ONE_HOUR(R.string.arrays__mute_for_one_hour, duration = TimeUnit.HOURS.toMillis(1)),
|
||||
TWO_HOURS(R.string.arrays__mute_for_two_hours, duration = TimeUnit.HOURS.toMillis(2)),
|
||||
ONE_DAY(R.string.arrays__mute_for_one_day, duration = TimeUnit.DAYS.toMillis(1)),
|
||||
SEVEN_DAYS(R.string.arrays__mute_for_seven_days, duration = TimeUnit.DAYS.toMillis(7)),
|
||||
FOREVER(R.string.arrays__mute_forever, getTime = { Long.MAX_VALUE });
|
||||
ONE_HOUR(R.string.notificationsMuteFor, duration = TimeUnit.HOURS.toMillis(1)),
|
||||
TWO_HOURS(R.string.notificationsMuteFor, duration = TimeUnit.HOURS.toMillis(2)),
|
||||
ONE_DAY(R.string.notificationsMuteFor, duration = TimeUnit.DAYS.toMillis(1)),
|
||||
SEVEN_DAYS(R.string.notificationsMuteFor, duration = TimeUnit.DAYS.toMillis(7)),
|
||||
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;
|
||||
|
||||
import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.app.KeyguardManager;
|
||||
import android.content.ComponentName;
|
||||
@ -25,20 +27,18 @@ import android.content.ServiceConnection;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.Bundle;
|
||||
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.animation.Animation;
|
||||
import android.view.animation.BounceInterpolator;
|
||||
import android.view.animation.TranslateAnimation;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import android.widget.TextView;
|
||||
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
|
||||
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.libsignal.utilities.Log;
|
||||
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.util.AnimationCompleteListener;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.Signature;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
//TODO Rename to ScreenLockActivity and refactor to Kotlin.
|
||||
public class PassphrasePromptActivity extends BaseActionBarActivity {
|
||||
|
||||
@ -158,6 +153,16 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
|
||||
}
|
||||
|
||||
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);
|
||||
fingerprintPrompt = findViewById(R.id.fingerprint_auth_container);
|
||||
lockScreenButton = findViewById(R.id.lock_screen_auth_container);
|
||||
@ -165,10 +170,6 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
|
||||
fingerprintCancellationSignal = new CancellationSignal();
|
||||
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.getBackground().setColorFilter(getResources().getColor(R.color.signal_primary), PorterDuff.Mode.SRC_IN);
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
package org.thoughtcrime.securesms
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
@ -7,12 +9,14 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Button
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.LinearLayout.VERTICAL
|
||||
import android.widget.Space
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.annotation.StyleRes
|
||||
@ -30,6 +34,7 @@ annotation class DialogDsl
|
||||
@DialogDsl
|
||||
class SessionDialogBuilder(val context: Context) {
|
||||
|
||||
private val dp8 = toPx(8, context.resources)
|
||||
private val dp20 = toPx(20, context.resources)
|
||||
private val dp40 = toPx(40, 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 var dialog: AlertDialog? = null
|
||||
private fun dismiss() = dialog?.dismiss()
|
||||
fun dismiss() = dialog?.dismiss()
|
||||
|
||||
private val topView = LinearLayout(context)
|
||||
.apply { setPadding(0, dp20, 0, 0) }
|
||||
.apply { orientation = VERTICAL }
|
||||
.also(dialogBuilder::setCustomTitle)
|
||||
|
||||
private val contentView = LinearLayout(context).apply { orientation = VERTICAL }
|
||||
|
||||
private val buttonLayout = LinearLayout(context)
|
||||
|
||||
private val root = LinearLayout(context).apply { orientation = VERTICAL }
|
||||
@ -53,24 +60,29 @@ class SessionDialogBuilder(val context: Context) {
|
||||
addView(buttonLayout)
|
||||
}
|
||||
|
||||
fun title(@StringRes id: Int) = title(context.getString(id))
|
||||
|
||||
fun title(text: CharSequence?) = title(text?.toString())
|
||||
// Main title entry point
|
||||
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)
|
||||
fun text(text: CharSequence?, @StyleRes style: Int = 0) {
|
||||
// Convenience assessor for title that takes a string resource
|
||||
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) {
|
||||
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
||||
.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
|
||||
TextView(context, null, 0, style)
|
||||
TextView(context, null, 0, style ?: R.style.TextAppearance_Session_Dialog_Message)
|
||||
.apply {
|
||||
setText(text)
|
||||
textAlignment = View.TEXT_ALIGNMENT_CENTER
|
||||
@ -78,7 +90,7 @@ class SessionDialogBuilder(val context: Context) {
|
||||
}.let(topView::addView)
|
||||
|
||||
Space(context).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(0, dp20)
|
||||
layoutParams = LinearLayout.LayoutParams(0, dp8)
|
||||
}.let(topView::addView)
|
||||
}
|
||||
|
||||
@ -95,17 +107,31 @@ class SessionDialogBuilder(val context: Context) {
|
||||
fun singleChoiceItems(
|
||||
options: Collection<String>,
|
||||
currentSelected: Int = 0,
|
||||
dismissOnRadioSelect: Boolean = true,
|
||||
onSelect: (Int) -> Unit
|
||||
) = singleChoiceItems(options.toTypedArray(), currentSelected, onSelect)
|
||||
) = singleChoiceItems(
|
||||
options.toTypedArray(),
|
||||
currentSelected,
|
||||
dismissOnRadioSelect,
|
||||
onSelect
|
||||
)
|
||||
|
||||
fun singleChoiceItems(
|
||||
options: Array<String>,
|
||||
currentSelected: Int = 0,
|
||||
dismissOnRadioSelect: Boolean = true,
|
||||
onSelect: (Int) -> Unit
|
||||
): AlertDialog.Builder = dialogBuilder.setSingleChoiceItems(
|
||||
options,
|
||||
currentSelected
|
||||
) { dialog, it -> onSelect(it); dialog.dismiss() }
|
||||
): AlertDialog.Builder{
|
||||
val adapter = ArrayAdapter<CharSequence>(context, R.layout.view_dialog_single_choice_item, options)
|
||||
|
||||
return dialogBuilder.setSingleChoiceItems(
|
||||
adapter,
|
||||
currentSelected
|
||||
) { dialog, it ->
|
||||
onSelect(it)
|
||||
if(dismissOnRadioSelect) dialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
fun items(
|
||||
options: Array<String>,
|
||||
@ -125,16 +151,21 @@ class SessionDialogBuilder(val context: Context) {
|
||||
) { 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(
|
||||
@StringRes text: Int,
|
||||
@StringRes contentDescriptionRes: Int = text,
|
||||
@StyleRes style: Int = R.style.Widget_Session_Button_Dialog_UnimportantText,
|
||||
@ColorRes textColor: Int? = null,
|
||||
dismiss: Boolean = true,
|
||||
listener: (() -> Unit) = {}
|
||||
) = Button(context, null, 0, style).apply {
|
||||
setText(text)
|
||||
textColor?.let{
|
||||
setTextColor(it)
|
||||
}
|
||||
contentDescription = resources.getString(contentDescriptionRes)
|
||||
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, dp60, 1f)
|
||||
setOnClickListener {
|
||||
@ -149,22 +180,18 @@ class SessionDialogBuilder(val context: Context) {
|
||||
|
||||
fun Context.showSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
|
||||
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 =
|
||||
showOpenUrlDialog {
|
||||
okButton { openUrl(url) }
|
||||
cancelButton()
|
||||
}
|
||||
public fun Context.copyURLToClipboard(url: String) {
|
||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText(url, url)
|
||||
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 Fragment.showSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
|
||||
SessionDialogBuilder(requireContext()).apply { build() }.show()
|
||||
|
||||
fun Fragment.createSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
|
||||
SessionDialogBuilder(requireContext()).apply { build() }.create()
|
||||
|
@ -29,12 +29,14 @@ import android.provider.OpenableColumns;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
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.DistributionTypes;
|
||||
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.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
|
||||
*
|
||||
@ -69,7 +65,6 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
|
||||
public static final String EXTRA_ADDRESS_MARSHALLED = "address_marshalled";
|
||||
public static final String EXTRA_DISTRIBUTION_TYPE = "distribution_type";
|
||||
|
||||
|
||||
private ContactSelectionListFragment contactsFragment;
|
||||
private SearchToolbar searchToolbar;
|
||||
private ImageView searchAction;
|
||||
@ -132,7 +127,7 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
private void initializeToolbar() {
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
Toolbar toolbar = findViewById(R.id.search_toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
|
@ -37,7 +37,7 @@ public class ShortcutLauncherActivity extends AppCompatActivity {
|
||||
String serializedAddress = getIntent().getStringExtra(KEY_SERIALIZED_ADDRESS);
|
||||
|
||||
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));
|
||||
finish();
|
||||
return;
|
||||
|
@ -8,10 +8,9 @@ import android.hardware.SensorManager;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.os.PowerManager;
|
||||
import android.os.PowerManager.WakeLock;
|
||||
import android.os.PowerManager;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.OptIn;
|
||||
@ -23,7 +22,8 @@ import androidx.media3.common.PlaybackParameters;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.session.libsession.utilities.ServiceUtil;
|
||||
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.mms.AudioSlide;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
public class AudioSlidePlayer implements SensorEventListener {
|
||||
|
||||
private static final String TAG = AudioSlidePlayer.class.getSimpleName();
|
||||
@ -170,7 +167,6 @@ public class AudioSlidePlayer implements SensorEventListener {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onPlayerError(PlaybackException error) {
|
||||
Log.w(TAG, "MediaPlayer Error: " + error);
|
||||
@ -209,9 +205,7 @@ public class AudioSlidePlayer implements SensorEventListener {
|
||||
this.mediaPlayer.release();
|
||||
}
|
||||
|
||||
if (this.audioAttachmentServer != null) {
|
||||
this.audioAttachmentServer.stop();
|
||||
}
|
||||
if (this.audioAttachmentServer != null) { this.audioAttachmentServer.stop(); }
|
||||
|
||||
sensorManager.unregisterListener(AudioSlidePlayer.this);
|
||||
|
||||
@ -220,9 +214,7 @@ public class AudioSlidePlayer implements SensorEventListener {
|
||||
}
|
||||
|
||||
public synchronized static void stopAll() {
|
||||
if (playing.isPresent()) {
|
||||
playing.get().stop();
|
||||
}
|
||||
if (playing.isPresent()) { playing.get().stop(); }
|
||||
}
|
||||
|
||||
public synchronized boolean isReady() {
|
||||
@ -364,9 +356,8 @@ public class AudioSlidePlayer implements SensorEventListener {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
||||
public void onAccuracyChanged(Sensor sensor, int accuracy) { /* Do nothing */ }
|
||||
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onPlayerStart(@NonNull AudioSlidePlayer player);
|
||||
|
@ -32,7 +32,7 @@ class AvatarSelection(
|
||||
private val bgColor by lazy { activity.getColorFromAttr(android.R.attr.colorPrimary) }
|
||||
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 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]
|
||||
@ -120,7 +120,7 @@ class AvatarSelection(
|
||||
|
||||
val chooserIntent = Intent.createChooser(
|
||||
galleryIntent,
|
||||
context.getString(R.string.CreateProfileActivity_profile_photo)
|
||||
context.getString(R.string.image)
|
||||
)
|
||||
|
||||
if (!extraIntents.isEmpty()) {
|
||||
|
@ -13,11 +13,13 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewOutlineProvider
|
||||
import android.view.WindowManager
|
||||
import android.widget.TextView
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import com.squareup.phrase.Phrase
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
@ -27,6 +29,7 @@ import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityWebrtcBinding
|
||||
import org.apache.commons.lang3.time.DurationFormatUtils
|
||||
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.truncateIdForDisplay
|
||||
import org.session.libsignal.utilities.Log
|
||||
@ -202,6 +205,10 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
||||
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.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.document.setText(getFileType(documentSlide.getFileName()));
|
||||
this.setOnClickListener(new OpenClickedListener(documentSlide));
|
||||
|
@ -54,7 +54,7 @@ public class FromTextView extends EmojiTextView {
|
||||
|
||||
|
||||
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())) {
|
||||
SpannableString profileName = new SpannableString(" (~" + recipient.getProfileName() + ") ");
|
||||
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);
|
||||
setOrientation(VERTICAL);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
Toolbar toolbar = findViewById(R.id.search_toolbar);
|
||||
|
||||
toolbar.setNavigationIcon(
|
||||
getContext().getResources().getDrawable(R.drawable.ic_baseline_clear_24));
|
||||
toolbar.setNavigationIcon(getContext().getResources().getDrawable(R.drawable.ic_baseline_clear_24));
|
||||
toolbar.inflateMenu(R.menu.conversation_list_search);
|
||||
|
||||
this.searchItem = toolbar.getMenu().findItem(R.id.action_filter_search);
|
||||
@ -56,8 +55,8 @@ public class SearchToolbar extends LinearLayout {
|
||||
|
||||
searchView.setSubmitButtonEnabled(false);
|
||||
|
||||
if (searchText != null) searchText.setHint(R.string.SearchToolbar_search);
|
||||
else searchView.setQueryHint(getResources().getString(R.string.SearchToolbar_search));
|
||||
if (searchText != null) searchText.setHint(R.string.search);
|
||||
else searchView.setQueryHint(getResources().getString(R.string.search));
|
||||
|
||||
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
|
||||
@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.add(TextSecurePreferences.getLocalNumber(context));
|
||||
|
@ -36,7 +36,7 @@ class ContactSelectionListLoader(context: Context, val mode: Int, val filter: St
|
||||
list.addAll(getClosedGroups(contacts))
|
||||
}
|
||||
if (isFlagSet(DisplayMode.FLAG_OPEN_GROUPS)) {
|
||||
list.addAll(getOpenGroups(contacts))
|
||||
list.addAll(getCommunities(contacts))
|
||||
}
|
||||
if (isFlagSet(DisplayMode.FLAG_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> {
|
||||
return getItems(contacts, context.getString(R.string.fragment_contact_selection_contacts_title)) {
|
||||
return getItems(contacts, context.getString(R.string.contactContacts)) {
|
||||
!it.isGroupRecipient
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
private fun getOpenGroups(contacts: List<Recipient>): List<ContactSelectionListItem> {
|
||||
return getItems(contacts, context.getString(R.string.fragment_contact_selection_open_groups_title)) {
|
||||
private fun getCommunities(contacts: List<Recipient>): List<ContactSelectionListItem> {
|
||||
return getItems(contacts, context.getString(R.string.conversationsCommunities)) {
|
||||
it.address.isCommunity
|
||||
}
|
||||
}
|
||||
|
@ -18,10 +18,10 @@ public final class ContactUtil {
|
||||
String contactName = ContactUtil.getDisplayName(contact);
|
||||
|
||||
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) {
|
||||
|
@ -159,7 +159,7 @@ public class ContactsCursorLoader extends CursorLoader {
|
||||
|
||||
private Cursor getGroupsHeaderCursor() {
|
||||
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,
|
||||
"",
|
||||
@ -221,16 +221,6 @@ public class ContactsCursorLoader extends CursorLoader {
|
||||
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) {
|
||||
int sum = 0;
|
||||
for (Cursor cursor : list) {
|
||||
|
@ -35,7 +35,7 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
binding = ActivitySelectContactsBinding.inflate(layoutInflater)
|
||||
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()
|
||||
val emptyStateText = intent.getStringExtra(emptyStateTextKey)
|
||||
|
@ -14,7 +14,6 @@ import com.bumptech.glide.RequestManager
|
||||
|
||||
class UserView : LinearLayout {
|
||||
private lateinit var binding: ViewUserBinding
|
||||
var openGroupThreadID: Long = -1 // FIXME: This is a bit ugly
|
||||
|
||||
enum class ActionIndicator {
|
||||
None,
|
||||
@ -47,11 +46,13 @@ class UserView : LinearLayout {
|
||||
// region Updating
|
||||
fun bind(user: Recipient, glide: RequestManager, actionIndicator: ActionIndicator, isSelected: Boolean = false) {
|
||||
val isLocalUser = user.isLocalNumber
|
||||
|
||||
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)
|
||||
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
|
||||
}
|
||||
|
||||
val address = user.address.serialize()
|
||||
binding.profilePictureView.update(user)
|
||||
binding.actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24)
|
||||
@ -84,8 +85,6 @@ class UserView : LinearLayout {
|
||||
}
|
||||
}
|
||||
|
||||
fun unbind() {
|
||||
binding.profilePictureView.recycle()
|
||||
}
|
||||
fun unbind() { binding.profilePictureView.recycle() }
|
||||
// endregion
|
||||
}
|
||||
|
@ -11,21 +11,26 @@ import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.squareup.phrase.Phrase
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewConversationActionBarBinding
|
||||
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.open_groups.OpenGroup
|
||||
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.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||
import org.thoughtcrime.securesms.database.LokiAPIDatabase
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import org.thoughtcrime.securesms.ui.getSubbedString
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ConversationActionBarView @JvmOverloads constructor(
|
||||
@ -82,7 +87,7 @@ class ConversationActionBarView @JvmOverloads constructor(
|
||||
|
||||
fun update(recipient: Recipient, openGroup: OpenGroup? = null, config: ExpirationConfiguration? = null) {
|
||||
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)
|
||||
|
||||
binding.conversationTitleContainer.modifyLayoutParams<MarginLayoutParams> {
|
||||
@ -92,37 +97,56 @@ class ConversationActionBarView @JvmOverloads constructor(
|
||||
|
||||
fun updateSubtitle(recipient: Recipient, openGroup: OpenGroup? = null, config: ExpirationConfiguration? = null) {
|
||||
val settings = mutableListOf<ConversationSetting>()
|
||||
|
||||
// Specify the disappearing messages subtitle if we should
|
||||
if (config?.isEnabled == true) {
|
||||
val prefix = when (config.expiryMode) {
|
||||
is ExpiryMode.AfterRead -> R.string.expiration_type_disappear_after_read
|
||||
else -> R.string.expiration_type_disappear_after_send
|
||||
}.let(context::getString)
|
||||
// Get the type of disappearing message and the abbreviated duration..
|
||||
val dmTypeString = when (config.expiryMode) {
|
||||
is AfterRead -> context.getString(R.string.read)
|
||||
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(
|
||||
"$prefix - ${ExpirationUtil.getExpirationAbbreviatedDisplayValue(context, config.expiryMode.expirySeconds)}",
|
||||
subtitleTxt,
|
||||
ConversationSettingType.EXPIRATION,
|
||||
R.drawable.ic_timer,
|
||||
resources.getString(R.string.AccessibilityId_disappearing_messages_type_and_time)
|
||||
resources.getString(R.string.AccessibilityId_disappearingMessagesDisappear)
|
||||
)
|
||||
}
|
||||
|
||||
if (recipient.isMuted) {
|
||||
settings += ConversationSetting(
|
||||
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())) }
|
||||
?: context.getString(R.string.ConversationActivity_muted_forever),
|
||||
?.let {
|
||||
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,
|
||||
R.drawable.ic_outline_notifications_off_24
|
||||
)
|
||||
}
|
||||
|
||||
if (recipient.isGroupRecipient) {
|
||||
val title = if (recipient.isCommunityRecipient) {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
settingsAdapter.submitList(settings)
|
||||
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.ExpirationUtil
|
||||
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.getExpirationTypeDisplayValue
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.showSessionDialog
|
||||
import org.thoughtcrime.securesms.ui.getSubbedCharSequence
|
||||
import org.thoughtcrime.securesms.ui.getSubbedString
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
@ -43,22 +47,18 @@ class DisappearingMessages @Inject constructor(
|
||||
}
|
||||
|
||||
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) {
|
||||
context.getString(R.string.dialog_disappearing_messages_follow_setting_off_body)
|
||||
context.getText(R.string.disappearingMessagesFollowSettingOff)
|
||||
} else {
|
||||
context.getString(
|
||||
R.string.dialog_disappearing_messages_follow_setting_on_body,
|
||||
ExpirationUtil.getExpirationDisplayValue(
|
||||
context,
|
||||
message.expiresIn.milliseconds
|
||||
),
|
||||
context.getExpirationTypeDisplayValue(message.isNotDisappearAfterRead)
|
||||
)
|
||||
context.getSubbedCharSequence(R.string.disappearingMessagesFollowSettingOn,
|
||||
TIME_KEY to ExpirationUtil.getExpirationDisplayValue(context, message.expiresIn.milliseconds),
|
||||
DISAPPEARING_MESSAGES_TYPE_KEY to context.getExpirationTypeDisplayValue(message.isNotDisappearAfterRead))
|
||||
})
|
||||
|
||||
dangerButton(
|
||||
text = if (message.expiresIn == 0L) R.string.dialog_disappearing_messages_follow_setting_confirm else R.string.dialog_disappearing_messages_follow_setting_set,
|
||||
contentDescription = if (message.expiresIn == 0L) R.string.AccessibilityId_confirm else R.string.AccessibilityId_set_button
|
||||
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_setButton
|
||||
) {
|
||||
set(message.threadId, message.recipient.address, message.expiryMode, message.recipient.isClosedGroupRecipient)
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ class DisappearingMessagesActivity: PassphraseRequiredActionBarActivity() {
|
||||
viewModel.event.collect {
|
||||
when (it) {
|
||||
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() {
|
||||
setSupportActionBar(binding.toolbar)
|
||||
setSupportActionBar(binding.searchToolbar)
|
||||
supportActionBar?.apply {
|
||||
title = getString(R.string.activity_disappearing_messages_title)
|
||||
title = getString(R.string.disappearingMessages)
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
setHomeButtonEnabled(true)
|
||||
}
|
||||
|
@ -24,8 +24,8 @@ data class State(
|
||||
val showDebugOptions: Boolean = false
|
||||
) {
|
||||
val subtitle get() = when {
|
||||
isGroup || isNoteToSelf -> GetString(R.string.activity_disappearing_messages_subtitle_sent)
|
||||
else -> GetString(R.string.activity_disappearing_messages_subtitle)
|
||||
isGroup || isNoteToSelf -> GetString(R.string.disappearingMessagesDisappearAfterSendDescription)
|
||||
else -> GetString(R.string.disappearingMessagesDescription1)
|
||||
}
|
||||
|
||||
val typeOptionsHidden get() = isNoteToSelf || (isGroup && isNewConfigEnabled)
|
||||
@ -51,25 +51,25 @@ enum class ExpiryType(
|
||||
) {
|
||||
NONE(
|
||||
{ ExpiryMode.NONE },
|
||||
R.string.expiration_off,
|
||||
contentDescription = R.string.AccessibilityId_disable_disappearing_messages,
|
||||
R.string.off,
|
||||
contentDescription = R.string.AccessibilityId_disappearingMessagesOff,
|
||||
),
|
||||
LEGACY(
|
||||
ExpiryMode::Legacy,
|
||||
R.string.expiration_type_disappear_legacy,
|
||||
contentDescription = R.string.expiration_type_disappear_legacy_description
|
||||
contentDescription = R.string.AccessibilityId_disappearingMessagesLegacy
|
||||
),
|
||||
AFTER_READ(
|
||||
ExpiryMode::AfterRead,
|
||||
R.string.expiration_type_disappear_after_read,
|
||||
R.string.expiration_type_disappear_after_read_description,
|
||||
R.string.AccessibilityId_disappear_after_read_option
|
||||
R.string.disappearingMessagesDisappearAfterRead,
|
||||
R.string.disappearingMessagesDisappearAfterReadDescription,
|
||||
R.string.AccessibilityId_disappearingMessagesDisappearAfterRead
|
||||
),
|
||||
AFTER_SEND(
|
||||
ExpiryMode::AfterSend,
|
||||
R.string.expiration_type_disappear_after_send,
|
||||
R.string.expiration_type_disappear_after_send_description,
|
||||
R.string.AccessibilityId_disappear_after_send_option
|
||||
R.string.disappearingMessagesDisappearAfterSend,
|
||||
R.string.disappearingMessagesDisappearAfterSendDescription,
|
||||
R.string.AccessibilityId_disappearingMessagesDisappearAfterSent
|
||||
);
|
||||
|
||||
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(
|
||||
cards = listOfNotNull(
|
||||
typeOptions()?.let { ExpiryOptionsCardData(GetString(R.string.activity_disappearing_messages_delete_type), it) },
|
||||
timeOptions()?.let { ExpiryOptionsCardData(GetString(R.string.activity_disappearing_messages_timer), it) }
|
||||
typeOptions()?.let { ExpiryOptionsCardData(GetString(R.string.disappearingMessagesDeleteType), it) },
|
||||
timeOptions()?.let { ExpiryOptionsCardData(GetString(R.string.disappearingMessagesTimer), it) }
|
||||
),
|
||||
showGroupFooter = isGroup && isNewConfigEnabled,
|
||||
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 debugModes(isDebug: Boolean, type: ExpiryType) =
|
||||
debugTimes(isDebug).map { type.mode(it.inWholeSeconds) }
|
||||
|
||||
private fun State.debugOptions(): List<ExpiryRadioOption> =
|
||||
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 afterReadTimes = buildList {
|
||||
|
@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms.conversation.disappearingmessages.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||
import org.thoughtcrime.securesms.ui.Callbacks
|
||||
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
|
||||
import org.thoughtcrime.securesms.ui.NoOpCallbacks
|
||||
import org.thoughtcrime.securesms.ui.OptionsCard
|
||||
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.contentDescription
|
||||
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
|
||||
|
||||
typealias ExpiryCallbacks = Callbacks<ExpiryMode>
|
||||
typealias ExpiryCallbacks = Callbacks<ExpiryMode>
|
||||
typealias ExpiryRadioOption = RadioOption<ExpiryMode>
|
||||
|
||||
@Composable
|
||||
@ -59,7 +57,9 @@ fun DisappearingMessages(
|
||||
}
|
||||
|
||||
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,
|
||||
fontWeight = FontWeight(400),
|
||||
color = LocalColors.current.textSecondary,
|
||||
@ -72,9 +72,9 @@ fun DisappearingMessages(
|
||||
}
|
||||
|
||||
if (state.showSetButton) SlimOutlineButton(
|
||||
stringResource(R.string.disappearing_messages_set_button_title),
|
||||
stringResource(R.string.set),
|
||||
modifier = Modifier
|
||||
.contentDescription(R.string.AccessibilityId_set_button)
|
||||
.contentDescription(R.string.AccessibilityId_setButton)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(bottom = LocalDimensions.current.spacing),
|
||||
onClick = callbacks::onSetClick
|
||||
|
@ -15,6 +15,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
@ -41,12 +42,14 @@ internal fun StartConversationScreen(
|
||||
accountId: String,
|
||||
delegate: StartConversationDelegate
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
Column(modifier = Modifier.background(
|
||||
LocalColors.current.backgroundSecondary,
|
||||
shape = MaterialTheme.shapes.small
|
||||
)) {
|
||||
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
|
||||
actions = { AppBarCloseIcon(onClose = delegate::onDialogClosePressed) }
|
||||
)
|
||||
@ -57,30 +60,31 @@ internal fun StartConversationScreen(
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
val newMessageTitleTxt:String = context.resources.getQuantityString(R.plurals.messageNew, 1, 1)
|
||||
ItemButton(
|
||||
textId = R.string.messageNew,
|
||||
text = newMessageTitleTxt,
|
||||
icon = R.drawable.ic_message,
|
||||
modifier = Modifier.contentDescription(R.string.AccessibilityId_new_direct_message),
|
||||
modifier = Modifier.contentDescription(R.string.AccessibilityId_messageNew),
|
||||
onClick = delegate::onNewMessageSelected)
|
||||
Divider(startIndent = LocalDimensions.current.dividerIndent)
|
||||
ItemButton(
|
||||
textId = R.string.activity_create_group_title,
|
||||
textId = R.string.groupCreate,
|
||||
icon = R.drawable.ic_group,
|
||||
modifier = Modifier.contentDescription(R.string.AccessibilityId_create_group),
|
||||
modifier = Modifier.contentDescription(R.string.AccessibilityId_groupCreate),
|
||||
onClick = delegate::onCreateGroupSelected
|
||||
)
|
||||
Divider(startIndent = LocalDimensions.current.dividerIndent)
|
||||
ItemButton(
|
||||
textId = R.string.dialog_join_community_title,
|
||||
textId = R.string.communityJoin,
|
||||
icon = R.drawable.ic_globe,
|
||||
modifier = Modifier.contentDescription(R.string.AccessibilityId_join_community),
|
||||
modifier = Modifier.contentDescription(R.string.AccessibilityId_communityJoin),
|
||||
onClick = delegate::onJoinCommunitySelected
|
||||
)
|
||||
Divider(startIndent = LocalDimensions.current.dividerIndent)
|
||||
ItemButton(
|
||||
textId = R.string.activity_settings_invite_button_title,
|
||||
textId = R.string.sessionInviteAFriend,
|
||||
icon = R.drawable.ic_invite_friend,
|
||||
Modifier.contentDescription(R.string.AccessibilityId_invite_friend_button),
|
||||
Modifier.contentDescription(R.string.AccessibilityId_sessionInviteAFriendButton),
|
||||
onClick = delegate::onInviteFriend
|
||||
)
|
||||
Column(
|
||||
@ -99,7 +103,7 @@ internal fun StartConversationScreen(
|
||||
Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing))
|
||||
QrImage(
|
||||
string = accountId,
|
||||
Modifier.contentDescription(R.string.AccessibilityId_qr_code),
|
||||
Modifier.contentDescription(R.string.AccessibilityId_qrCode),
|
||||
icon = R.drawable.session
|
||||
)
|
||||
}
|
||||
|
@ -14,10 +14,13 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.squareup.phrase.Phrase
|
||||
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.BackAppBar
|
||||
import org.thoughtcrime.securesms.ui.components.SlimOutlineButton
|
||||
@ -43,7 +46,7 @@ internal fun InviteFriend(
|
||||
shape = MaterialTheme.shapes.small
|
||||
)) {
|
||||
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
|
||||
onBack = onBack,
|
||||
actions = { AppBarCloseIcon(onClose = onClose) }
|
||||
@ -55,7 +58,7 @@ internal fun InviteFriend(
|
||||
Text(
|
||||
accountId,
|
||||
modifier = Modifier
|
||||
.contentDescription(R.string.AccessibilityId_account_id)
|
||||
.contentDescription(R.string.AccessibilityId_shareAccountId)
|
||||
.fillMaxWidth()
|
||||
.border()
|
||||
.padding(LocalDimensions.current.spacing),
|
||||
@ -66,7 +69,10 @@ internal fun InviteFriend(
|
||||
Spacer(modifier = Modifier.height(LocalDimensions.current.xsSpacing))
|
||||
|
||||
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,
|
||||
style = LocalType.current.small,
|
||||
color = LocalColors.current.textSecondary,
|
||||
|
@ -32,6 +32,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
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 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)
|
||||
@Composable
|
||||
@ -79,8 +80,12 @@ internal fun NewMessage(
|
||||
LocalColors.current.backgroundSecondary,
|
||||
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(
|
||||
title = stringResource(R.string.messageNew),
|
||||
title = newMessageTitleTxt,
|
||||
backgroundColor = Color.Transparent, // transparent to show the rounded shape of the container
|
||||
onBack = onBack,
|
||||
actions = { AppBarCloseIcon(onClose = onClose) }
|
||||
@ -88,7 +93,7 @@ internal fun NewMessage(
|
||||
SessionTabRow(pagerState, TITLES)
|
||||
HorizontalPager(pagerState) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -116,7 +121,7 @@ private fun EnterAccountId(
|
||||
.verticalScroll(rememberScrollState())
|
||||
|
||||
// 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
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
val keyboardHeight by keyboardHeight()
|
||||
@ -149,9 +154,9 @@ private fun EnterAccountId(
|
||||
Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsSpacing))
|
||||
|
||||
BorderlessButtonWithIcon(
|
||||
text = stringResource(R.string.messageNewDescription),
|
||||
text = stringResource(R.string.messageNewDescriptionMobile),
|
||||
modifier = Modifier
|
||||
.contentDescription(R.string.AccessibilityId_help_desk_link)
|
||||
.contentDescription(R.string.AccessibilityId_messageNewDescriptionMobile)
|
||||
.padding(horizontal = LocalDimensions.current.mediumSpacing)
|
||||
.fillMaxWidth(),
|
||||
style = LocalType.current.small,
|
||||
|
@ -4,6 +4,8 @@ import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import java.util.concurrent.TimeoutException
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
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.timeout
|
||||
import org.thoughtcrime.securesms.ui.GetString
|
||||
import java.util.concurrent.TimeoutException
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class NewMessageViewModel @Inject constructor(
|
||||
@ -41,7 +41,6 @@ internal class NewMessageViewModel @Inject constructor(
|
||||
override fun onChange(value: String) {
|
||||
loadOnsJob?.cancel()
|
||||
loadOnsJob = null
|
||||
|
||||
_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)) {
|
||||
onPublicKey(value)
|
||||
} 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) {
|
||||
is SnodeAPI.Error.Generic -> application.getString(R.string.onsErrorNotRecognized)
|
||||
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()
|
||||
}
|
||||
|
||||
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.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Resources
|
||||
import android.database.Cursor
|
||||
import android.graphics.Rect
|
||||
@ -18,10 +19,7 @@ import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.MediaStore
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.SpannedString
|
||||
import android.text.TextUtils
|
||||
import android.text.style.StyleSpan
|
||||
import android.util.Pair
|
||||
import android.util.TypedValue
|
||||
import android.view.ActionMode
|
||||
@ -35,8 +33,12 @@ import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.text.set
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.compose.runtime.getValue
|
||||
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.isVisible
|
||||
import androidx.fragment.app.DialogFragment
|
||||
@ -51,6 +53,8 @@ import androidx.loader.content.Loader
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.annimon.stream.Stream
|
||||
import com.bumptech.glide.Glide
|
||||
import com.squareup.phrase.Phrase
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
@ -64,7 +68,6 @@ import network.loki.messenger.databinding.ActivityConversationV2Binding
|
||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
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.applyExpiryMode
|
||||
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.GroupUtil
|
||||
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.TextSecurePreferences
|
||||
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.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.SessionDialogBuilder
|
||||
import org.thoughtcrime.securesms.attachments.ScreenshotObserver
|
||||
import org.thoughtcrime.securesms.audio.AudioRecorder
|
||||
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.mms.AudioSlide
|
||||
import org.thoughtcrime.securesms.mms.GifSlide
|
||||
import com.bumptech.glide.Glide
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints
|
||||
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.any.ReactWithAnyEmojiDialogFragment
|
||||
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.ConfigurationMessageUtilities
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.thoughtcrime.securesms.util.NetworkUtils
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask
|
||||
import org.thoughtcrime.securesms.util.drawToBitmap
|
||||
import org.thoughtcrime.securesms.util.isScrolledToBottom
|
||||
import org.thoughtcrime.securesms.util.isScrolledToWithin30dpOfBottom
|
||||
import org.thoughtcrime.securesms.util.push
|
||||
import org.thoughtcrime.securesms.util.show
|
||||
import org.thoughtcrime.securesms.util.toPx
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.LinkedList
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
@ -188,6 +197,8 @@ import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.sqrt
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
|
||||
private const val TAG = "ConversationActivityV2"
|
||||
|
||||
@ -231,6 +242,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
.get(LinkPreviewViewModel::class.java)
|
||||
}
|
||||
|
||||
private var openLinkDialogUrl: String? by mutableStateOf(null)
|
||||
|
||||
private val threadId: Long by lazy {
|
||||
var threadId = intent.getLongExtra(THREAD_ID, -1L)
|
||||
if (threadId == -1L) {
|
||||
@ -279,8 +292,17 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
var searchViewItem: MenuItem? = null
|
||||
|
||||
private val bufferedLastSeenChannel = Channel<Long>(capacity = 512, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
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
|
||||
get() = binding.conversationRecyclerView.isScrolledToBottom
|
||||
|
||||
@ -385,12 +407,33 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
// endregion
|
||||
|
||||
fun showOpenUrlDialog(url: String){
|
||||
openLinkDialogUrl = url
|
||||
}
|
||||
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
binding = ActivityConversationV2Binding.inflate(layoutInflater)
|
||||
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
|
||||
messageToScrollTimestamp.set(intent.getLongExtra(SCROLL_MESSAGE_ID, -1))
|
||||
messageToScrollAuthor.set(intent.getParcelableExtra(SCROLL_MESSAGE_AUTHOR))
|
||||
@ -704,7 +747,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
|
||||
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
|
||||
@ -755,9 +798,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
// called from onCreate
|
||||
private fun setUpBlockedBanner() {
|
||||
val recipient = viewModel.recipient?.takeUnless { it.isGroupRecipient } ?: return
|
||||
val accountID = recipient.address.toString()
|
||||
val name = sessionContactDb.getContactWithAccountID(accountID)?.displayName(Contact.ContactContext.REGULAR) ?: accountID
|
||||
binding.blockedBannerTextView.text = resources.getString(R.string.activity_conversation_blocked_banner_text, name)
|
||||
binding.blockedBannerTextView.text = applicationContext.getString(R.string.blockBlockedDescription)
|
||||
binding.blockedBanner.isVisible = recipient.isBlocked
|
||||
binding.blockedBanner.setOnClickListener { viewModel.unblock() }
|
||||
}
|
||||
@ -770,8 +811,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
|
||||
binding.outdatedBanner.isVisible = 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()
|
||||
}
|
||||
|
||||
// Update placeholder / control messages in a conversation
|
||||
private fun updatePlaceholder() {
|
||||
val recipient = viewModel.recipient ?: return Log.w("Loki", "recipient was null in placeholder update")
|
||||
val blindedRecipient = viewModel.blindedRecipient
|
||||
val openGroup = viewModel.openGroup
|
||||
|
||||
val (textResource, insertParam) = when {
|
||||
recipient.isLocalNumber -> R.string.activity_conversation_empty_state_note_to_self to null
|
||||
openGroup != null && !openGroup.canWrite -> R.string.activity_conversation_empty_state_read_only to recipient.toShortString()
|
||||
blindedRecipient?.blocksCommunityMessageRequests == true -> R.string.activity_conversation_empty_state_blocks_community_requests to recipient.toShortString()
|
||||
else -> R.string.activity_conversation_empty_state_default to recipient.toShortString()
|
||||
// Get the correct placeholder text for this type of empty conversation
|
||||
val isNoteToSelf = recipient.isLocalNumber
|
||||
val txtCS: CharSequence = when {
|
||||
recipient.isLocalNumber -> getString(R.string.noteToSelfEmpty)
|
||||
|
||||
// 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
|
||||
binding.placeholderText.isVisible = showPlaceholder
|
||||
if (showPlaceholder) {
|
||||
if (insertParam != null) {
|
||||
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)
|
||||
}
|
||||
binding.placeholderText.text = txtCS
|
||||
}
|
||||
}
|
||||
|
||||
@ -1117,11 +1175,21 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
|
||||
override fun block(deleteThread: Boolean) {
|
||||
val recipient = viewModel.recipient ?: return Log.w("Loki", "Recipient was null for block action")
|
||||
showSessionDialog {
|
||||
title(R.string.RecipientPreferenceActivity_block_this_contact_question)
|
||||
text(R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact)
|
||||
dangerButton(R.string.RecipientPreferenceActivity_block, R.string.AccessibilityId_block_confirm) {
|
||||
title(R.string.block)
|
||||
text(
|
||||
Phrase.from(context, R.string.blockDescription)
|
||||
.put(NAME_KEY, recipient.name)
|
||||
.format()
|
||||
)
|
||||
dangerButton(R.string.block, R.string.AccessibilityId_blockConfirm) {
|
||||
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) {
|
||||
viewModel.deleteThread()
|
||||
finish()
|
||||
@ -1135,7 +1203,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
val clip = ClipData.newPlainText("Account ID", accountId)
|
||||
val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
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) {
|
||||
@ -1147,7 +1215,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
val clip = ClipData.newPlainText("Community URL", openGroup.joinURL)
|
||||
val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
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) {
|
||||
@ -1160,13 +1228,20 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
|
||||
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 {
|
||||
title(R.string.ConversationActivity_unblock_this_contact_question)
|
||||
text(R.string.ConversationActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact)
|
||||
dangerButton(
|
||||
R.string.ConversationActivity_unblock,
|
||||
R.string.AccessibilityId_block_confirm
|
||||
) { viewModel.unblock() }
|
||||
title(R.string.blockUnblock)
|
||||
text(
|
||||
Phrase.from(context, R.string.blockUnblockName)
|
||||
.put(NAME_KEY, recipient.name)
|
||||
.format()
|
||||
)
|
||||
dangerButton(R.string.blockUnblock, R.string.AccessibilityId_unblockConfirm) { viewModel.unblock() }
|
||||
cancelButton()
|
||||
}
|
||||
}
|
||||
@ -1177,10 +1252,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
if (actionMode != null) {
|
||||
onDeselect(message, position, actionMode)
|
||||
} else {
|
||||
// NOTE:
|
||||
// We have to use onContentClick (rather than a click listener directly on
|
||||
// NOTE: 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
|
||||
// onClickListeners directly to message content views.
|
||||
// onClickListeners directly to message content views!
|
||||
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) {
|
||||
// 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
|
||||
val recipient = viewModel.recipient ?: return Log.w(TAG, "Could not locate recipient when sending emoji reaction")
|
||||
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) {
|
||||
val recipient = viewModel.recipient ?: return
|
||||
val message = VisibleMessage()
|
||||
@ -1591,9 +1705,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
val isNoteToSelf = (recipient.isContactRecipient && recipient.address.toString() == userPublicKey)
|
||||
if (seed in text && !isNoteToSelf && !hasPermissionToSendSeed) {
|
||||
showSessionDialog {
|
||||
title(R.string.dialog_send_seed_title)
|
||||
text(R.string.dialog_send_seed_explanation)
|
||||
button(R.string.dialog_send_seed_send_button_title) { sendTextOnlyMessage(true) }
|
||||
title(R.string.warning)
|
||||
text(R.string.recoveryPasswordWarningSendDescription)
|
||||
button(R.string.send) { sendTextOnlyMessage(true) }
|
||||
cancelButton()
|
||||
}
|
||||
|
||||
@ -1673,9 +1787,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
val hasSeenGIFMetaDataWarning: Boolean = textSecurePreferences.hasSeenGIFMetaDataWarning()
|
||||
if (!hasSeenGIFMetaDataWarning) {
|
||||
showSessionDialog {
|
||||
title(R.string.giphy_permission_title)
|
||||
text(R.string.giphy_permission_message)
|
||||
button(R.string.continue_2) {
|
||||
title(R.string.giphyWarning)
|
||||
text(Phrase.from(context, R.string.giphyWarningDescription).put(APP_NAME_KEY, getString(R.string.app_name)).format())
|
||||
button(R.string.theContinue) {
|
||||
textSecurePreferences.setHasSeenGIFMetaDataWarning()
|
||||
selectGif()
|
||||
}
|
||||
@ -1718,11 +1832,27 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
val mediaPreppedListener = object : ListenableFuture.Listener<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)
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -1805,8 +1935,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
} else {
|
||||
Permissions.with(this)
|
||||
.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)
|
||||
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_requires_the_microphone_permission_in_order_to_send_audio_messages))
|
||||
.withRationaleDialog(getString(R.string.permissionsMicrophoneAccessRequired), R.drawable.ic_baseline_mic_48)
|
||||
.withPermanentDenialDialog(Phrase.from(applicationContext, R.string.permissionsMicrophoneAccessRequired)
|
||||
.put(APP_NAME_KEY, getString(R.string.app_name))
|
||||
.format().toString())
|
||||
.execute()
|
||||
}
|
||||
}
|
||||
@ -1876,7 +2008,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
|
||||
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>) {
|
||||
val messageCount = 1
|
||||
showSessionDialog {
|
||||
title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
|
||||
text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
|
||||
title(resources.getQuantityString(R.plurals.deleteMessage, messages.count(), messages.count()))
|
||||
text(resources.getString(R.string.deleteMessagesDescriptionDevice))
|
||||
button(R.string.delete) { messages.forEach(viewModel::deleteLocally); 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 (recipient.isCommunityRecipient || recipient.isLocalNumber) {
|
||||
val messageCount = 1 // Only used for plurals string
|
||||
showSessionDialog {
|
||||
title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
|
||||
text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
|
||||
button(R.string.delete) {
|
||||
messages.forEach(viewModel::deleteForEveryone); endActionMode()
|
||||
}
|
||||
title(resources.getQuantityString(R.plurals.deleteMessage, messages.count(), messages.count()))
|
||||
text(resources.getString(R.string.deleteMessageDescriptionEveryone))
|
||||
button(R.string.delete) { messages.forEach(viewModel::deleteForEveryone); endActionMode() }
|
||||
cancelButton { endActionMode() }
|
||||
}
|
||||
// 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.
|
||||
{
|
||||
val messageCount = 1
|
||||
showSessionDialog {
|
||||
title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
|
||||
text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
|
||||
button(R.string.delete) {
|
||||
messages.forEach(viewModel::deleteLocally); endActionMode()
|
||||
}
|
||||
title(resources.getQuantityString(R.plurals.deleteMessage, messages.count(), messages.count()))
|
||||
text(resources.getString(R.string.deleteMessageDescriptionDevice))
|
||||
dangerButton(R.string.delete) { messages.forEach(viewModel::deleteLocally); endActionMode() }
|
||||
cancelButton(::endActionMode)
|
||||
}
|
||||
}
|
||||
@ -1995,18 +2120,18 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
|
||||
override fun banUser(messages: Set<MessageRecord>) {
|
||||
showSessionDialog {
|
||||
title(R.string.ConversationFragment_ban_selected_user)
|
||||
text("This will ban the selected user from this room. It won't ban them from other rooms.")
|
||||
button(R.string.ban) { viewModel.banUser(messages.first().individualRecipient); endActionMode() }
|
||||
title(R.string.banUser)
|
||||
text(R.string.communityBanDescription)
|
||||
button(R.string.banUser) { viewModel.banUser(messages.first().individualRecipient); endActionMode() }
|
||||
cancelButton(::endActionMode)
|
||||
}
|
||||
}
|
||||
|
||||
override fun banAndDeleteAll(messages: Set<MessageRecord>) {
|
||||
showSessionDialog {
|
||||
title(R.string.ConversationFragment_ban_selected_user)
|
||||
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.")
|
||||
button(R.string.ban) { viewModel.banAndDeleteAll(messages.first()); endActionMode() }
|
||||
title(R.string.banUser)
|
||||
text(R.string.communityBanDeleteDescription)
|
||||
button(R.string.banUser) { viewModel.banAndDeleteAll(messages.first()); endActionMode() }
|
||||
cancelButton(::endActionMode)
|
||||
}
|
||||
}
|
||||
@ -2042,7 +2167,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
if (TextUtils.isEmpty(result)) { return }
|
||||
val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||
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()
|
||||
}
|
||||
|
||||
@ -2051,7 +2176,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
val clip = ClipData.newPlainText("Account ID", accountID)
|
||||
val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||
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()
|
||||
}
|
||||
|
||||
@ -2093,41 +2218,95 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
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>) {
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.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))
|
||||
.maxSdkVersion(Build.VERSION_CODES.P) // P is 28
|
||||
.withPermanentDenialDialog(Phrase.from(applicationContext, R.string.permissionsStorageSaveDenied)
|
||||
.put(APP_NAME_KEY, getString(R.string.app_name))
|
||||
.format().toString())
|
||||
.onAnyDenied {
|
||||
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 {
|
||||
endActionMode()
|
||||
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@onAllGranted
|
||||
}
|
||||
Toast.makeText(this,
|
||||
resources.getQuantityString(R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card, 1),
|
||||
Toast.LENGTH_LONG).show()
|
||||
saveAttachments(message)
|
||||
}
|
||||
.execute()
|
||||
}
|
||||
@ -2182,6 +2361,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
searchViewModel.onMissingResult() }
|
||||
}
|
||||
}
|
||||
|
||||
binding.searchBottomBar.setData(result.position, result.getResults().size)
|
||||
})
|
||||
}
|
||||
@ -2191,6 +2371,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
binding.searchBottomBar.visibility = View.VISIBLE
|
||||
binding.searchBottomBar.setData(0, 0)
|
||||
binding.inputBar.visibility = View.INVISIBLE
|
||||
|
||||
}
|
||||
|
||||
fun onSearchClosed() {
|
||||
|
@ -12,6 +12,8 @@ import androidx.core.util.getOrDefault
|
||||
import androidx.core.util.set
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import kotlin.math.min
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
@ -20,6 +22,7 @@ import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
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.VisibleMessageView
|
||||
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate
|
||||
@ -29,8 +32,8 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import com.bumptech.glide.RequestManager
|
||||
import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity
|
||||
import org.thoughtcrime.securesms.showSessionDialog
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import kotlin.math.min
|
||||
import org.thoughtcrime.securesms.ui.getSubbedCharSequence
|
||||
import org.thoughtcrime.securesms.ui.getSubbedString
|
||||
|
||||
class ConversationAdapter(
|
||||
context: Context,
|
||||
@ -154,9 +157,13 @@ class ConversationAdapter(
|
||||
if (message.isCallLog && message.isFirstMissedCall) {
|
||||
viewHolder.view.setOnClickListener {
|
||||
context.showSessionDialog {
|
||||
title(R.string.CallNotificationBuilder_first_call_title)
|
||||
text(R.string.CallNotificationBuilder_first_call_message)
|
||||
button(R.string.activity_settings_title) {
|
||||
val titleTxt = context.getSubbedString(R.string.callsMissedCallFrom, NAME_KEY to message.individualRecipient.name!!)
|
||||
title(titleTxt)
|
||||
|
||||
val bodyTxt = context.getSubbedCharSequence(R.string.callsYouMissedCallPermissions, NAME_KEY to message.individualRecipient.name!!)
|
||||
text(bodyTxt)
|
||||
|
||||
button(R.string.sessionSettings) {
|
||||
Intent(context, PrivacySettingsActivity::class.java)
|
||||
.let(context::startActivity)
|
||||
}
|
||||
@ -190,7 +197,7 @@ class ConversationAdapter(
|
||||
private fun getMessageBefore(position: Int, cursor: Cursor): MessageRecord? {
|
||||
// The message that's visually before the current one is actually after the current
|
||||
// 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 }
|
||||
|
||||
return messageDB.readerFor(cursor).current
|
||||
|
@ -22,7 +22,11 @@ import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.doOnLayout
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat
|
||||
import com.squareup.phrase.Phrase
|
||||
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.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@ -30,7 +34,9 @@ import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.LocalisedTimeUtil.toShortTwoPartString
|
||||
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.ThemeUtil
|
||||
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.util.AnimationCompleteListener
|
||||
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
|
||||
class ConversationReactionOverlay : FrameLayout {
|
||||
@ -529,11 +527,11 @@ class ConversationReactionOverlay : FrameLayout {
|
||||
?: return emptyList()
|
||||
val userPublicKey = getLocalNumber(context)!!
|
||||
// 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
|
||||
val canWrite = openGroup == null || openGroup.canWrite
|
||||
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
|
||||
if (!containsControlMessage && hasText) {
|
||||
@ -541,34 +539,42 @@ class ConversationReactionOverlay : FrameLayout {
|
||||
}
|
||||
// Copy Account ID
|
||||
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
|
||||
if (userCanDeleteSelectedItems(context, message, openGroup, userPublicKey, blindedPublicKey)) {
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
if (message.isMms && (message as MediaMmsMessageRecord).containsMediaSlide()) {
|
||||
items += ActionItem(R.attr.menu_save_icon, R.string.conversation_context_image__save_attachment, { handleActionItemClicked(Action.DOWNLOAD) }, R.string.AccessibilityId_save_attachment)
|
||||
// Save media..
|
||||
if (message.isMms) {
|
||||
// ..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
|
||||
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?)?
|
||||
get() = if (expiresIn <= 0) {
|
||||
null
|
||||
@ -715,6 +717,10 @@ private val MessageRecord.subtitle: ((Context) -> CharSequence?)?
|
||||
(expiresIn - (SnodeAPI.nowWithOffset - (expireStarted.takeIf { it > 0 } ?: timestamp)))
|
||||
.coerceAtLeast(0L)
|
||||
.milliseconds
|
||||
.to2partString()
|
||||
?.let { context.getString(R.string.auto_deletes_in, it) }
|
||||
.toShortTwoPartString()
|
||||
.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()) {
|
||||
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.deleteForMeTextView.setOnClickListener(this)
|
||||
|
@ -100,7 +100,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
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)
|
||||
|
||||
@ -313,7 +313,7 @@ fun ExpandButton(modifier: Modifier = Modifier, onClick: () -> Unit) {
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_expand),
|
||||
contentDescription = stringResource(id = R.string.expand),
|
||||
contentDescription = stringResource(id = R.string.AccessibilityId_expand),
|
||||
modifier = Modifier.clickable { onClick() },
|
||||
)
|
||||
}
|
||||
@ -331,7 +331,7 @@ fun PreviewMessageDetails(
|
||||
imageAttachments = listOf(
|
||||
Attachment(
|
||||
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",
|
||||
uri = Uri.parse(""),
|
||||
@ -339,7 +339,7 @@ fun PreviewMessageDetails(
|
||||
),
|
||||
Attachment(
|
||||
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",
|
||||
uri = Uri.parse(""),
|
||||
@ -347,7 +347,7 @@ fun PreviewMessageDetails(
|
||||
),
|
||||
Attachment(
|
||||
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",
|
||||
uri = Uri.parse(""),
|
||||
@ -356,14 +356,14 @@ fun PreviewMessageDetails(
|
||||
|
||||
),
|
||||
nonImageAttachmentFileDetails = listOf(
|
||||
TitledText(R.string.message_details_header__file_id, "Screen Shot 2023-07-06 at 11.35.50 am.png"),
|
||||
TitledText(R.string.message_details_header__file_type, "image/png"),
|
||||
TitledText(R.string.message_details_header__file_size, "195.6kB"),
|
||||
TitledText(R.string.message_details_header__resolution, "342x312"),
|
||||
TitledText(R.string.attachmentsFileId, "Screen Shot 2023-07-06 at 11.35.50 am.png"),
|
||||
TitledText(R.string.attachmentsFileType, "image/png"),
|
||||
TitledText(R.string.attachmentsFileSize, "195.6kB"),
|
||||
TitledText(R.string.attachmentsResolution, "342x312"),
|
||||
),
|
||||
sent = TitledText(R.string.message_details_header__sent, "6:12 AM Tue, 09/08/2022"),
|
||||
received = TitledText(R.string.message_details_header__received, "6:12 AM Tue, 09/08/2022"),
|
||||
error = TitledText(R.string.message_details_header__error, "Message failed to send"),
|
||||
sent = TitledText(R.string.sent, "6:12 AM Tue, 09/08/2022"),
|
||||
received = TitledText(R.string.received, "6:12 AM Tue, 09/08/2022"),
|
||||
error = TitledText(R.string.error, "Message failed to send"),
|
||||
senderInfo = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54"),
|
||||
)
|
||||
)
|
||||
|
@ -78,9 +78,9 @@ class MessageDetailsViewModel @Inject constructor(
|
||||
MessageDetailsState(
|
||||
attachments = slides.map(::Attachment),
|
||||
record = record,
|
||||
sent = dateSent.let(::Date).toString().let { TitledText(R.string.message_details_header__sent, it) },
|
||||
received = dateReceived.let(::Date).toString().let { TitledText(R.string.message_details_header__received, it) },
|
||||
error = lokiMessageDatabase.getErrorMessage(id)?.let { TitledText(R.string.message_details_header__error, it) },
|
||||
sent = dateSent.let(::Date).toString().let { TitledText(R.string.sent, it) },
|
||||
received = dateReceived.let(::Date).toString().let { TitledText(R.string.received, it) },
|
||||
error = lokiMessageDatabase.getErrorMessage(id)?.let { TitledText(R.string.theError, it) },
|
||||
senderInfo = individualRecipient.run { name?.let { TitledText(it, address.serialize()) } },
|
||||
sender = individualRecipient,
|
||||
thread = threadDb.getRecipientForThreadId(threadId)!!,
|
||||
@ -90,14 +90,14 @@ class MessageDetailsViewModel @Inject constructor(
|
||||
|
||||
private val Slide.details: List<TitledText>
|
||||
get() = listOfNotNull(
|
||||
fileName.orNull()?.let { TitledText(R.string.message_details_header__file_id, it) },
|
||||
TitledText(R.string.message_details_header__file_type, asAttachment().contentType),
|
||||
TitledText(R.string.message_details_header__file_size, Util.getPrettyFileSize(fileSize)),
|
||||
fileName.orNull()?.let { TitledText(R.string.attachmentsFileId, it) },
|
||||
TitledText(R.string.attachmentsFileType, asAttachment().contentType),
|
||||
TitledText(R.string.attachmentsFileSize, Util.getPrettyFileSize(fileSize)),
|
||||
takeIf { it is ImageSlide }
|
||||
?.let(Slide::asAttachment)
|
||||
?.run { "${width}x$height" }
|
||||
?.let { TitledText(R.string.message_details_header__resolution, it) },
|
||||
attachmentDb.duration(this)?.let { TitledText(R.string.message_details_header__duration, it) },
|
||||
?.let { TitledText(R.string.attachmentsResolution, it) },
|
||||
attachmentDb.duration(this)?.let { TitledText(R.string.attachmentsDuration, it) },
|
||||
)
|
||||
|
||||
private fun AttachmentDatabase.duration(slide: Slide): String? =
|
||||
@ -157,7 +157,7 @@ data class MessageDetailsState(
|
||||
val sender: 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
|
||||
}
|
||||
|
||||
|
@ -15,9 +15,12 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.squareup.phrase.Phrase
|
||||
import network.loki.messenger.R
|
||||
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 {
|
||||
private lateinit var binding: FragmentModalUrlBottomSheetBinding
|
||||
@ -29,7 +32,8 @@ class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(),
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
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 startIndex = explanation.indexOf(url)
|
||||
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))
|
||||
requireContext().startActivity(intent)
|
||||
} 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()
|
||||
}
|
||||
@ -53,7 +57,7 @@ class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(),
|
||||
val clip = ClipData.newPlainText("URL", url)
|
||||
val manager = requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||
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()
|
||||
}
|
||||
|
||||
|
@ -17,22 +17,12 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.net.Uri
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.TextUtils
|
||||
import android.text.style.StyleSpan
|
||||
import android.view.View
|
||||
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 org.session.libsignal.utilities.Log
|
||||
|
||||
object Util {
|
||||
private val TAG: String = Log.tag(Util::class.java)
|
||||
@ -92,22 +82,6 @@ object Util {
|
||||
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) {
|
||||
try {
|
||||
(lock as Object).wait(timeout)
|
||||
@ -123,8 +97,7 @@ object Util {
|
||||
return results
|
||||
}
|
||||
|
||||
val elements =
|
||||
source.split(delimiter.toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
val elements = source.split(delimiter.toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
Collections.addAll(results, *elements)
|
||||
|
||||
return results
|
||||
|
@ -11,10 +11,12 @@ import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.isVisible
|
||||
import com.squareup.phrase.Phrase
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.AlbumThumbnailViewBinding
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
||||
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.thoughtcrime.securesms.MediaPreviewActivity
|
||||
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 ->
|
||||
// overflowText will be null if !overflowed
|
||||
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
|
||||
}
|
||||
@ -110,10 +115,9 @@ class AlbumThumbnailView : RelativeLayout {
|
||||
|
||||
// endregion
|
||||
|
||||
|
||||
fun layoutRes(slideCount: Int) = when (slideCount) {
|
||||
1 -> R.layout.album_thumbnail_1 // single
|
||||
2 -> R.layout.album_thumbnail_2// two sidebyside
|
||||
1 -> R.layout.album_thumbnail_1 // single
|
||||
2 -> R.layout.album_thumbnail_2 // two side-by-side
|
||||
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 org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.createSessionDialog
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.ui.getSubbedCharSequence
|
||||
|
||||
/** Shown upon sending a message to a user that's blocked. */
|
||||
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 name = contact?.displayName(Contact.ContactContext.REGULAR) ?: accountID
|
||||
|
||||
val explanation = resources.getString(R.string.dialog_blocked_explanation, name)
|
||||
val spannable = SpannableStringBuilder(explanation)
|
||||
val startIndex = explanation.indexOf(name)
|
||||
val explanationCS = context.getSubbedCharSequence(R.string.blockUnblockName, NAME_KEY to name)
|
||||
val spannable = SpannableStringBuilder(explanationCS)
|
||||
val startIndex = explanationCS.indexOf(name)
|
||||
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)
|
||||
button(R.string.ConversationActivity_unblock) { unblock() }
|
||||
dangerButton(R.string.blockUnblock, R.string.AccessibilityId_unblockConfirm) { unblock() }
|
||||
cancelButton { dismiss() }
|
||||
}
|
||||
|
||||
|
@ -7,12 +7,14 @@ import android.text.Spannable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.StyleSpan
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.squareup.phrase.Phrase
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
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.database.SessionContactDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
@ -29,15 +31,19 @@ class DownloadDialog(private val recipient: Recipient) : DialogFragment() {
|
||||
val accountID = recipient.address.toString()
|
||||
val contact = contactDB.getContactWithAccountID(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 startIndex = explanation.indexOf(name)
|
||||
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
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() }
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.dialogs
|
||||
|
||||
import org.thoughtcrime.securesms.createSessionDialog
|
||||
import android.app.Dialog
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
@ -8,11 +9,13 @@ import android.text.SpannableStringBuilder
|
||||
import android.text.style.StyleSpan
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.squareup.phrase.Phrase
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
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.thoughtcrime.securesms.createSessionDialog
|
||||
import org.thoughtcrime.securesms.groups.OpenGroupManager
|
||||
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() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
|
||||
title(resources.getString(R.string.dialog_join_open_group_title, name))
|
||||
val explanation = resources.getString(R.string.dialog_join_open_group_explanation, name)
|
||||
title(resources.getString(R.string.communityJoin))
|
||||
val explanation = Phrase.from(context, R.string.communityJoinDescription).put(COMMUNITY_NAME_KEY, name).format()
|
||||
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)
|
||||
text(spannable)
|
||||
cancelButton { dismiss() }
|
||||
button(R.string.open_group_invitation_view__join_accessibility_description) { join() }
|
||||
button(R.string.join) { 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)
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(activity)
|
||||
} 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()
|
||||
|
@ -4,18 +4,22 @@ import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
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.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
|
||||
* let them know that Session offers the ability to send and receive link previews. */
|
||||
class LinkPreviewDialog(private val onEnabled: () -> Unit) : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
|
||||
title(R.string.dialog_link_preview_title)
|
||||
text(R.string.dialog_link_preview_explanation)
|
||||
button(R.string.dialog_link_preview_enable_button_title) { enable() }
|
||||
cancelButton { dismiss() }
|
||||
title(R.string.linkPreviewsEnable)
|
||||
val txt = context.getSubbedCharSequence(R.string.linkPreviewsFirstDescription, APP_NAME_KEY to APP_NAME)
|
||||
text(txt)
|
||||
button(R.string.enable) { enable() }
|
||||
cancelButton { dismiss() }
|
||||
}
|
||||
|
||||
private fun enable() {
|
||||
|
@ -79,9 +79,9 @@ class InputBar @JvmOverloads constructor(
|
||||
var voiceMessageDurationMS = 0L
|
||||
var voiceRecorderState = VoiceRecorderState.Idle
|
||||
|
||||
private val attachmentsButton = InputBarButton(context, R.drawable.ic_plus_24).apply { contentDescription = context.getString(R.string.AccessibilityId_attachments_button)}
|
||||
val microphoneButton = InputBarButton(context, R.drawable.ic_microphone).apply { contentDescription = context.getString(R.string.AccessibilityId_microphone_button)}
|
||||
private val sendButton = InputBarButton(context, R.drawable.ic_arrow_up, true).apply { contentDescription = context.getString(R.string.AccessibilityId_send_message_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_voiceMessageNew)}
|
||||
private val sendButton = InputBarButton(context, R.drawable.ic_arrow_up, true).apply { contentDescription = context.getString(R.string.AccessibilityId_send)}
|
||||
|
||||
init {
|
||||
// Attachments button
|
||||
|
@ -19,7 +19,6 @@ import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewInputBarRecordingBinding
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.animateSizeChange
|
||||
import org.thoughtcrime.securesms.util.disableClipping
|
||||
import org.thoughtcrime.securesms.util.toPx
|
||||
@ -106,8 +105,7 @@ class InputBarRecordingView : RelativeLayout {
|
||||
timerJob = scope.launch {
|
||||
while (isActive) {
|
||||
val duration = (Date().time - startTimestamp) / 1000L
|
||||
binding.recordingViewDurationTextView.text = DateUtils.formatElapsedTime(duration)
|
||||
|
||||
binding.recordingViewDurationTextView.text = android.text.format.DateUtils.formatElapsedTime(duration)
|
||||
delay(500)
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,8 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
|
||||
val edKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!
|
||||
val blindedPublicKey = openGroup?.publicKey?.let { SodiumUtilities.blindedKeyPair(it, edKeyPair)?.publicKey?.asBytes }
|
||||
?.let { AccountId(IdPrefix.BLINDED, it) }?.hexString
|
||||
|
||||
// Embedded function
|
||||
fun userCanDeleteSelectedItems(): Boolean {
|
||||
val allSentByCurrentUser = 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 }
|
||||
return OpenGroupManager.isUserModerator(context, openGroup.groupId, userPublicKey, blindedPublicKey)
|
||||
}
|
||||
|
||||
// Embedded function
|
||||
fun userCanBanSelectedUsers(): Boolean {
|
||||
if (openGroup == null) { return false }
|
||||
val anySentByCurrentUser = selectedItems.any { it.isOutgoing }
|
||||
@ -55,6 +59,9 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
|
||||
if (selectedUsers.size > 1) { return false }
|
||||
return OpenGroupManager.isUserModerator(context, openGroup.groupId, userPublicKey, blindedPublicKey)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Delete message
|
||||
menu.findItem(R.id.menu_context_delete_message).isVisible = userCanDeleteSelectedItems()
|
||||
// Ban user
|
||||
|
@ -16,10 +16,13 @@ import androidx.appcompat.widget.SearchView.OnQueryTextListener
|
||||
import androidx.core.content.pm.ShortcutInfoCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import com.squareup.phrase.Phrase
|
||||
import java.io.IOException
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.messaging.sending_receiving.leave
|
||||
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.recipients.Recipient
|
||||
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.showSessionDialog
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil
|
||||
import java.io.IOException
|
||||
|
||||
object ConversationMenuHelper {
|
||||
|
||||
@ -50,11 +52,11 @@ object ConversationMenuHelper {
|
||||
) {
|
||||
// Prepare
|
||||
menu.clear()
|
||||
val isOpenGroup = thread.isCommunityRecipient
|
||||
val isCommunity = thread.isCommunityRecipient
|
||||
// Base menu (options that should always be present)
|
||||
inflater.inflate(R.menu.menu_conversation, menu)
|
||||
// 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)
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
// Open group menu
|
||||
if (isOpenGroup) {
|
||||
if (isCommunity) {
|
||||
inflater.inflate(R.menu.menu_conversation_open_group, menu)
|
||||
}
|
||||
// Muting
|
||||
@ -162,9 +164,9 @@ object ConversationMenuHelper {
|
||||
|
||||
if (!TextSecurePreferences.isCallNotificationsEnabled(context)) {
|
||||
context.showSessionDialog {
|
||||
title(R.string.ConversationActivity_call_title)
|
||||
text(R.string.ConversationActivity_call_prompt)
|
||||
button(R.string.activity_settings_title, R.string.AccessibilityId_settings) {
|
||||
title(R.string.callsPermissionsRequired)
|
||||
text(R.string.callsPermissionsRequiredDescription)
|
||||
button(R.string.sessionSettings, R.string.AccessibilityId_sessionSettings) {
|
||||
Intent(context, PrivacySettingsActivity::class.java).let(context::startActivity)
|
||||
}
|
||||
cancelButton()
|
||||
@ -178,7 +180,6 @@ object ConversationMenuHelper {
|
||||
Intent(context, WebRtcCallActivity::class.java)
|
||||
.apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK }
|
||||
.let(context::startActivity)
|
||||
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@ -215,7 +216,7 @@ object ConversationMenuHelper {
|
||||
.setIntent(ShortcutLauncherActivity.createIntent(context, thread.address))
|
||||
.build()
|
||||
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()
|
||||
@ -272,15 +273,24 @@ object ConversationMenuHelper {
|
||||
val accountID = TextSecurePreferences.getLocalNumber(context)
|
||||
val isCurrentUserAdmin = admins.any { it.toString() == accountID }
|
||||
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 {
|
||||
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 {
|
||||
title(R.string.ConversationActivity_leave_group)
|
||||
title(R.string.groupLeave)
|
||||
text(message)
|
||||
button(R.string.yes) {
|
||||
try {
|
||||
@ -309,7 +319,7 @@ object ConversationMenuHelper {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -8,17 +8,19 @@ import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.squareup.phrase.Phrase
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewControlMessageBinding
|
||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
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.expiryMode
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ControlMessageView : LinearLayout {
|
||||
@ -75,8 +77,10 @@ class ControlMessageView : LinearLayout {
|
||||
}
|
||||
}
|
||||
message.isMessageRequestResponse -> {
|
||||
binding.textView.text = context.getString(R.string.message_requests_accepted)
|
||||
binding.root.contentDescription=context.getString(R.string.AccessibilityId_message_request_config_message)
|
||||
binding.textView.text = context.getString(R.string.messageRequestsAccepted)
|
||||
binding.root.contentDescription = Phrase.from(context, R.string.messageRequestYouHaveAccepted)
|
||||
.put(NAME_KEY, message.individualRecipient.name)
|
||||
.format()
|
||||
}
|
||||
message.isCallLog -> {
|
||||
val drawable = when {
|
||||
|
@ -21,7 +21,7 @@ class DeletedMessageView : LinearLayout {
|
||||
// region Updating
|
||||
fun bind(message: MessageRecord, @ColorInt textColor: Int) {
|
||||
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.deletedMessageViewIconImageView.imageTintList = ColorStateList.valueOf(textColor)
|
||||
}
|
||||
|
@ -12,8 +12,10 @@ import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.setPadding
|
||||
import com.google.android.flexbox.JustifyContent
|
||||
import com.squareup.phrase.Phrase
|
||||
import network.loki.messenger.R
|
||||
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.ThemeUtil
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
@ -199,7 +201,7 @@ class EmojiReactionsView : ConstraintLayout, OnTouchListener {
|
||||
} else {
|
||||
emojiView.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) {
|
||||
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.view.MotionEvent
|
||||
import android.widget.LinearLayout
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.isVisible
|
||||
import com.bumptech.glide.RequestManager
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewLinkPreviewBinding
|
||||
import org.session.libsession.utilities.getColorFromAttr
|
||||
import org.session.libsignal.utilities.Log
|
||||
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.database.model.MmsMessageRecord
|
||||
import com.bumptech.glide.RequestManager
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide
|
||||
|
||||
class LinkPreviewView : LinearLayout {
|
||||
@ -84,10 +84,11 @@ class LinkPreviewView : LinearLayout {
|
||||
}
|
||||
}
|
||||
|
||||
fun openURL() {
|
||||
val url = this.url ?: return
|
||||
val activity = context as AppCompatActivity
|
||||
ModalUrlBottomSheet(url).show(activity.supportFragmentManager, "Open URL Dialog")
|
||||
// Method to show the open or copy URL dialog
|
||||
private fun openURL() {
|
||||
val url = this.url ?: return Log.w("LinkPreviewView", "Cannot open a null URL")
|
||||
val activity = context as? ConversationActivityV2
|
||||
activity?.showOpenUrlDialog(url)
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -75,13 +75,13 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
|
||||
val quoteIsLocalUser = localNumber != null && authorPublicKey == localNumber
|
||||
|
||||
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)}"
|
||||
binding.quoteViewAuthorTextView.text = authorDisplayName
|
||||
binding.quoteViewAuthorTextView.setTextColor(getTextColor(isOutgoingMessage))
|
||||
// Body
|
||||
binding.quoteViewBodyTextView.text = if (isOpenGroupInvitation)
|
||||
resources.getString(R.string.open_group_invitation_view__open_group_invitation)
|
||||
resources.getString(R.string.communityInvitation)
|
||||
else MentionUtilities.highlightMentions(
|
||||
text = (body ?: "").toSpannable(),
|
||||
isOutgoingMessage = isOutgoingMessage,
|
||||
@ -106,7 +106,7 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
|
||||
attachments.audioSlide != null -> {
|
||||
binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone)
|
||||
binding.quoteViewAttachmentPreviewImageView.isVisible = true
|
||||
binding.quoteViewBodyTextView.text = resources.getString(R.string.Slide_audio)
|
||||
binding.quoteViewBodyTextView.text = resources.getString(R.string.audio)
|
||||
}
|
||||
attachments.documentSlide != null -> {
|
||||
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))
|
||||
binding.quoteViewAttachmentThumbnailImageView.root.setImageResource(glide, slide, false)
|
||||
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 androidx.annotation.ColorInt
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.squareup.phrase.Phrase
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewUntrustedAttachmentBinding
|
||||
import org.session.libsession.utilities.StringSubstitutionConstants.FILE_TYPE_KEY
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.conversation.v2.dialogs.DownloadDialog
|
||||
import org.thoughtcrime.securesms.util.ActivityDispatcher
|
||||
import java.util.Locale
|
||||
|
||||
class UntrustedAttachmentView: LinearLayout {
|
||||
private val binding: ViewUntrustedAttachmentBinding by lazy { ViewUntrustedAttachmentBinding.bind(this) }
|
||||
@ -30,13 +31,17 @@ class UntrustedAttachmentView: LinearLayout {
|
||||
// region Updating
|
||||
fun bind(attachmentType: AttachmentType, @ColorInt textColor: Int) {
|
||||
val (iconRes, stringRes) = when (attachmentType) {
|
||||
AttachmentType.AUDIO -> R.drawable.ic_microphone to R.string.Slide_audio
|
||||
AttachmentType.DOCUMENT -> R.drawable.ic_document_large_light to R.string.document
|
||||
AttachmentType.AUDIO -> R.drawable.ic_microphone to R.string.audio
|
||||
AttachmentType.DOCUMENT -> R.drawable.ic_document_large_light to R.string.files
|
||||
AttachmentType.MEDIA -> R.drawable.ic_image_white_24dp to R.string.media
|
||||
}
|
||||
val iconDrawable = ContextCompat.getDrawable(context,iconRes)!!
|
||||
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.untrustedAttachmentTitle.text = text
|
||||
|
@ -12,34 +12,29 @@ import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.text.getSpans
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.isVisible
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.RequestManager
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewVisibleMessageContentBinding
|
||||
import okhttp3.HttpUrl
|
||||
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.utilities.ThemeUtil
|
||||
import org.session.libsession.utilities.getColorFromAttr
|
||||
import org.session.libsession.utilities.modifyLayoutParams
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
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.ModalURLSpan
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
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.SearchUtil
|
||||
import org.thoughtcrime.securesms.util.getAccentColor
|
||||
@ -117,7 +112,7 @@ class VisibleMessageContentView : ConstraintLayout {
|
||||
binding.quoteView.root.isVisible = true
|
||||
val quote = message.quote!!
|
||||
val quoteText = if (quote.isOriginalMissing) {
|
||||
context.getString(R.string.QuoteView_original_missing)
|
||||
context.getString(R.string.messageErrorOriginal)
|
||||
} else {
|
||||
quote.text
|
||||
}
|
||||
@ -292,8 +287,8 @@ class VisibleMessageContentView : ConstraintLayout {
|
||||
body.getSpans<URLSpan>(0, body.length).toList().forEach { urlSpan ->
|
||||
val updatedUrl = urlSpan.url.let { it.toHttpUrlOrNull().toString() }
|
||||
val replacementSpan = ModalURLSpan(updatedUrl) { url ->
|
||||
val activity = context as AppCompatActivity
|
||||
ModalUrlBottomSheet(url).show(activity.supportFragmentManager, "Open URL Dialog")
|
||||
val activity = context as? ConversationActivityV2
|
||||
activity?.showOpenUrlDialog(url)
|
||||
}
|
||||
val start = body.getSpanStart(urlSpan)
|
||||
val end = body.getSpanEnd(urlSpan)
|
||||
|
@ -27,6 +27,13 @@ import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.marginBottom
|
||||
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.databinding.ViewEmojiReactionsBinding
|
||||
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.toDp
|
||||
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"
|
||||
|
||||
@ -269,8 +269,7 @@ class VisibleMessageView : FrameLayout {
|
||||
// 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
|
||||
// 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
|
||||
// animated clock icon for incoming messages.
|
||||
// be displaying either "Sent" or "Read" and the animating clock icon.
|
||||
private fun showStatusMessage(message: MessageRecord) {
|
||||
// We'll start by hiding everything and then only make visible what we need
|
||||
binding.messageStatusTextView.isVisible = false
|
||||
@ -384,37 +383,47 @@ class VisibleMessageView : FrameLayout {
|
||||
message.isFailed ->
|
||||
MessageStatusInfo(R.drawable.ic_delivery_status_failed,
|
||||
getThemedColor(context, R.attr.danger),
|
||||
R.string.delivery_status_failed
|
||||
R.string.messageStatusFailedToSend
|
||||
)
|
||||
message.isSyncFailed ->
|
||||
MessageStatusInfo(
|
||||
R.drawable.ic_delivery_status_failed,
|
||||
context.getColor(R.color.accent_orange),
|
||||
R.string.delivery_status_sync_failed
|
||||
R.string.messageStatusFailedToSync
|
||||
)
|
||||
message.isPending ->
|
||||
MessageStatusInfo(
|
||||
R.drawable.ic_delivery_status_sending,
|
||||
context.getColorFromAttr(R.attr.message_status_color),
|
||||
R.string.delivery_status_sending
|
||||
)
|
||||
// Non-mms messages display 'Sending'..
|
||||
if (!message.isMms) {
|
||||
MessageStatusInfo(
|
||||
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 ->
|
||||
MessageStatusInfo(
|
||||
R.drawable.ic_delivery_status_sending,
|
||||
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 ->
|
||||
MessageStatusInfo(
|
||||
R.drawable.ic_delivery_status_read,
|
||||
context.getColorFromAttr(R.attr.message_status_color),
|
||||
R.string.delivery_status_read
|
||||
R.string.read
|
||||
)
|
||||
message.isSent ->
|
||||
MessageStatusInfo(
|
||||
R.drawable.ic_delivery_status_sent,
|
||||
context.getColorFromAttr(R.attr.message_status_color),
|
||||
R.string.delivery_status_sent
|
||||
R.string.disappearingMessagesSent
|
||||
)
|
||||
else -> {
|
||||
// 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.View
|
||||
import android.widget.LinearLayout
|
||||
import com.squareup.phrase.Phrase
|
||||
import network.loki.messenger.R
|
||||
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 {
|
||||
private lateinit var binding: ViewSearchBottomBarBinding
|
||||
@ -35,7 +37,7 @@ class SearchBottomBar : LinearLayout {
|
||||
}
|
||||
}
|
||||
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 {
|
||||
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 {
|
||||
|
||||
fun getResults(): List<MessageResult?> {
|
||||
|
@ -16,6 +16,9 @@
|
||||
*/
|
||||
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.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
@ -30,10 +33,14 @@ import android.provider.OpenableColumns;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
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.libsignal.utilities.ListenableFuture;
|
||||
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.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 {
|
||||
|
||||
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 AttachmentListener attachmentListener;
|
||||
|
||||
@ -252,13 +255,31 @@ public class AttachmentManager {
|
||||
} else {
|
||||
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.
|
||||
.execute();
|
||||
}
|
||||
|
||||
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);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
builder = builder.request(Manifest.permission.READ_MEDIA_VIDEO)
|
||||
@ -266,8 +287,8 @@ public class AttachmentManager {
|
||||
} else {
|
||||
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)
|
||||
builder.withPermanentDenialDialog(cameraPermissionDeniedTxt)
|
||||
.withRationaleDialog(needStoragePermissionTxt, R.drawable.ic_baseline_photo_library_24)
|
||||
.onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode))
|
||||
.execute();
|
||||
}
|
||||
@ -291,10 +312,19 @@ public class AttachmentManager {
|
||||
}
|
||||
|
||||
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)
|
||||
.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))
|
||||
.withRationaleDialog(activity.getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera),R.drawable.ic_baseline_photo_camera_24)
|
||||
.withPermanentDenialDialog(cameraPermissionDeniedTxt)
|
||||
.withRationaleDialog(requireCameraPermissionTxt, R.drawable.ic_baseline_photo_camera_24)
|
||||
.onAllGranted(() -> {
|
||||
Intent captureIntent = MediaSendActivity.buildCameraIntent(activity, recipient);
|
||||
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
|
||||
@ -326,7 +356,7 @@ public class AttachmentManager {
|
||||
activity.startActivityForResult(intent, requestCode);
|
||||
} catch (ActivityNotFoundException anfe) {
|
||||
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 @NonNull MediaConstraints constraints)
|
||||
{
|
||||
return slide == null ||
|
||||
constraints.isSatisfied(context, slide.asAttachment()) ||
|
||||
constraints.canResize(slide.asAttachment());
|
||||
// Null attachment? Not satisfied.
|
||||
if (slide == null) return false;
|
||||
|
||||
// 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 {
|
||||
|
@ -54,13 +54,14 @@ object MentionUtilities {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||
val openGroup by lazy { DatabaseComponent.get(context).storage().getOpenGroup(threadID) }
|
||||
|
||||
// format the mention text
|
||||
// Format the mention text
|
||||
if (matcher.find(startIndex)) {
|
||||
while (true) {
|
||||
val publicKey = text.subSequence(matcher.start() + 1, matcher.end()).toString() // +1 to get rid of the @
|
||||
|
||||
val isYou = isYou(publicKey, userPublicKey, openGroup)
|
||||
val userDisplayName: String? = if (isYou) {
|
||||
context.getString(R.string.MessageRecord_you)
|
||||
context.getString(R.string.you)
|
||||
} else {
|
||||
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithAccountID(publicKey)
|
||||
@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 {
|
||||
fun showNotifyDialog(context: Context, thread: Recipient, notifyTypeHandler: (Int)->Unit) {
|
||||
context.showSessionDialog {
|
||||
title(R.string.RecipientPreferenceActivity_notification_settings)
|
||||
title(R.string.sessionNotifications)
|
||||
singleChoiceItems(
|
||||
context.resources.getStringArray(R.array.notify_types),
|
||||
thread.notifyType
|
||||
|
@ -1,9 +1,13 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.utilities
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.graphics.Typeface
|
||||
import android.text.Layout
|
||||
import android.text.SpannableString
|
||||
import android.text.Spanned
|
||||
import android.text.StaticLayout
|
||||
import android.text.TextPaint
|
||||
import android.text.style.StyleSpan
|
||||
import android.view.MotionEvent
|
||||
import android.widget.TextView
|
||||
import androidx.core.text.getSpans
|
||||
|
@ -3,14 +3,8 @@ package org.thoughtcrime.securesms.database;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@ -24,10 +18,10 @@ public class DraftDatabase extends Database {
|
||||
public static final String DRAFT_VALUE = "value";
|
||||
|
||||
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 = {
|
||||
"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) {
|
||||
@ -59,8 +53,8 @@ public class DraftDatabase extends Database {
|
||||
|
||||
for (long threadId : threadIds) {
|
||||
where.append(" OR ")
|
||||
.append(THREAD_ID)
|
||||
.append(" = ?");
|
||||
.append(THREAD_ID)
|
||||
.append(" = ?");
|
||||
|
||||
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 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";
|
||||
public static final String TEXT = "text";
|
||||
|
||||
private final String type;
|
||||
private final String value;
|
||||
@ -117,48 +109,10 @@ public class DraftDatabase extends Database {
|
||||
public String getValue() {
|
||||
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> {
|
||||
private Draft getDraftOfType(String type) {
|
||||
for (Draft draft : this) {
|
||||
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;
|
||||
}
|
||||
// We don't do anything with drafts of a given type anymore (image, audio etc.) - we store TEXT
|
||||
// drafts, and any files or audio get sent to the recipient when added as a message.
|
||||
}
|
||||
}
|
||||
}
|
@ -633,7 +633,11 @@ open class Storage(
|
||||
// Notify the user
|
||||
val threadID = getOrCreateThreadIdFor(Address.fromSerialized(groupId))
|
||||
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
|
||||
// Start polling
|
||||
ClosedGroupPollerV2.shared.startPolling(group.accountId)
|
||||
|
@ -808,8 +808,8 @@ public class ThreadDatabase extends Database {
|
||||
private @NonNull String getFormattedBodyFor(@NonNull MessageRecord messageRecord) {
|
||||
if (messageRecord.isMms()) {
|
||||
MmsMessageRecord record = (MmsMessageRecord) messageRecord;
|
||||
if (record.getSharedContacts().size() > 0) {
|
||||
Contact contact = ((MmsMessageRecord) messageRecord).getSharedContacts().get(0);
|
||||
if (!record.getSharedContacts().isEmpty()) {
|
||||
Contact contact = ((MmsMessageRecord)messageRecord).getSharedContacts().get(0);
|
||||
return ContactUtil.getStringSummary(context, contact).toString();
|
||||
}
|
||||
String attachmentString = record.getSlideDeck().getBody();
|
||||
|
@ -1,18 +1,20 @@
|
||||
package org.thoughtcrime.securesms.database.helpers;
|
||||
|
||||
import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
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.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabaseHook;
|
||||
import net.zetetic.database.sqlcipher.SQLiteOpenHelper;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
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.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
@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
|
||||
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);
|
||||
channel.enableVibration(true);
|
||||
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)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setColor(context.getResources().getColor(R.color.textsecure_primary))
|
||||
.setCategory(NotificationCompat.CATEGORY_ERROR)
|
||||
.setContentTitle(context.getString(R.string.ErrorNotifier_migration))
|
||||
.setContentText(context.getString(R.string.ErrorNotifier_migration_downgrade))
|
||||
.setContentTitle(context.getString(R.string.errorDatabase))
|
||||
.setContentText(errorTxt)
|
||||
.setAutoCancel(true);
|
||||
|
||||
notificationManager.notify(5874, builder.build());
|
||||
|
@ -14,6 +14,8 @@ import org.session.libsession.utilities.Address;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.RelativeDay;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
@ -88,16 +90,18 @@ public class BucketedThreadMediaLoader extends AsyncTaskLoader<BucketedThreadMed
|
||||
|
||||
private final TimeBucket[] TIME_SECTIONS;
|
||||
|
||||
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));
|
||||
this.YESTERDAY = new TimeBucket(context.getString(R.string.BucketedThreadMedia_Yesterday), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -2), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -1));
|
||||
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.TIME_SECTIONS = new TimeBucket[]{TODAY, YESTERDAY, THIS_WEEK, THIS_MONTH};
|
||||
this.OLDER = new MonthBuckets();
|
||||
public BucketedThreadMedia(@NonNull Context context) {
|
||||
String localisedTodayString = DateUtils.INSTANCE.getLocalisedRelativeDayString(RelativeDay.TODAY);
|
||||
String localisedYesterdayString = DateUtils.INSTANCE.getLocalisedRelativeDayString(RelativeDay.YESTERDAY);
|
||||
|
||||
this.TODAY = new TimeBucket(localisedTodayString, TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -1), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, 1000));
|
||||
this.YESTERDAY = new TimeBucket(localisedYesterdayString, TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -2), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -1));
|
||||
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) {
|
||||
for (TimeBucket timeSection : TIME_SECTIONS) {
|
||||
if (timeSection.inRange(mediaRecord.getDate())) {
|
||||
|
@ -77,14 +77,6 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
||||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -57,15 +57,7 @@ public class SmsMessageRecord extends MessageRecord {
|
||||
|
||||
@Override
|
||||
public SpannableString getDisplayBody(@NonNull Context context) {
|
||||
if (SmsDatabase.Types.isFailedDecryptType(type)) {
|
||||
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);
|
||||
}
|
||||
return super.getDisplayBody(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -17,21 +17,25 @@
|
||||
*/
|
||||
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.net.Uri;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.StyleSpan;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.squareup.phrase.Phrase;
|
||||
import org.session.libsession.utilities.ExpirationUtil;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
/**
|
||||
@ -42,146 +46,179 @@ import network.loki.messenger.R;
|
||||
*/
|
||||
public class ThreadRecord extends DisplayRecord {
|
||||
|
||||
private @Nullable final Uri snippetUri;
|
||||
public @Nullable final MessageRecord lastMessage;
|
||||
private final long count;
|
||||
private final int unreadCount;
|
||||
private final int unreadMentionCount;
|
||||
private final int distributionType;
|
||||
private final boolean archived;
|
||||
private final long expiresIn;
|
||||
private final long lastSeen;
|
||||
private final boolean pinned;
|
||||
private final int initialRecipientHash;
|
||||
private @Nullable final Uri snippetUri;
|
||||
public @Nullable final MessageRecord lastMessage;
|
||||
private final long count;
|
||||
private final int unreadCount;
|
||||
private final int unreadMentionCount;
|
||||
private final int distributionType;
|
||||
private final boolean archived;
|
||||
private final long expiresIn;
|
||||
private final long lastSeen;
|
||||
private final boolean pinned;
|
||||
private final int initialRecipientHash;
|
||||
private final long dateSent;
|
||||
|
||||
public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri,
|
||||
@Nullable MessageRecord lastMessage, @NonNull Recipient recipient, long date, long count, int unreadCount,
|
||||
int unreadMentionCount, long threadId, int deliveryReceiptCount, int status,
|
||||
long snippetType, int distributionType, boolean archived, long expiresIn,
|
||||
long lastSeen, int readReceiptCount, boolean pinned)
|
||||
{
|
||||
super(body, recipient, date, date, threadId, status, deliveryReceiptCount, snippetType, readReceiptCount);
|
||||
this.snippetUri = snippetUri;
|
||||
this.lastMessage = lastMessage;
|
||||
this.count = count;
|
||||
this.unreadCount = unreadCount;
|
||||
this.unreadMentionCount = unreadMentionCount;
|
||||
this.distributionType = distributionType;
|
||||
this.archived = archived;
|
||||
this.expiresIn = expiresIn;
|
||||
this.lastSeen = lastSeen;
|
||||
this.pinned = pinned;
|
||||
this.initialRecipientHash = recipient.hashCode();
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri,
|
||||
@Nullable MessageRecord lastMessage, @NonNull Recipient recipient, long date, long count, int unreadCount,
|
||||
int unreadMentionCount, long threadId, int deliveryReceiptCount, int status,
|
||||
long snippetType, int distributionType, boolean archived, long expiresIn,
|
||||
long lastSeen, int readReceiptCount, boolean pinned)
|
||||
{
|
||||
super(body, recipient, date, date, threadId, status, deliveryReceiptCount, snippetType, readReceiptCount);
|
||||
this.snippetUri = snippetUri;
|
||||
this.lastMessage = lastMessage;
|
||||
this.count = count;
|
||||
this.unreadCount = unreadCount;
|
||||
this.unreadMentionCount = unreadMentionCount;
|
||||
this.distributionType = distributionType;
|
||||
this.archived = archived;
|
||||
this.expiresIn = expiresIn;
|
||||
this.lastSeen = lastSeen;
|
||||
this.pinned = pinned;
|
||||
this.initialRecipientHash = recipient.hashCode();
|
||||
this.dateSent = date;
|
||||
}
|
||||
}
|
||||
|
||||
private SpannableString emphasisAdded(String sequence) {
|
||||
return emphasisAdded(sequence, 0, sequence.length());
|
||||
}
|
||||
public @Nullable Uri getSnippetUri() {
|
||||
return snippetUri;
|
||||
}
|
||||
|
||||
private SpannableString emphasisAdded(String sequence, int start, int end) {
|
||||
SpannableString spannable = new SpannableString(sequence);
|
||||
spannable.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC),
|
||||
start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return spannable;
|
||||
}
|
||||
private String getName() {
|
||||
String name = getRecipient().getName();
|
||||
if (name == null) {
|
||||
Log.w("ThreadRecord", "Got a null name - using: Unknown");
|
||||
name = "Unknown";
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
public long getCount() {
|
||||
return count;
|
||||
}
|
||||
private String getDisappearingMsgExpiryTypeString(Context context) {
|
||||
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() {
|
||||
return unreadCount;
|
||||
}
|
||||
// Note: This works because expireStarted is 0 for messages which are 'Disappear after read'
|
||||
// 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() {
|
||||
return unreadMentionCount;
|
||||
}
|
||||
@Override
|
||||
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() {
|
||||
return getDateReceived();
|
||||
}
|
||||
// Implied that disappearing messages is enabled..
|
||||
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() {
|
||||
return archived;
|
||||
}
|
||||
} else if (MmsSmsColumns.Types.isMediaSavedExtraction(type)) {
|
||||
String txt = Phrase.from(context, R.string.attachmentsMediaSaved)
|
||||
.put(NAME_KEY, getName())
|
||||
.format().toString();
|
||||
return emphasisAdded(txt);
|
||||
|
||||
public int getDistributionType() {
|
||||
return distributionType;
|
||||
}
|
||||
} else if (MmsSmsColumns.Types.isScreenshotExtraction(type)) {
|
||||
String txt = Phrase.from(context, R.string.screenshotTaken)
|
||||
.put(NAME_KEY, getName())
|
||||
.format().toString();
|
||||
return emphasisAdded(txt);
|
||||
|
||||
public long getExpiresIn() {
|
||||
return expiresIn;
|
||||
}
|
||||
} else if (MmsSmsColumns.Types.isMessageRequestResponse(type)) {
|
||||
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() {
|
||||
return lastSeen;
|
||||
}
|
||||
private SpannableString emphasisAdded(String sequence) {
|
||||
return emphasisAdded(sequence, 0, sequence.length());
|
||||
}
|
||||
|
||||
public boolean isPinned() {
|
||||
return pinned;
|
||||
}
|
||||
private SpannableString emphasisAdded(String sequence, int start, int end) {
|
||||
SpannableString spannable = new SpannableString(sequence);
|
||||
spannable.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC),
|
||||
start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return spannable;
|
||||
}
|
||||
|
||||
public int getInitialRecipientHash() {
|
||||
return initialRecipientHash;
|
||||
}
|
||||
public long getCount() { return count; }
|
||||
|
||||
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),
|
||||
OBJECTS(5, "Objects", R.attr.emoji_category_objects),
|
||||
SYMBOLS(6, "Symbols", R.attr.emoji_category_symbol),
|
||||
FLAGS(7, "Flags", R.attr.emoji_category_flags),
|
||||
EMOTICONS(8, "Emoticons", R.attr.emoji_category_emoticons);
|
||||
FLAGS(7, "Flags", R.attr.emoji_category_flags);
|
||||
|
||||
@StringRes
|
||||
fun getCategoryLabel(): Int {
|
||||
@ -31,15 +30,14 @@ enum class EmojiCategory(val priority: Int, val key: String, @AttrRes val icon:
|
||||
@StringRes
|
||||
fun getCategoryLabel(@AttrRes iconAttr: Int): Int {
|
||||
return when (iconAttr) {
|
||||
R.attr.emoji_category_people -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__smileys_and_people
|
||||
R.attr.emoji_category_nature -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__nature
|
||||
R.attr.emoji_category_foods -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__food
|
||||
R.attr.emoji_category_activity -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__activities
|
||||
R.attr.emoji_category_places -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__places
|
||||
R.attr.emoji_category_objects -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__objects
|
||||
R.attr.emoji_category_symbol -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__symbols
|
||||
R.attr.emoji_category_flags -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__flags
|
||||
R.attr.emoji_category_emoticons -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__emoticons
|
||||
R.attr.emoji_category_people -> R.string.emojiCategorySmileys
|
||||
R.attr.emoji_category_nature -> R.string.emojiCategoryAnimals
|
||||
R.attr.emoji_category_foods -> R.string.emojiCategoryFood
|
||||
R.attr.emoji_category_activity -> R.string.emojiCategoryActivities
|
||||
R.attr.emoji_category_places -> R.string.emojiCategoryTravel
|
||||
R.attr.emoji_category_objects -> R.string.emojiCategoryObjects
|
||||
R.attr.emoji_category_symbol -> R.string.emojiCategorySymbols
|
||||
R.attr.emoji_category_flags -> R.string.emojiCategoryFlags
|
||||
else -> throw AssertionError()
|
||||
}
|
||||
}
|
||||
|
@ -110,10 +110,12 @@ class EmojiSource(
|
||||
val parsedData: ParsedEmojiData = EmojiJsonParser.parse(it, ::getAssetsUri).getOrThrow()
|
||||
return EmojiSource(
|
||||
ScreenDensity.xhdpiRelativeDensityScaleFactor("xhdpi"),
|
||||
|
||||
parsedData.copy(
|
||||
displayPages = parsedData.displayPages + PAGE_EMOTICONS,
|
||||
dataPages = parsedData.dataPages + PAGE_EMOTICONS
|
||||
displayPages = parsedData.displayPages,
|
||||
dataPages = parsedData.dataPages
|
||||
)
|
||||
|
||||
) { 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)
|
||||
|
||||
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 org.session.libsession.utilities.MediaTypes;
|
||||
import org.session.libsession.utilities.NonTranslatableStringConstants;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
@ -120,7 +121,7 @@ public class GiphyActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
protected void onPostExecute(@Nullable Uri uri) {
|
||||
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) {
|
||||
Intent intent = new Intent();
|
||||
intent.setData(uri);
|
||||
@ -165,8 +166,8 @@ public class GiphyActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
if (position == 0) return context.getString(R.string.GiphyFragmentPagerAdapter_gifs);
|
||||
else return context.getString(R.string.GiphyFragmentPagerAdapter_stickers);
|
||||
if (position == 0) return NonTranslatableStringConstants.GIF;
|
||||
else return context.getString(R.string.stickers);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,17 +76,20 @@ class CreateGroupFragment : Fragment() {
|
||||
if (isLoading) return@setOnClickListener
|
||||
val name = binding.nameEditText.text.trim()
|
||||
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)) {
|
||||
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
|
||||
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
|
||||
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())!!
|
||||
isLoading = true
|
||||
|
@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.groups
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableString
|
||||
import android.text.style.StyleSpan
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
@ -16,7 +18,10 @@ import androidx.loader.app.LoaderManager
|
||||
import androidx.loader.content.Loader
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.squareup.phrase.Phrase
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
import network.loki.messenger.R
|
||||
import nl.komponents.kovenant.Promise
|
||||
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.utilities.Address
|
||||
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.ThemeUtil
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
@ -40,8 +46,6 @@ import org.thoughtcrime.securesms.groups.ClosedGroupManager.updateLegacyGroup
|
||||
import com.bumptech.glide.Glide
|
||||
import org.thoughtcrime.securesms.util.fadeIn
|
||||
import org.thoughtcrime.securesms.util.fadeOut
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
|
||||
@ -107,17 +111,17 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
|
||||
groupID = intent.getStringExtra(groupIDKey)!!
|
||||
val groupInfo = DatabaseComponent.get(this).groupDatabase().getGroup(groupID).get()
|
||||
originalName = groupInfo.title
|
||||
isSelfAdmin = groupInfo.admins.any{ it.serialize() == TextSecurePreferences.getLocalNumber(this) }
|
||||
isSelfAdmin = groupInfo.admins.any { it.serialize() == TextSecurePreferences.getLocalNumber(this) }
|
||||
|
||||
name = originalName
|
||||
|
||||
mainContentContainer = findViewById(R.id.mainContentContainer)
|
||||
cntGroupNameEdit = findViewById(R.id.cntGroupNameEdit)
|
||||
cntGroupNameDisplay = findViewById(R.id.cntGroupNameDisplay)
|
||||
edtGroupName = findViewById(R.id.edtGroupName)
|
||||
emptyStateContainer = findViewById(R.id.emptyStateContainer)
|
||||
lblGroupNameDisplay = findViewById(R.id.lblGroupNameDisplay)
|
||||
loaderContainer = findViewById(R.id.loaderContainer)
|
||||
cntGroupNameEdit = findViewById(R.id.cntGroupNameEdit)
|
||||
cntGroupNameDisplay = findViewById(R.id.cntGroupNameDisplay)
|
||||
edtGroupName = findViewById(R.id.edtGroupName)
|
||||
emptyStateContainer = findViewById(R.id.emptyStateContainer)
|
||||
lblGroupNameDisplay = findViewById(R.id.lblGroupNameDisplay)
|
||||
loaderContainer = findViewById(R.id.loaderContainer)
|
||||
|
||||
findViewById<View>(R.id.addMembersClosedGroupButton).setOnClickListener {
|
||||
onAddMembersClick()
|
||||
@ -129,7 +133,19 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
|
||||
}
|
||||
|
||||
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.btnSaveGroupNameEdit).setOnClickListener { saveName() }
|
||||
edtGroupName.setImeActionLabel(getString(R.string.save), EditorInfo.IME_ACTION_DONE)
|
||||
@ -245,10 +261,10 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
|
||||
private fun saveName() {
|
||||
val name = edtGroupName.text.toString().trim()
|
||||
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) {
|
||||
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
|
||||
lblGroupNameDisplay.text = name
|
||||
@ -283,20 +299,22 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
|
||||
}
|
||||
|
||||
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
|
||||
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 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))) {
|
||||
val message = "Can't leave while adding or removing other members."
|
||||
return Toast.makeText(this@EditClosedGroupActivity, message, Toast.LENGTH_LONG).show()
|
||||
return Log.w("EditClosedGroup", "Can't leave group while adding or removing other members.")
|
||||
}
|
||||
|
||||
if (isClosedGroup) {
|
||||
|
@ -12,6 +12,7 @@ import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.squareup.phrase.Phrase
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
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.GroupUtil
|
||||
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.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.conversation.start.StartConversationDelegate
|
||||
@ -47,6 +49,7 @@ class JoinCommunityFragment : Fragment() {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.backButton.setOnClickListener { delegate.onDialogBackPressed() }
|
||||
binding.closeButton.setOnClickListener { delegate.onDialogClosePressed() }
|
||||
|
||||
fun showLoader() {
|
||||
binding.loader.visibility = View.VISIBLE
|
||||
binding.loader.animate().setDuration(150).alpha(1.0f).start()
|
||||
@ -61,18 +64,23 @@ class JoinCommunityFragment : Fragment() {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun joinCommunityIfPossible(url: String) {
|
||||
val openGroup = try {
|
||||
OpenGroupUrlParser.parseUrl(url)
|
||||
} catch (e: OpenGroupUrlParser.Error) {
|
||||
when (e) {
|
||||
is OpenGroupUrlParser.Error.MalformedURL -> return Toast.makeText(activity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
|
||||
is OpenGroupUrlParser.Error.InvalidPublicKey -> return Toast.makeText(activity, R.string.invalid_public_key, 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.MalformedURL, OpenGroupUrlParser.Error.NoRoom -> {
|
||||
return Toast.makeText(activity, context?.resources?.getString(R.string.communityJoinError), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
is OpenGroupUrlParser.Error.InvalidPublicKey, OpenGroupUrlParser.Error.NoPublicKey -> {
|
||||
return Toast.makeText(activity, R.string.communityEnterUrlErrorInvalidDescription, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showLoader()
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val sanitizedServer = openGroup.server.removeSuffix("/")
|
||||
@ -90,10 +98,11 @@ class JoinCommunityFragment : Fragment() {
|
||||
delegate.onDialogClosePressed()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Couldn't join open group.", e)
|
||||
Log.e("Loki", "Couldn't join community.", e)
|
||||
withContext(Dispatchers.Main) {
|
||||
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
|
||||
}
|
||||
@ -107,8 +116,8 @@ class JoinCommunityFragment : Fragment() {
|
||||
)
|
||||
val mediator = TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, pos ->
|
||||
tab.text = when (pos) {
|
||||
0 -> getString(R.string.activity_join_public_chat_enter_community_url_tab_title)
|
||||
1 -> getString(R.string.activity_join_public_chat_scan_qr_code_tab_title)
|
||||
0 -> getString(R.string.communityUrl)
|
||||
1 -> getString(R.string.qrScan)
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,20 @@
|
||||
package org.thoughtcrime.securesms.groups
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
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 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.OpenGroupApi
|
||||
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.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
object OpenGroupManager {
|
||||
private val executorService = Executors.newScheduledThreadPool(4)
|
||||
@ -111,35 +113,43 @@ object OpenGroupManager {
|
||||
|
||||
@WorkerThread
|
||||
fun delete(server: String, room: String, context: Context) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val configFactory = MessagingModuleConfiguration.shared.configFactory
|
||||
val threadDB = DatabaseComponent.get(context).threadDatabase()
|
||||
val openGroupID = "${server.removeSuffix("/")}.$room"
|
||||
val threadID = GroupManager.getOpenGroupThreadID(openGroupID, context)
|
||||
val recipient = threadDB.getRecipientForThreadId(threadID) ?: return
|
||||
threadDB.setThreadArchived(threadID)
|
||||
val groupID = recipient.address.serialize()
|
||||
// Stop the poller if needed
|
||||
val openGroups = storage.getAllOpenGroups().filter { it.value.server == server }
|
||||
if (openGroups.isNotEmpty()) {
|
||||
synchronized(pollUpdaterLock) {
|
||||
val poller = pollers[server]
|
||||
poller?.stop()
|
||||
pollers.remove(server)
|
||||
try {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val configFactory = MessagingModuleConfiguration.shared.configFactory
|
||||
val threadDB = DatabaseComponent.get(context).threadDatabase()
|
||||
val openGroupID = "${server.removeSuffix("/")}.$room"
|
||||
val threadID = GroupManager.getOpenGroupThreadID(openGroupID, context)
|
||||
val recipient = threadDB.getRecipientForThreadId(threadID) ?: return
|
||||
threadDB.setThreadArchived(threadID)
|
||||
val groupID = recipient.address.serialize()
|
||||
// Stop the poller if needed
|
||||
val openGroups = storage.getAllOpenGroups().filter { it.value.server == server }
|
||||
if (openGroups.isNotEmpty()) {
|
||||
synchronized(pollUpdaterLock) {
|
||||
val poller = pollers[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
|
||||
|
@ -136,7 +136,7 @@ class ConversationView : LinearLayout {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -147,7 +147,7 @@ class ConversationView : LinearLayout {
|
||||
|
||||
private fun ThreadRecord.getSnippetPrefix(): CharSequence? = when {
|
||||
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()
|
||||
}
|
||||
// endregion
|
||||
|
@ -9,20 +9,25 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.squareup.phrase.Phrase
|
||||
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.theme.LocalColors
|
||||
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.SessionColorsParameterProvider
|
||||
import org.thoughtcrime.securesms.ui.theme.ThemeColors
|
||||
import org.thoughtcrime.securesms.ui.theme.LocalColors
|
||||
import org.thoughtcrime.securesms.ui.theme.LocalType
|
||||
|
||||
@Composable
|
||||
internal fun EmptyView(newAccount: Boolean) {
|
||||
@ -44,7 +49,13 @@ internal fun EmptyView(newAccount: Boolean) {
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
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,
|
||||
color = LocalColors.current.primary,
|
||||
textAlign = TextAlign.Center
|
||||
|
@ -8,8 +8,11 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateUtils
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.PluralsRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
@ -17,6 +20,7 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.squareup.phrase.Phrase
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
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.ThreadUtils
|
||||
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.PassphraseRequiredActionBarActivity
|
||||
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.util.ConfigurationMessageUtilities
|
||||
import org.thoughtcrime.securesms.util.IP2Country
|
||||
import org.thoughtcrime.securesms.util.RelativeDay
|
||||
import org.thoughtcrime.securesms.util.disableClipping
|
||||
import org.thoughtcrime.securesms.util.push
|
||||
import org.thoughtcrime.securesms.util.show
|
||||
import org.thoughtcrime.securesms.util.start
|
||||
import java.io.IOException
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
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 FROM_ONBOARDING = "HomeActivity_FROM_ONBOARDING"
|
||||
|
||||
@ -88,6 +104,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
ConversationClickListener,
|
||||
GlobalSearchInputLayout.GlobalSearchInputLayoutListener {
|
||||
|
||||
private val TAG = "HomeActivity"
|
||||
|
||||
private lateinit var binding: ActivityHomeBinding
|
||||
private lateinit var glide: RequestManager
|
||||
|
||||
@ -244,17 +262,17 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
globalSearchViewModel.result.map { result ->
|
||||
result.query to when {
|
||||
result.query.isEmpty() -> buildList {
|
||||
add(GlobalSearchAdapter.Model.Header(R.string.contacts))
|
||||
add(GlobalSearchAdapter.Model.Header(R.string.contactContacts))
|
||||
add(GlobalSearchAdapter.Model.SavedMessages(publicKey))
|
||||
addAll(result.groupedContacts)
|
||||
}
|
||||
else -> buildList {
|
||||
result.contactAndGroupList.takeUnless { it.isEmpty() }?.let {
|
||||
add(GlobalSearchAdapter.Model.Header(R.string.conversations))
|
||||
add(GlobalSearchAdapter.Model.Header(R.string.sessionConversations))
|
||||
addAll(it)
|
||||
}
|
||||
result.messageResults.takeUnless { it.isEmpty() }?.let {
|
||||
add(GlobalSearchAdapter.Model.Header(R.string.global_search_messages))
|
||||
add(GlobalSearchAdapter.Model.Header(R.string.messages))
|
||||
addAll(it)
|
||||
}
|
||||
}
|
||||
@ -427,7 +445,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
val clip = ClipData.newPlainText("Account ID", thread.recipient.address.toString())
|
||||
val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||
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) {
|
||||
val threadId = threadDb.getThreadIdIfExistsFor(thread.recipient)
|
||||
@ -436,7 +454,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
val clip = ClipData.newPlainText("Community URL", openGroup.joinURL)
|
||||
val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||
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 = {
|
||||
@ -482,9 +500,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
|
||||
private fun blockConversation(thread: ThreadRecord) {
|
||||
showSessionDialog {
|
||||
title(R.string.RecipientPreferenceActivity_block_this_contact_question)
|
||||
text(R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact)
|
||||
button(R.string.RecipientPreferenceActivity_block) {
|
||||
title(R.string.block)
|
||||
text(Phrase.from(context, R.string.blockDescription)
|
||||
.put(NAME_KEY, thread.recipient.name)
|
||||
.format())
|
||||
dangerButton(R.string.block, R.string.AccessibilityId_blockConfirm) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
storage.setBlocked(listOf(thread.recipient), true)
|
||||
|
||||
@ -492,6 +512,9 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
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()
|
||||
}
|
||||
@ -499,12 +522,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
|
||||
private fun unblockConversation(thread: ThreadRecord) {
|
||||
showSessionDialog {
|
||||
title(R.string.RecipientPreferenceActivity_unblock_this_contact_question)
|
||||
text(R.string.RecipientPreferenceActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact)
|
||||
button(R.string.RecipientPreferenceActivity_unblock) {
|
||||
title(R.string.blockUnblock)
|
||||
text(Phrase.from(context, R.string.blockUnblockName).put(NAME_KEY, thread.recipient.name).format())
|
||||
dangerButton(R.string.blockUnblock, R.string.AccessibilityId_unblockConfirm) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
storage.setBlocked(listOf(thread.recipient), false)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||
}
|
||||
@ -559,18 +581,42 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
private fun deleteConversation(thread: ThreadRecord) {
|
||||
val threadID = thread.threadId
|
||||
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()
|
||||
|
||||
// If you are an admin of this group you can delete it
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
title(title)
|
||||
text(message)
|
||||
button(R.string.yes) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
@ -583,7 +629,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
GroupUtil.doubleDecodeGroupID(recipient.address.toString()).toHexString()
|
||||
.takeIf(DatabaseComponent.get(context).lokiAPIDatabase()::isClosedGroup)
|
||||
?.let { MessageSender.explicitLeave(it, false) }
|
||||
} catch (_: IOException) {
|
||||
} catch (ioe: IOException) {
|
||||
Log.w(TAG, "Got an IOException while sending leave group message")
|
||||
}
|
||||
}
|
||||
// Delete the conversation
|
||||
@ -597,8 +644,9 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
}
|
||||
// Update the badge count
|
||||
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context)
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
@ -618,7 +666,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
|
||||
private fun hideMessageRequests() {
|
||||
showSessionDialog {
|
||||
text(getString(R.string.hide_message_requests))
|
||||
text(getString(R.string.hide))
|
||||
button(R.string.yes) {
|
||||
textSecurePreferences.setHasHiddenMessageRequests()
|
||||
homeViewModel.tryReload()
|
||||
|
@ -27,9 +27,12 @@ import kotlinx.coroutines.withContext
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityPathBinding
|
||||
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.libsignal.utilities.Snode
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.ui.getSubbedString
|
||||
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
||||
import org.thoughtcrime.securesms.util.IP2Country
|
||||
import org.thoughtcrime.securesms.util.PathDotView
|
||||
@ -49,7 +52,12 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
binding = ActivityPathBinding.inflate(layoutInflater)
|
||||
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.learnMoreButton.setOnClickListener { learnMore() }
|
||||
update(false)
|
||||
@ -98,6 +106,7 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
||||
|
||||
private fun update(isAnimated: Boolean) {
|
||||
binding.pathRowsContainer.removeAllViews()
|
||||
|
||||
if (OnionRequestAPI.paths.isNotEmpty()) {
|
||||
val path = OnionRequestAPI.paths.firstOrNull() ?: return finish()
|
||||
val dotAnimationRepeatInterval = path.count().toLong() * 1000 + 1000
|
||||
@ -105,8 +114,8 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
||||
val isGuardSnode = (OnionRequestAPI.guardSnodes.contains(snode))
|
||||
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 destinationRow = getPathRow(resources.getString(R.string.activity_path_destination_row_title), null, LineView.Location.Bottom, path.count().toLong() * 1000 + 2000, dotAnimationRepeatInterval)
|
||||
val youRow = getPathRow(resources.getString(R.string.onionRoutingPath), null, LineView.Location.Top, 1000, 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 )
|
||||
for (row in rows) {
|
||||
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 {
|
||||
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) {
|
||||
IP2Country.shared.countryNamesCache[snode.ip] ?: resources.getString(R.string.activity_path_resolving_progress)
|
||||
IP2Country.shared.countryNamesCache[snode.ip] ?: resources.getString(R.string.resolving)
|
||||
} else {
|
||||
resources.getString(R.string.activity_path_resolving_progress)
|
||||
resources.getString(R.string.resolving)
|
||||
}
|
||||
return getPathRow(title, subtitle, location, dotAnimationStartDelay, dotAnimationRepeatInterval)
|
||||
}
|
||||
@ -179,7 +188,7 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
startActivity(intent)
|
||||
} 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
|
||||
@ -250,13 +259,11 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
|
||||
startAnimation()
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
|
||||
stopAnimation()
|
||||
}
|
||||
|
||||
|
@ -18,15 +18,15 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
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.theme.ThemeColors
|
||||
import org.thoughtcrime.securesms.ui.theme.LocalColors
|
||||
import org.thoughtcrime.securesms.ui.components.SlimPrimaryOutlineButton
|
||||
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.PreviewTheme
|
||||
import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider
|
||||
import org.thoughtcrime.securesms.ui.theme.ThemeColors
|
||||
|
||||
@Composable
|
||||
internal fun SeedReminder(startRecoveryPasswordActivity: () -> Unit) {
|
||||
@ -49,23 +49,23 @@ internal fun SeedReminder(startRecoveryPasswordActivity: () -> Unit) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Row {
|
||||
Text(
|
||||
stringResource(R.string.save_your_recovery_password),
|
||||
stringResource(R.string.recoveryPasswordBannerTitle),
|
||||
style = LocalType.current.h8
|
||||
)
|
||||
Spacer(Modifier.requiredWidth(LocalDimensions.current.xxsSpacing))
|
||||
SessionShieldIcon()
|
||||
}
|
||||
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
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(LocalDimensions.current.xsSpacing))
|
||||
SlimPrimaryOutlineButton(
|
||||
text = stringResource(R.string.continue_2),
|
||||
text = stringResource(R.string.theContinue),
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.contentDescription(R.string.AccessibilityId_reveal_recovery_phrase_button),
|
||||
.contentDescription(R.string.AccessibilityId_recoveryPasswordBanner),
|
||||
onClick = startRecoveryPasswordActivity
|
||||
)
|
||||
}
|
||||
@ -78,6 +78,6 @@ private fun PreviewSeedReminder(
|
||||
@PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors
|
||||
) {
|
||||
PreviewTheme(colors) {
|
||||
SeedReminder {}
|
||||
SeedReminder { }
|
||||
}
|
||||
}
|
||||
|
@ -100,7 +100,7 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
|
||||
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Account ID", publicKey)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT)
|
||||
Toast.makeText(requireContext(), R.string.copied, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
true
|
||||
}
|
||||
|
@ -6,12 +6,14 @@ import android.text.SpannableStringBuilder
|
||||
import android.text.style.StyleSpan
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import java.util.Locale
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsession.utilities.truncateIdForDisplay
|
||||
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.Header
|
||||
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.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.SearchUtil
|
||||
import java.util.Locale
|
||||
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Contact as ContactModel
|
||||
|
||||
|
||||
class GlobalSearchDiff(
|
||||
private val oldQuery: String?,
|
||||
@ -78,8 +77,8 @@ fun ContentView.bindQuery(query: String, model: GlobalSearchAdapter.Model) {
|
||||
}
|
||||
binding.searchResultSubtitle.text = getHighlight(query, membersString)
|
||||
}
|
||||
is Header, // do nothing for header
|
||||
is SubHeader, // do nothing for subheader
|
||||
is Header, // do nothing for header
|
||||
is SubHeader, // do nothing for subheader
|
||||
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
|
||||
val recipient = Recipient.from(root.context, Address.fromSerialized(model.contact.accountID), false)
|
||||
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()
|
||||
searchResultTitle.text = getHighlight(query, nameString)
|
||||
}
|
||||
@ -120,7 +119,7 @@ fun ContentView.bindModel(query: String?, model: ContactModel) = binding.run {
|
||||
fun ContentView.bindModel(model: SavedMessages) {
|
||||
binding.searchResultSubtitle.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.isVisible = true
|
||||
}
|
||||
@ -128,11 +127,13 @@ fun ContentView.bindModel(model: SavedMessages) {
|
||||
fun ContentView.bindModel(query: String?, model: Message) = binding.apply {
|
||||
searchResultProfilePicture.isVisible = true
|
||||
searchResultTimestamp.isVisible = true
|
||||
|
||||
// val hasUnreads = model.unread > 0
|
||||
// unreadCountIndicator.isVisible = hasUnreads
|
||||
// if (hasUnreads) {
|
||||
// unreadCountTextView.text = model.unread.toString()
|
||||
// }
|
||||
|
||||
searchResultTimestamp.text = DateUtils.getDisplayFormattedTimeSpanString(root.context, Locale.getDefault(), model.messageResult.sentTimestampMs)
|
||||
searchResultProfilePicture.update(model.messageResult.conversationRecipient)
|
||||
val textSpannable = SpannableStringBuilder()
|
||||
@ -146,7 +147,7 @@ fun ContentView.bindModel(query: String?, model: Message) = binding.apply {
|
||||
model.messageResult.bodySnippet
|
||||
))
|
||||
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()
|
||||
searchResultSubtitle.isVisible = true
|
||||
}
|
||||
|
@ -1,216 +1,233 @@
|
||||
package org.thoughtcrime.securesms.linkpreview;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Build;
|
||||
import android.text.Html;
|
||||
import android.text.SpannableString;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.URLSpan;
|
||||
import android.text.util.Linkify;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.session.libsignal.utilities.guava.Optional;
|
||||
|
||||
import org.session.libsession.utilities.Util;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
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 {
|
||||
|
||||
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_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 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 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_HREF_PATTERN = Pattern.compile("href\\s*=\\s*\"([^\"]*)\"", 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_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 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 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_HREF_PATTERN = Pattern.compile("href\\s*=\\s*\"([^\"]*)\"", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* @return All whitelisted URLs in the source text.
|
||||
*/
|
||||
public static @NonNull List<Link> findWhitelistedUrls(@NonNull String text) {
|
||||
SpannableString spannable = new SpannableString(text);
|
||||
boolean found = Linkify.addLinks(spannable, Linkify.WEB_URLS);
|
||||
/**
|
||||
* @return All whitelisted URLs in the source text.
|
||||
*/
|
||||
public static @NonNull List<Link> findWhitelistedUrls(@NonNull String text) {
|
||||
SpannableString spannable = new SpannableString(text);
|
||||
boolean found = Linkify.addLinks(spannable, Linkify.WEB_URLS);
|
||||
|
||||
if (!found) {
|
||||
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;
|
||||
if (!found) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static @NonNull OpenGraph parseOpenGraphFields(@Nullable String html) {
|
||||
return parseOpenGraphFields(html, text -> Html.fromHtml(text).toString());
|
||||
}
|
||||
|
||||
static @NonNull OpenGraph parseOpenGraphFields(@Nullable String html, @NonNull HtmlDecoder htmlDecoder) {
|
||||
if (html == null) {
|
||||
return new OpenGraph(Collections.emptyMap(), null, null);
|
||||
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();
|
||||
}
|
||||
|
||||
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()) {
|
||||
String tag = openGraphMatcher.group();
|
||||
String property = openGraphMatcher.groupCount() > 0 ? openGraphMatcher.group(1) : null;
|
||||
HttpUrl url = HttpUrl.parse(linkUrl);
|
||||
return url != null &&
|
||||
!TextUtils.isEmpty(url.scheme()) &&
|
||||
"https".equals(url.scheme()) &&
|
||||
isLegalUrl(linkUrl);
|
||||
}
|
||||
|
||||
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);
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
String htmlTitle = "";
|
||||
String faviconUrl = "";
|
||||
|
||||
Matcher titleMatcher = TITLE_PATTERN.matcher(html);
|
||||
if (titleMatcher.find() && titleMatcher.groupCount() > 0) {
|
||||
htmlTitle = htmlDecoder.fromEncoded(titleMatcher.group(1));
|
||||
public static @NonNull OpenGraph parseOpenGraphFields(@Nullable String html) {
|
||||
return parseOpenGraphFields(html, text -> Html.fromHtml(text).toString());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
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);
|
||||
|
||||
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 final @Nullable String faviconUrl;
|
||||
private static final String KEY_TITLE = "title";
|
||||
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";
|
||||
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";
|
||||
public OpenGraph(@NonNull Map<String, String> values, @Nullable String htmlTitle, @Nullable String faviconUrl) {
|
||||
this.values = values;
|
||||
this.htmlTitle = htmlTitle;
|
||||
this.faviconUrl = faviconUrl;
|
||||
}
|
||||
|
||||
public OpenGraph(@NonNull Map<String, String> values, @Nullable String htmlTitle, @Nullable String faviconUrl) {
|
||||
this.values = values;
|
||||
this.htmlTitle = htmlTitle;
|
||||
this.faviconUrl = faviconUrl;
|
||||
public @NonNull Optional<String> getTitle() {
|
||||
return Optional.of(Util.getFirstNonEmpty(values.get(KEY_TITLE), htmlTitle));
|
||||
}
|
||||
|
||||
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() {
|
||||
return Optional.of(Util.getFirstNonEmpty(values.get(KEY_TITLE), htmlTitle));
|
||||
public interface HtmlDecoder {
|
||||
@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() -> {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
text = stringResource(R.string.media_overview_documents_fragment__no_documents_found),
|
||||
text = stringResource(R.string.attachmentsFilesEmpty),
|
||||
style = LocalType.current.base,
|
||||
color = LocalColors.current.text
|
||||
)
|
||||
|
@ -35,10 +35,10 @@ class FixedTimeBuckets(
|
||||
@StringRes
|
||||
fun getBucketText(time: ZonedDateTime): Int? {
|
||||
return when {
|
||||
time >= startOfToday -> R.string.BucketedThreadMedia_Today
|
||||
time >= startOfToday -> R.string.BucketedThreadMedia_Today
|
||||
time >= startOfYesterday -> R.string.BucketedThreadMedia_Yesterday
|
||||
time >= startOfThisWeek -> R.string.BucketedThreadMedia_This_week
|
||||
time >= startOfThisMonth -> R.string.BucketedThreadMedia_This_month
|
||||
time >= startOfThisWeek -> R.string.attachmentsThisWeek
|
||||
time >= startOfThisMonth -> R.string.attachmentsThisMonth
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@ -68,7 +69,7 @@ fun MediaOverviewScreen(
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission,
|
||||
R.string.cameraGrantAccessDenied,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
@ -101,31 +102,18 @@ fun MediaOverviewScreen(
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.ConversationItem_unable_to_open_media,
|
||||
R.string.attachmentsErrorOpen,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
is MediaOverviewEvent.ShowSaveAttachmentError -> {
|
||||
val message = context.resources.getQuantityText(
|
||||
R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card,
|
||||
event.errorCount
|
||||
)
|
||||
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
||||
Toast.makeText(context, R.string.attachmentsSaveError, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
is MediaOverviewEvent.ShowSaveAttachmentSuccess -> {
|
||||
val message = if (event.directory.isNotBlank()) {
|
||||
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()
|
||||
Toast.makeText(context, R.string.saved, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -241,15 +229,11 @@ private fun SaveAttachmentWarningDialog(
|
||||
val context = LocalContext.current
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = context.getString(R.string.ConversationFragment_save_to_sd_card),
|
||||
text = context.resources.getQuantityString(
|
||||
R.plurals.ConversationFragment_saving_n_media_to_storage_warning,
|
||||
numSelected,
|
||||
numSelected
|
||||
),
|
||||
title = context.getString(R.string.warning),
|
||||
text = context.resources.getString(R.string.attachmentsWarning),
|
||||
buttons = listOf(
|
||||
DialogButtonModel(GetString(R.string.save), onClick = onAccepted),
|
||||
DialogButtonModel(GetString(android.R.string.cancel), dismissOnClick = true)
|
||||
DialogButtonModel(GetString(R.string.save), GetString(R.string.AccessibilityId_save), color = LocalColors.current.danger, onClick = onAccepted),
|
||||
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
|
||||
get() = when (this) {
|
||||
MediaOverviewTab.Media -> R.string.MediaOverviewActivity_Media
|
||||
MediaOverviewTab.Documents -> R.string.MediaOverviewActivity_Documents
|
||||
}
|
||||
|
||||
MediaOverviewTab.Media -> R.string.media
|
||||
MediaOverviewTab.Documents -> R.string.document
|
||||
}
|
@ -50,7 +50,7 @@ fun MediaOverviewTopAppBar(
|
||||
IconButton(onClick = onSelectAllClicked) {
|
||||
Icon(
|
||||
painterResource(R.drawable.ic_baseline_select_all_24),
|
||||
contentDescription = stringResource(R.string.MediaOverviewActivity_Select_all),
|
||||
contentDescription = stringResource(R.string.selectAll),
|
||||
tint = LocalColors.current.text,
|
||||
)
|
||||
}
|
||||
|
@ -234,11 +234,7 @@ class MediaOverviewViewModel(
|
||||
viewModelScope.launch {
|
||||
val selectedMedia = selectedMedia.toList()
|
||||
|
||||
mutableShowingActionProgress.value = application.resources.getQuantityString(
|
||||
R.plurals.ConversationFragment_saving_n_attachments,
|
||||
selectedMedia.size,
|
||||
selectedMedia.size,
|
||||
)
|
||||
mutableShowingActionProgress.value = application.resources.getString(R.string.saving)
|
||||
|
||||
val attachments = selectedMedia
|
||||
.asSequence()
|
||||
@ -308,7 +304,7 @@ class MediaOverviewViewModel(
|
||||
}
|
||||
|
||||
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
|
||||
val threadId = withContext(Dispatchers.Default) {
|
||||
|
@ -64,7 +64,7 @@ fun MediaPage(
|
||||
state.isEmpty() -> {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
text = stringResource(R.string.media_overview_activity__no_media),
|
||||
text = stringResource(R.string.attachmentsMediaEmpty),
|
||||
style = LocalType.current.base,
|
||||
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