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:
Fanchao Liu 2024-08-21 09:30:02 +10:00 committed by fanchao
parent d1c4283f42
commit 67bcc937ce
667 changed files with 25483 additions and 69013 deletions

View File

@ -221,11 +221,13 @@ android {
} }
dependencies { dependencies {
implementation project(':content-descriptions')
ksp("androidx.hilt:hilt-compiler:$jetpackHiltVersion")
ksp("com.google.dagger:hilt-compiler:$daggerHiltVersion")
ksp("com.github.bumptech.glide:ksp:$glideVersion")
implementation("com.google.dagger:hilt-android:$daggerHiltVersion") implementation("com.google.dagger:hilt-android:$daggerHiltVersion")
ksp("com.google.dagger:hilt-compiler:$daggerHiltVersion")
ksp("androidx.hilt:hilt-compiler:$jetpackHiltVersion")
implementation "androidx.appcompat:appcompat:$appcompatVersion" implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation "com.google.android.material:material:$materialVersion" implementation "com.google.android.material:material:$materialVersion"
@ -249,12 +251,15 @@ dependencies {
implementation 'androidx.fragment:fragment-ktx:1.5.3' implementation 'androidx.fragment:fragment-ktx:1.5.3'
implementation "androidx.core:core-ktx:$coreVersion" implementation "androidx.core:core-ktx:$coreVersion"
implementation "androidx.work:work-runtime-ktx:2.7.1" implementation "androidx.work:work-runtime-ktx:2.7.1"
playImplementation ("com.google.firebase:firebase-messaging:24.0.0") { playImplementation ("com.google.firebase:firebase-messaging:24.0.0") {
exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-core'
exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
} }
if (project.hasProperty('huawei')) huaweiImplementation 'com.huawei.hms:push:6.7.0.300' if (project.hasProperty('huawei')) huaweiImplementation 'com.huawei.hms:push:6.7.0.300'
implementation 'androidx.media3:media3-exoplayer:1.4.0' implementation 'androidx.media3:media3-exoplayer:1.4.0'
implementation 'androidx.media3:media3-ui:1.4.0' implementation 'androidx.media3:media3-ui:1.4.0'
implementation 'org.conscrypt:conscrypt-android:2.5.2' implementation 'org.conscrypt:conscrypt-android:2.5.2'
@ -268,7 +273,6 @@ dependencies {
implementation 'com.github.chrisbanes:PhotoView:2.1.3' implementation 'com.github.chrisbanes:PhotoView:2.1.3'
implementation "com.github.bumptech.glide:glide:$glideVersion" implementation "com.github.bumptech.glide:glide:$glideVersion"
implementation "com.github.bumptech.glide:compose:1.0.0-beta01" implementation "com.github.bumptech.glide:compose:1.0.0-beta01"
ksp "com.github.bumptech.glide:ksp:$glideVersion"
implementation 'com.makeramen:roundedimageview:2.1.0' implementation 'com.makeramen:roundedimageview:2.1.0'
implementation 'com.pnikosis:materialish-progress:1.5' implementation 'com.pnikosis:materialish-progress:1.5'
implementation 'org.greenrobot:eventbus:3.0.0' implementation 'org.greenrobot:eventbus:3.0.0'

View File

@ -2,8 +2,6 @@ package network.loki.messenger
import android.Manifest import android.Manifest
import android.app.Instrumentation import android.app.Instrumentation
import android.content.ClipboardManager
import android.content.Context
import android.view.View import android.view.View
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.pressBack import androidx.test.espresso.Espresso.pressBack
@ -16,8 +14,6 @@ import androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withSubstring
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
@ -25,6 +21,7 @@ import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import com.adevinta.android.barista.interaction.PermissionGranter import com.adevinta.android.barista.interaction.PermissionGranter
import com.bumptech.glide.Glide
import network.loki.messenger.util.InputBarButtonDrawableMatcher.Companion.inputButtonWithDrawable import network.loki.messenger.util.InputBarButtonDrawableMatcher.Companion.inputButtonWithDrawable
import org.hamcrest.Matcher import org.hamcrest.Matcher
import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.allOf
@ -36,11 +33,9 @@ import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.guava.Optional
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBar import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBar
import org.thoughtcrime.securesms.home.HomeActivity import org.thoughtcrime.securesms.home.HomeActivity
import com.bumptech.glide.Glide
/** /**
* Currently not used as part of our CI/Deployment processes !!!! * Currently not used as part of our CI/Deployment processes !!!!
@ -62,7 +57,6 @@ class HomeActivityTests {
@Before @Before
fun setUp() { fun setUp() {
InstrumentationRegistry.getInstrumentation().addMonitor(activityMonitor) InstrumentationRegistry.getInstrumentation().addMonitor(activityMonitor)
} }
@After @After
@ -96,10 +90,10 @@ class HomeActivityTests {
device.pressKeyCode(67) device.pressKeyCode(67)
// Continue with display name // Continue with display name
objectFromDesc(R.string.continue_2).click() objectFromDesc(R.string.theContinue).click()
// Continue with default push notification setting // Continue with default push notification setting
objectFromDesc(R.string.continue_2).click() objectFromDesc(R.string.theContinue).click()
// PN select // PN select
if (hasViewedSeed) { if (hasViewedSeed) {
@ -110,7 +104,6 @@ class HomeActivityTests {
PermissionGranter.allowPermissionsIfNeeded(Manifest.permission.POST_NOTIFICATIONS) PermissionGranter.allowPermissionsIfNeeded(Manifest.permission.POST_NOTIFICATIONS)
} }
/* private fun goToMyChat() { /* private fun goToMyChat() {
onView(withId(R.id.newConversationButton)).perform(ViewActions.click()) onView(withId(R.id.newConversationButton)).perform(ViewActions.click())
onView(withId(R.id.createPrivateChatButton)).perform(ViewActions.click()) onView(withId(R.id.createPrivateChatButton)).perform(ViewActions.click())
@ -131,7 +124,7 @@ class HomeActivityTests {
@Test @Test
fun testLaunches_dismiss_seedView() { fun testLaunches_dismiss_seedView() {
setupLoggedInState() setupLoggedInState()
objectFromDesc(R.string.continue_2).click() objectFromDesc(R.string.theContinue).click()
objectFromDesc(R.string.copy).click() objectFromDesc(R.string.copy).click()
pressBack() pressBack()
onView(withId(R.id.seedReminderView)).check(matches(not(isDisplayed()))) onView(withId(R.id.seedReminderView)).check(matches(not(isDisplayed())))
@ -182,6 +175,7 @@ class HomeActivityTests {
onView(withText(dialogPromptText)).check(matches(isDisplayed())) onView(withText(dialogPromptText)).check(matches(isDisplayed()))
}*/ }*/
/** /**
* Perform action of waiting for a specific time. * Perform action of waiting for a specific time.
*/ */
@ -198,5 +192,4 @@ class HomeActivityTests {
} }
} }
} }
} }

View File

@ -37,7 +37,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
<uses-permission android:name="network.loki.messenger.ACCESS_SESSION_SECRETS" /> <uses-permission android:name="network.loki.messenger.ACCESS_SESSION_SECRETS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- Only used on Android API 29 and lower -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
@ -79,7 +79,7 @@
android:networkSecurityConfig="@xml/network_security_configuration" android:networkSecurityConfig="@xml/network_security_configuration"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Session.DayNight" android:theme="@style/Theme.Session.DayNight"
tools:replace="android:allowBackup"> tools:replace="android:allowBackup,android:label" >
<!-- Disable all analytics --> <!-- Disable all analytics -->
@ -130,12 +130,12 @@
<activity <activity
android:name="org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity" android:name="org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity"
android:exported="false" android:exported="false"
android:label="@string/activity_message_requests_title" android:label="@string/sessionMessageRequests"
android:screenOrientation="portrait" /> android:screenOrientation="portrait" />
<activity <activity
android:name="org.thoughtcrime.securesms.preferences.SettingsActivity" android:name="org.thoughtcrime.securesms.preferences.SettingsActivity"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:label="@string/activity_settings_title" /> android:label="@string/sessionSettings" />
<activity <activity
android:name="org.thoughtcrime.securesms.debugmenu.DebugActivity" android:name="org.thoughtcrime.securesms.debugmenu.DebugActivity"
android:screenOrientation="portrait" android:screenOrientation="portrait"
@ -151,11 +151,11 @@
android:name="org.thoughtcrime.securesms.preferences.BlockedContactsActivity" android:name="org.thoughtcrime.securesms.preferences.BlockedContactsActivity"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:theme="@style/Theme.Session.DayNight.FlatActionBar" android:theme="@style/Theme.Session.DayNight.FlatActionBar"
android:label="@string/blocked_contacts_title" android:label="@string/conversationsBlockedContacts"
/> />
<activity <activity
android:name="org.thoughtcrime.securesms.groups.EditClosedGroupActivity" android:name="org.thoughtcrime.securesms.groups.EditClosedGroupActivity"
android:label="@string/activity_edit_closed_group_title" android:label="@string/groupEdit"
android:screenOrientation="portrait" /> android:screenOrientation="portrait" />
<activity <activity
android:name="org.thoughtcrime.securesms.recoverypassword.RecoveryPasswordActivity" android:name="org.thoughtcrime.securesms.recoverypassword.RecoveryPasswordActivity"
@ -165,7 +165,7 @@
android:screenOrientation="portrait" /> android:screenOrientation="portrait" />
<activity <activity
android:name="org.thoughtcrime.securesms.preferences.PrivacySettingsActivity" android:name="org.thoughtcrime.securesms.preferences.PrivacySettingsActivity"
android:label="@string/activity_privacy_settings_title" android:label="@string/sessionPrivacy"
android:screenOrientation="portrait" /> android:screenOrientation="portrait" />
<activity <activity
android:name="org.thoughtcrime.securesms.preferences.NotificationSettingsActivity" android:name="org.thoughtcrime.securesms.preferences.NotificationSettingsActivity"
@ -175,7 +175,7 @@
android:screenOrientation="portrait" /> android:screenOrientation="portrait" />
<activity <activity
android:name="org.thoughtcrime.securesms.preferences.HelpSettingsActivity" android:name="org.thoughtcrime.securesms.preferences.HelpSettingsActivity"
android:label="@string/activity_help_settings_title" android:label="@string/sessionHelp"
android:screenOrientation="portrait" /> android:screenOrientation="portrait" />
<activity android:name="org.thoughtcrime.securesms.preferences.appearance.AppearanceSettingsActivity" <activity android:name="org.thoughtcrime.securesms.preferences.appearance.AppearanceSettingsActivity"
android:screenOrientation="portrait"/> android:screenOrientation="portrait"/>
@ -268,18 +268,10 @@
<activity <activity
android:name="org.thoughtcrime.securesms.MediaPreviewActivity" android:name="org.thoughtcrime.securesms.MediaPreviewActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:label="@string/AndroidManifest__media_preview"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:theme="@style/Theme.Session.DayNight.NoActionBar" android:theme="@style/Theme.Session.DayNight.NoActionBar"
android:launchMode="singleTask" android:launchMode="singleTask"
android:windowSoftInputMode="stateHidden" /> android:windowSoftInputMode="stateHidden" />
<activity
android:name="org.thoughtcrime.securesms.MediaOverviewActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:launchMode="singleTask"
android:screenOrientation="portrait"
android:theme="@style/Theme.Session.DayNight.NoActionBar"
android:windowSoftInputMode="stateHidden" />
<activity <activity
android:name="org.thoughtcrime.securesms.DummyActivity" android:name="org.thoughtcrime.securesms.DummyActivity"
android:allowTaskReparenting="true" android:allowTaskReparenting="true"

View File

@ -8,19 +8,8 @@ class DeleteMediaDialog {
@JvmStatic @JvmStatic
fun show(context: Context, recordCount: Int, doDelete: Runnable) = context.showSessionDialog { fun show(context: Context, recordCount: Int, doDelete: Runnable) = context.showSessionDialog {
iconAttribute(R.attr.dialog_alert_icon) iconAttribute(R.attr.dialog_alert_icon)
title( title(context.resources.getQuantityString(R.plurals.deleteMessage, recordCount, recordCount))
context.resources.getQuantityString( text(context.resources.getString(R.string.deleteMessageDescriptionEveryone))
R.plurals.MediaOverviewActivity_Media_delete_confirm_title,
recordCount,
recordCount
)
)
text(
context.resources.getQuantityString(R.plurals.MediaOverviewActivity_Media_delete_confirm_message,
recordCount,
recordCount
)
)
button(R.string.delete) { doDelete.run() } button(R.string.delete) { doDelete.run() }
cancelButton() cancelButton()
} }

View File

@ -9,9 +9,9 @@ class DeleteMediaPreviewDialog {
fun show(context: Context, doDelete: Runnable) { fun show(context: Context, doDelete: Runnable) {
context.showSessionDialog { context.showSessionDialog {
iconAttribute(R.attr.dialog_alert_icon) iconAttribute(R.attr.dialog_alert_icon)
title(R.string.MediaPreviewActivity_media_delete_confirmation_title) title(context.resources.getQuantityString(R.plurals.deleteMessage, 1, 1))
text(R.string.MediaPreviewActivity_media_delete_confirmation_message) text(R.string.deleteMessageDescriptionEveryone)
button(R.string.delete) { doDelete.run() } dangerButton(R.string.delete) { doDelete.run() }
cancelButton() cancelButton()
} }
} }

View File

@ -16,6 +16,8 @@
*/ */
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@ -23,8 +25,8 @@ import android.database.Cursor;
import android.database.CursorIndexOutOfBoundsException; import android.database.CursorIndexOutOfBoundsException;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build;
import android.os.Build.VERSION; import android.os.Build.VERSION;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
@ -42,7 +44,6 @@ import android.view.WindowInsetsController;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
@ -54,10 +55,14 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager; import androidx.viewpager.widget.ViewPager;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestManager; import com.bumptech.glide.RequestManager;
import com.squareup.phrase.Phrase;
import java.io.IOException;
import java.util.Locale;
import java.util.WeakHashMap;
import kotlin.Unit;
import network.loki.messenger.R;
import org.session.libsession.messaging.messages.control.DataExtractionNotification; import org.session.libsession.messaging.messages.control.DataExtractionNotification;
import org.session.libsession.messaging.sending_receiving.MessageSender; import org.session.libsession.messaging.sending_receiving.MessageSender;
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment; import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
@ -78,15 +83,8 @@ import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.AttachmentUtil; import org.thoughtcrime.securesms.util.AttachmentUtil;
import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment; import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import java.io.IOException;
import java.util.Locale;
import java.util.WeakHashMap;
import kotlin.Unit;
import network.loki.messenger.R;
/** /**
* Activity for displaying media attachments in-app * Activity for displaying media attachments in-app
@ -242,12 +240,12 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
CharSequence relativeTimeSpan; CharSequence relativeTimeSpan;
if (mediaItem.date > 0) { if (mediaItem.date > 0) {
relativeTimeSpan = DateUtils.getDisplayFormattedTimeSpanString(this, Locale.getDefault(), mediaItem.date); relativeTimeSpan = DateUtils.INSTANCE.getDisplayFormattedTimeSpanString(this, Locale.getDefault(), mediaItem.date);
} else { } else {
relativeTimeSpan = getString(R.string.MediaPreviewActivity_draft); relativeTimeSpan = getString(R.string.draft);
} }
if (mediaItem.outgoing) getSupportActionBar().setTitle(getString(R.string.MediaPreviewActivity_you)); if (mediaItem.outgoing) getSupportActionBar().setTitle(getString(R.string.you));
else if (mediaItem.recipient != null) getSupportActionBar().setTitle(mediaItem.recipient.toShortString()); else if (mediaItem.recipient != null) getSupportActionBar().setTitle(mediaItem.recipient.toShortString());
else getSupportActionBar().setTitle(""); else getSupportActionBar().setTitle("");
@ -258,7 +256,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
initializeMedia(); initializeMedia();
} }
@ -291,7 +288,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
captionContainer = findViewById(R.id.media_preview_caption_container); captionContainer = findViewById(R.id.media_preview_caption_container);
playbackControlsContainer = findViewById(R.id.media_preview_playback_controls_container); playbackControlsContainer = findViewById(R.id.media_preview_playback_controls_container);
setSupportActionBar(findViewById(R.id.toolbar)); setSupportActionBar(findViewById(R.id.search_toolbar));
ActionBar actionBar = getSupportActionBar(); ActionBar actionBar = getSupportActionBar();
actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeButtonEnabled(true); actionBar.setHomeButtonEnabled(true);
@ -361,7 +358,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
private void initializeMedia() { private void initializeMedia() {
if (!isContentTypeSupported(initialMediaType)) { if (!isContentTypeSupported(initialMediaType)) {
Log.w(TAG, "Unsupported media type sent to MediaPreviewActivity, finishing."); Log.w(TAG, "Unsupported media type sent to MediaPreviewActivity, finishing.");
Toast.makeText(getApplicationContext(), R.string.MediaPreviewActivity_unssuported_media_type, Toast.LENGTH_LONG).show(); Toast.makeText(getApplicationContext(), R.string.attachmentsErrorNotSupported, Toast.LENGTH_LONG).show();
finish(); finish();
} }
@ -411,12 +408,19 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
MediaItem mediaItem = getCurrentMediaItem(); MediaItem mediaItem = getCurrentMediaItem();
if (mediaItem == null) return; if (mediaItem == null) return;
SaveAttachmentTask.showWarningDialog(this, 1, () -> { SaveAttachmentTask.showOneTimeWarningDialogOrSave(this, 1, () -> {
Permissions.with(this) Permissions.with(this)
.request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) .request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
.maxSdkVersion(Build.VERSION_CODES.P) .maxSdkVersion(Build.VERSION_CODES.P)
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied)) .withPermanentDenialDialog(Phrase.from(getApplicationContext(), R.string.permissionsStorageSaveDenied)
.onAnyDenied(() -> Toast.makeText(this, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show()) .put(APP_NAME_KEY, getString(R.string.app_name))
.format().toString())
.onAnyDenied(() -> {
String txt = Phrase.from(getApplicationContext(), R.string.permissionsStorageSaveDenied)
.put(APP_NAME_KEY, getString(R.string.app_name))
.format().toString();
Toast.makeText(this, txt, Toast.LENGTH_LONG).show();
})
.onAllGranted(() -> { .onAllGranted(() -> {
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this); SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this);
long saveDate = (mediaItem.date > 0) ? mediaItem.date : SnodeAPI.getNowWithOffset(); long saveDate = (mediaItem.date > 0) ? mediaItem.date : SnodeAPI.getNowWithOffset();
@ -482,6 +486,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
super.onOptionsItemSelected(item); super.onOptionsItemSelected(item);
switch (item.getItemId()) { switch (item.getItemId()) {
// TODO / WARNING: R.id values are NON-CONSTANT in Gradle 8.0+ - what would be the best way to address this?! -AL 2024/08/26
case R.id.media_preview__overview: showOverview(); return true; case R.id.media_preview__overview: showOverview(); return true;
case R.id.media_preview__forward: forward(); return true; case R.id.media_preview__forward: forward(); return true;
case R.id.save: saveToDisk(); return true; case R.id.save: saveToDisk(); return true;
@ -532,15 +537,11 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
throw new RuntimeException("restartItem = " + restartItem + ", data.second = " + data.second + " leftIsRecent = " + leftIsRecent, e); throw new RuntimeException("restartItem = " + restartItem + ", data.second = " + data.second + " leftIsRecent = " + leftIsRecent, e);
} }
if (item == 0) { if (item == 0) { viewPagerListener.onPageSelected(0); }
viewPagerListener.onPageSelected(0);
}
} }
@Override @Override
public void onLoaderReset(@NonNull Loader<Pair<Cursor, Integer>> loader) { public void onLoaderReset(@NonNull Loader<Pair<Cursor, Integer>> loader) { /* Do nothing */ }
}
private class ViewPagerListener implements ViewPager.OnPageChangeListener { private class ViewPagerListener implements ViewPager.OnPageChangeListener {
@ -575,13 +576,11 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
@Override @Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
/* Do nothing */
} }
@Override @Override
public void onPageScrollStateChanged(int state) { public void onPageScrollStateChanged(int state) { /* Do nothing */ }
}
} }
private static class SingleItemPagerAdapter extends MediaItemAdapter { private static class SingleItemPagerAdapter extends MediaItemAdapter {
@ -646,9 +645,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
} }
@Override @Override
public void pause(int position) { public void pause(int position) { /* Do nothing */ }
}
@Override @Override
public @Nullable View getPlaybackControls(int position) { public @Nullable View getPlaybackControls(int position) {

View File

@ -4,24 +4,45 @@ import android.content.Context
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.LocalisedTimeUtil
import org.session.libsession.utilities.StringSubstitutionConstants.TIME_LARGE_KEY
import org.thoughtcrime.securesms.ui.getSubbedString
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
fun showMuteDialog( fun showMuteDialog(
context: Context, context: Context,
onMuteDuration: (Long) -> Unit onMuteDuration: (Long) -> Unit
): AlertDialog = context.showSessionDialog { ): AlertDialog = context.showSessionDialog {
title(R.string.MuteDialog_mute_notifications) title(R.string.notificationsMute)
items(Option.values().map { it.stringRes }.map(context::getString).toTypedArray()) {
onMuteDuration(Option.values()[it].getTime()) items(Option.entries.mapIndexed { index, entry ->
if (entry.stringRes == R.string.notificationsMute) {
context.getString(R.string.notificationsMute)
} else {
val largeTimeUnitString = LocalisedTimeUtil.getDurationWithSingleLargestTimeUnit(context, Option.entries[index].getTime().milliseconds)
context.getSubbedString(entry.stringRes, TIME_LARGE_KEY to largeTimeUnitString)
}
}.toTypedArray()) {
// Note: We add the current timestamp to the mute duration to get the un-mute timestamp
// that gets stored in the database via ConversationMenuHelper.mute().
// Also: This is a kludge, but we ADD one second to the mute duration because otherwise by
// the time the view for how long the conversation is muted for gets set then it's actually
// less than the entire duration - so 1 hour becomes 59 minutes, 1 day becomes 23 hours etc.
// As we really want to see the actual set time (1 hour / 1 day etc.) then we'll bump it by
// 1 second which is neither here nor there in the grand scheme of things.
onMuteDuration(Option.entries[it].getTime() + System.currentTimeMillis() + 1.seconds.inWholeMilliseconds)
} }
} }
private enum class Option(@StringRes val stringRes: Int, val getTime: () -> Long) { private enum class Option(@StringRes val stringRes: Int, val getTime: () -> Long) {
ONE_HOUR(R.string.arrays__mute_for_one_hour, duration = TimeUnit.HOURS.toMillis(1)), ONE_HOUR(R.string.notificationsMuteFor, duration = TimeUnit.HOURS.toMillis(1)),
TWO_HOURS(R.string.arrays__mute_for_two_hours, duration = TimeUnit.HOURS.toMillis(2)), TWO_HOURS(R.string.notificationsMuteFor, duration = TimeUnit.HOURS.toMillis(2)),
ONE_DAY(R.string.arrays__mute_for_one_day, duration = TimeUnit.DAYS.toMillis(1)), ONE_DAY(R.string.notificationsMuteFor, duration = TimeUnit.DAYS.toMillis(1)),
SEVEN_DAYS(R.string.arrays__mute_for_seven_days, duration = TimeUnit.DAYS.toMillis(7)), SEVEN_DAYS(R.string.notificationsMuteFor, duration = TimeUnit.DAYS.toMillis(7)),
FOREVER(R.string.arrays__mute_forever, getTime = { Long.MAX_VALUE }); FOREVER(R.string.notificationsMute, getTime = { Long.MAX_VALUE } );
constructor(@StringRes stringRes: Int, duration: Long): this(stringRes, { System.currentTimeMillis() + duration }) constructor(@StringRes stringRes: Int, duration: Long): this(stringRes, { duration } )
} }

View File

@ -16,6 +16,8 @@
*/ */
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
import android.animation.Animator; import android.animation.Animator;
import android.app.KeyguardManager; import android.app.KeyguardManager;
import android.content.ComponentName; import android.content.ComponentName;
@ -25,20 +27,18 @@ import android.content.ServiceConnection;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.RelativeSizeSpan;
import android.text.style.TypefaceSpan;
import android.view.View; import android.view.View;
import android.view.animation.Animation; import android.view.animation.Animation;
import android.view.animation.BounceInterpolator; import android.view.animation.BounceInterpolator;
import android.view.animation.TranslateAnimation; import android.view.animation.TranslateAnimation;
import android.widget.Button; import android.widget.Button;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView;
import androidx.core.hardware.fingerprint.FingerprintManagerCompat; import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
import androidx.core.os.CancellationSignal; import androidx.core.os.CancellationSignal;
import com.squareup.phrase.Phrase;
import java.security.Signature;
import network.loki.messenger.R;
import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.components.AnimatingToggle; import org.thoughtcrime.securesms.components.AnimatingToggle;
@ -46,11 +46,6 @@ import org.thoughtcrime.securesms.crypto.BiometricSecretProvider;
import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.AnimationCompleteListener; import org.thoughtcrime.securesms.util.AnimationCompleteListener;
import java.security.InvalidKeyException;
import java.security.Signature;
import network.loki.messenger.R;
//TODO Rename to ScreenLockActivity and refactor to Kotlin. //TODO Rename to ScreenLockActivity and refactor to Kotlin.
public class PassphrasePromptActivity extends BaseActionBarActivity { public class PassphrasePromptActivity extends BaseActionBarActivity {
@ -158,6 +153,16 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
} }
private void initializeResources() { private void initializeResources() {
TextView statusTitle = findViewById(R.id.app_lock_status_title);
if (statusTitle != null) {
Context c = getApplicationContext();
String lockedTxt = Phrase.from(c, R.string.lockAppLocked)
.put(APP_NAME_KEY, c.getString(R.string.app_name))
.format().toString();
statusTitle.setText(lockedTxt);
}
visibilityToggle = findViewById(R.id.button_toggle); visibilityToggle = findViewById(R.id.button_toggle);
fingerprintPrompt = findViewById(R.id.fingerprint_auth_container); fingerprintPrompt = findViewById(R.id.fingerprint_auth_container);
lockScreenButton = findViewById(R.id.lock_screen_auth_container); lockScreenButton = findViewById(R.id.lock_screen_auth_container);
@ -165,10 +170,6 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
fingerprintCancellationSignal = new CancellationSignal(); fingerprintCancellationSignal = new CancellationSignal();
fingerprintListener = new FingerprintListener(); fingerprintListener = new FingerprintListener();
SpannableString hint = new SpannableString(" " + getString(R.string.PassphrasePromptActivity_enter_passphrase));
hint.setSpan(new RelativeSizeSpan(0.9f), 0, hint.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
hint.setSpan(new TypefaceSpan("sans-serif"), 0, hint.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp); fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_primary), PorterDuff.Mode.SRC_IN); fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_primary), PorterDuff.Mode.SRC_IN);

View File

@ -1,5 +1,7 @@
package org.thoughtcrime.securesms package org.thoughtcrime.securesms
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
@ -7,12 +9,14 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.ArrayAdapter
import android.widget.Button import android.widget.Button
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.LinearLayout.VERTICAL import android.widget.LinearLayout.VERTICAL
import android.widget.Space import android.widget.Space
import android.widget.TextView import android.widget.TextView
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.annotation.ColorRes
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.annotation.StyleRes import androidx.annotation.StyleRes
@ -30,6 +34,7 @@ annotation class DialogDsl
@DialogDsl @DialogDsl
class SessionDialogBuilder(val context: Context) { class SessionDialogBuilder(val context: Context) {
private val dp8 = toPx(8, context.resources)
private val dp20 = toPx(20, context.resources) private val dp20 = toPx(20, context.resources)
private val dp40 = toPx(40, context.resources) private val dp40 = toPx(40, context.resources)
private val dp60 = toPx(60, context.resources) private val dp60 = toPx(60, context.resources)
@ -37,13 +42,15 @@ class SessionDialogBuilder(val context: Context) {
private val dialogBuilder: AlertDialog.Builder = AlertDialog.Builder(context) private val dialogBuilder: AlertDialog.Builder = AlertDialog.Builder(context)
private var dialog: AlertDialog? = null private var dialog: AlertDialog? = null
private fun dismiss() = dialog?.dismiss() fun dismiss() = dialog?.dismiss()
private val topView = LinearLayout(context) private val topView = LinearLayout(context)
.apply { setPadding(0, dp20, 0, 0) } .apply { setPadding(0, dp20, 0, 0) }
.apply { orientation = VERTICAL } .apply { orientation = VERTICAL }
.also(dialogBuilder::setCustomTitle) .also(dialogBuilder::setCustomTitle)
private val contentView = LinearLayout(context).apply { orientation = VERTICAL } private val contentView = LinearLayout(context).apply { orientation = VERTICAL }
private val buttonLayout = LinearLayout(context) private val buttonLayout = LinearLayout(context)
private val root = LinearLayout(context).apply { orientation = VERTICAL } private val root = LinearLayout(context).apply { orientation = VERTICAL }
@ -53,24 +60,29 @@ class SessionDialogBuilder(val context: Context) {
addView(buttonLayout) addView(buttonLayout)
} }
fun title(@StringRes id: Int) = title(context.getString(id)) // Main title entry point
fun title(text: CharSequence?) = title(text?.toString())
fun title(text: String?) { fun title(text: String?) {
text(text, R.style.TextAppearance_AppCompat_Title) { setPadding(dp20, 0, dp20, 0) } text(text, R.style.TextAppearance_Session_Dialog_Title) { setPadding(dp20, 0, dp20, 0) }
} }
fun text(@StringRes id: Int, style: Int = 0) = text(context.getString(id), style) // Convenience assessor for title that takes a string resource
fun text(text: CharSequence?, @StyleRes style: Int = 0) { fun title(@StringRes id: Int) = title(context.getString(id))
// Convenience accessor for title that takes a CharSequence
fun title(text: CharSequence?) = title(text?.toString())
fun text(@StringRes id: Int, style: Int? = null) = text(context.getString(id), style)
fun text(text: CharSequence?, @StyleRes style: Int? = null) {
text(text, style) { text(text, style) {
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
.apply { updateMargins(dp40, 0, dp40, 0) } .apply { updateMargins(dp40, 0, dp40, 0) }
} }
} }
private fun text(text: CharSequence?, @StyleRes style: Int, modify: TextView.() -> Unit) { private fun text(text: CharSequence?, @StyleRes style: Int? = null, modify: TextView.() -> Unit) {
text ?: return text ?: return
TextView(context, null, 0, style) TextView(context, null, 0, style ?: R.style.TextAppearance_Session_Dialog_Message)
.apply { .apply {
setText(text) setText(text)
textAlignment = View.TEXT_ALIGNMENT_CENTER textAlignment = View.TEXT_ALIGNMENT_CENTER
@ -78,7 +90,7 @@ class SessionDialogBuilder(val context: Context) {
}.let(topView::addView) }.let(topView::addView)
Space(context).apply { Space(context).apply {
layoutParams = LinearLayout.LayoutParams(0, dp20) layoutParams = LinearLayout.LayoutParams(0, dp8)
}.let(topView::addView) }.let(topView::addView)
} }
@ -95,17 +107,31 @@ class SessionDialogBuilder(val context: Context) {
fun singleChoiceItems( fun singleChoiceItems(
options: Collection<String>, options: Collection<String>,
currentSelected: Int = 0, currentSelected: Int = 0,
dismissOnRadioSelect: Boolean = true,
onSelect: (Int) -> Unit onSelect: (Int) -> Unit
) = singleChoiceItems(options.toTypedArray(), currentSelected, onSelect) ) = singleChoiceItems(
options.toTypedArray(),
currentSelected,
dismissOnRadioSelect,
onSelect
)
fun singleChoiceItems( fun singleChoiceItems(
options: Array<String>, options: Array<String>,
currentSelected: Int = 0, currentSelected: Int = 0,
dismissOnRadioSelect: Boolean = true,
onSelect: (Int) -> Unit onSelect: (Int) -> Unit
): AlertDialog.Builder = dialogBuilder.setSingleChoiceItems( ): AlertDialog.Builder{
options, val adapter = ArrayAdapter<CharSequence>(context, R.layout.view_dialog_single_choice_item, options)
currentSelected
) { dialog, it -> onSelect(it); dialog.dismiss() } return dialogBuilder.setSingleChoiceItems(
adapter,
currentSelected
) { dialog, it ->
onSelect(it)
if(dismissOnRadioSelect) dialog.dismiss()
}
}
fun items( fun items(
options: Array<String>, options: Array<String>,
@ -125,16 +151,21 @@ class SessionDialogBuilder(val context: Context) {
) { listener() } ) { listener() }
fun okButton(listener: (() -> Unit) = {}) = button(android.R.string.ok) { listener() } fun okButton(listener: (() -> Unit) = {}) = button(android.R.string.ok) { listener() }
fun cancelButton(listener: (() -> Unit) = {}) = button(android.R.string.cancel, R.string.AccessibilityId_cancel_button) { listener() }
fun cancelButton(listener: (() -> Unit) = {}) = button(android.R.string.cancel, R.string.AccessibilityId_cancel) { listener() }
fun button( fun button(
@StringRes text: Int, @StringRes text: Int,
@StringRes contentDescriptionRes: Int = text, @StringRes contentDescriptionRes: Int = text,
@StyleRes style: Int = R.style.Widget_Session_Button_Dialog_UnimportantText, @StyleRes style: Int = R.style.Widget_Session_Button_Dialog_UnimportantText,
@ColorRes textColor: Int? = null,
dismiss: Boolean = true, dismiss: Boolean = true,
listener: (() -> Unit) = {} listener: (() -> Unit) = {}
) = Button(context, null, 0, style).apply { ) = Button(context, null, 0, style).apply {
setText(text) setText(text)
textColor?.let{
setTextColor(it)
}
contentDescription = resources.getString(contentDescriptionRes) contentDescription = resources.getString(contentDescriptionRes)
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, dp60, 1f) layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, dp60, 1f)
setOnClickListener { setOnClickListener {
@ -149,22 +180,18 @@ class SessionDialogBuilder(val context: Context) {
fun Context.showSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog = fun Context.showSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
SessionDialogBuilder(this).apply { build() }.show() SessionDialogBuilder(this).apply { build() }.show()
fun Context.showOpenUrlDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
SessionDialogBuilder(this).apply {
title(R.string.urlOpen)
text(R.string.urlOpenBrowser)
build()
}.show()
fun Context.showOpenUrlDialog(url: String): AlertDialog = public fun Context.copyURLToClipboard(url: String) {
showOpenUrlDialog { val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
okButton { openUrl(url) } val clip = ClipData.newPlainText(url, url)
cancelButton() clipboard.setPrimaryClip(clip)
} }
// Method to actually open a given URL via an Intent that will use the default browser
fun Context.openUrl(url: String) = Intent(Intent.ACTION_VIEW, Uri.parse(url)).let(::startActivity) fun Context.openUrl(url: String) = Intent(Intent.ACTION_VIEW, Uri.parse(url)).let(::startActivity)
fun Fragment.showSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog = fun Fragment.showSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
SessionDialogBuilder(requireContext()).apply { build() }.show() SessionDialogBuilder(requireContext()).apply { build() }.show()
fun Fragment.createSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog = fun Fragment.createSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
SessionDialogBuilder(requireContext()).apply { build() }.create() SessionDialogBuilder(requireContext()).apply { build() }.create()

View File

@ -29,12 +29,14 @@ import android.provider.OpenableColumns;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.ImageView; import android.widget.ImageView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import network.loki.messenger.R;
import org.session.libsession.utilities.Address; import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.DistributionTypes; import org.session.libsession.utilities.DistributionTypes;
import org.session.libsession.utilities.ViewUtil; import org.session.libsession.utilities.ViewUtil;
@ -49,12 +51,6 @@ import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import network.loki.messenger.R;
/** /**
* An activity to quickly share content with contacts * An activity to quickly share content with contacts
* *
@ -69,7 +65,6 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
public static final String EXTRA_ADDRESS_MARSHALLED = "address_marshalled"; public static final String EXTRA_ADDRESS_MARSHALLED = "address_marshalled";
public static final String EXTRA_DISTRIBUTION_TYPE = "distribution_type"; public static final String EXTRA_DISTRIBUTION_TYPE = "distribution_type";
private ContactSelectionListFragment contactsFragment; private ContactSelectionListFragment contactsFragment;
private SearchToolbar searchToolbar; private SearchToolbar searchToolbar;
private ImageView searchAction; private ImageView searchAction;
@ -132,7 +127,7 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
} }
private void initializeToolbar() { private void initializeToolbar() {
Toolbar toolbar = findViewById(R.id.toolbar); Toolbar toolbar = findViewById(R.id.search_toolbar);
setSupportActionBar(toolbar); setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar(); ActionBar actionBar = getSupportActionBar();
actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayHomeAsUpEnabled(true);

View File

@ -37,7 +37,7 @@ public class ShortcutLauncherActivity extends AppCompatActivity {
String serializedAddress = getIntent().getStringExtra(KEY_SERIALIZED_ADDRESS); String serializedAddress = getIntent().getStringExtra(KEY_SERIALIZED_ADDRESS);
if (serializedAddress == null) { if (serializedAddress == null) {
Toast.makeText(this, R.string.ShortcutLauncherActivity_invalid_shortcut, Toast.LENGTH_SHORT).show(); Toast.makeText(this, R.string.invalidShortcut, Toast.LENGTH_SHORT).show();
startActivity(new Intent(this, HomeActivity.class)); startActivity(new Intent(this, HomeActivity.class));
finish(); finish();
return; return;

View File

@ -8,10 +8,9 @@ import android.hardware.SensorManager;
import android.media.AudioManager; import android.media.AudioManager;
import android.os.Handler; import android.os.Handler;
import android.os.Message; import android.os.Message;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock; import android.os.PowerManager.WakeLock;
import android.os.PowerManager;
import android.util.Pair; import android.util.Pair;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.OptIn; import androidx.annotation.OptIn;
@ -23,7 +22,8 @@ import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.Player; import androidx.media3.common.Player;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.ExoPlayer;
import java.io.IOException;
import java.lang.ref.WeakReference;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.session.libsession.utilities.ServiceUtil; import org.session.libsession.utilities.ServiceUtil;
import org.session.libsession.utilities.Util; import org.session.libsession.utilities.Util;
@ -32,9 +32,6 @@ import org.session.libsignal.utilities.guava.Optional;
import org.thoughtcrime.securesms.attachments.AttachmentServer; import org.thoughtcrime.securesms.attachments.AttachmentServer;
import org.thoughtcrime.securesms.mms.AudioSlide; import org.thoughtcrime.securesms.mms.AudioSlide;
import java.io.IOException;
import java.lang.ref.WeakReference;
public class AudioSlidePlayer implements SensorEventListener { public class AudioSlidePlayer implements SensorEventListener {
private static final String TAG = AudioSlidePlayer.class.getSimpleName(); private static final String TAG = AudioSlidePlayer.class.getSimpleName();
@ -170,7 +167,6 @@ public class AudioSlidePlayer implements SensorEventListener {
} }
} }
@Override @Override
public void onPlayerError(PlaybackException error) { public void onPlayerError(PlaybackException error) {
Log.w(TAG, "MediaPlayer Error: " + error); Log.w(TAG, "MediaPlayer Error: " + error);
@ -209,9 +205,7 @@ public class AudioSlidePlayer implements SensorEventListener {
this.mediaPlayer.release(); this.mediaPlayer.release();
} }
if (this.audioAttachmentServer != null) { if (this.audioAttachmentServer != null) { this.audioAttachmentServer.stop(); }
this.audioAttachmentServer.stop();
}
sensorManager.unregisterListener(AudioSlidePlayer.this); sensorManager.unregisterListener(AudioSlidePlayer.this);
@ -220,9 +214,7 @@ public class AudioSlidePlayer implements SensorEventListener {
} }
public synchronized static void stopAll() { public synchronized static void stopAll() {
if (playing.isPresent()) { if (playing.isPresent()) { playing.get().stop(); }
playing.get().stop();
}
} }
public synchronized boolean isReady() { public synchronized boolean isReady() {
@ -364,9 +356,8 @@ public class AudioSlidePlayer implements SensorEventListener {
} }
@Override @Override
public void onAccuracyChanged(Sensor sensor, int accuracy) { public void onAccuracyChanged(Sensor sensor, int accuracy) { /* Do nothing */ }
}
public interface Listener { public interface Listener {
void onPlayerStart(@NonNull AudioSlidePlayer player); void onPlayerStart(@NonNull AudioSlidePlayer player);

View File

@ -32,7 +32,7 @@ class AvatarSelection(
private val bgColor by lazy { activity.getColorFromAttr(android.R.attr.colorPrimary) } private val bgColor by lazy { activity.getColorFromAttr(android.R.attr.colorPrimary) }
private val txtColor by lazy { activity.getColorFromAttr(android.R.attr.textColorPrimary) } private val txtColor by lazy { activity.getColorFromAttr(android.R.attr.textColorPrimary) }
private val imageScrim by lazy { ContextCompat.getColor(activity, R.color.avatar_background) } private val imageScrim by lazy { ContextCompat.getColor(activity, R.color.avatar_background) }
private val activityTitle by lazy { activity.getString(R.string.CropImageActivity_profile_avatar) } private val activityTitle by lazy { activity.getString(R.string.image) }
/** /**
* Returns result on [.REQUEST_CODE_CROP_IMAGE] * Returns result on [.REQUEST_CODE_CROP_IMAGE]
@ -120,7 +120,7 @@ class AvatarSelection(
val chooserIntent = Intent.createChooser( val chooserIntent = Intent.createChooser(
galleryIntent, galleryIntent,
context.getString(R.string.CreateProfileActivity_profile_photo) context.getString(R.string.image)
) )
if (!extraIntents.isEmpty()) { if (!extraIntents.isEmpty()) {

View File

@ -13,11 +13,13 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewOutlineProvider import android.view.ViewOutlineProvider
import android.view.WindowManager import android.view.WindowManager
import android.widget.TextView
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -27,6 +29,7 @@ import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityWebrtcBinding import network.loki.messenger.databinding.ActivityWebrtcBinding
import org.apache.commons.lang3.time.DurationFormatUtils import org.apache.commons.lang3.time.DurationFormatUtils
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.truncateIdForDisplay import org.session.libsession.utilities.truncateIdForDisplay
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
@ -202,6 +205,10 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
update() update()
} }
} }
// Substitute "Session" into the "{app_name} Call" text
val sessionCallTV = findViewById<TextView>(R.id.sessionCallText)
sessionCallTV?.text = Phrase.from(this, R.string.callsSessionCall).put(APP_NAME_KEY, getString(R.string.app_name)).format()
} }
/** /**

View File

@ -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);
}
}

View File

@ -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);
}
}
}
}

View File

@ -105,7 +105,7 @@ public class DocumentView extends FrameLayout {
this.documentSlide = documentSlide; this.documentSlide = documentSlide;
this.fileName.setText(documentSlide.getFileName().or(getContext().getString(R.string.DocumentView_unknown_file))); this.fileName.setText(documentSlide.getFileName().or(getContext().getString(R.string.attachmentsErrorNotSupported)));
this.fileSize.setText(Util.getPrettyFileSize(documentSlide.getFileSize())); this.fileSize.setText(Util.getPrettyFileSize(documentSlide.getFileSize()));
this.document.setText(getFileType(documentSlide.getFileName())); this.document.setText(getFileType(documentSlide.getFileName()));
this.setOnClickListener(new OpenClickedListener(documentSlide)); this.setOnClickListener(new OpenClickedListener(documentSlide));

View File

@ -54,7 +54,7 @@ public class FromTextView extends EmojiTextView {
if (recipient.isLocalNumber()) { if (recipient.isLocalNumber()) {
builder.append(getContext().getString(R.string.note_to_self)); builder.append(getContext().getString(R.string.noteToSelf));
} else if (recipient.getName() == null && !TextUtils.isEmpty(recipient.getProfileName())) { } else if (recipient.getName() == null && !TextUtils.isEmpty(recipient.getProfileName())) {
SpannableString profileName = new SpannableString(" (~" + recipient.getProfileName() + ") "); SpannableString profileName = new SpannableString(" (~" + recipient.getProfileName() + ") ");
profileName.setSpan(new CenterAlignedRelativeSizeSpan(0.75f), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); profileName.setSpan(new CenterAlignedRelativeSizeSpan(0.75f), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

View File

@ -44,10 +44,9 @@ public class SearchToolbar extends LinearLayout {
inflate(getContext(), R.layout.search_toolbar, this); inflate(getContext(), R.layout.search_toolbar, this);
setOrientation(VERTICAL); setOrientation(VERTICAL);
Toolbar toolbar = findViewById(R.id.toolbar); Toolbar toolbar = findViewById(R.id.search_toolbar);
toolbar.setNavigationIcon( toolbar.setNavigationIcon(getContext().getResources().getDrawable(R.drawable.ic_baseline_clear_24));
getContext().getResources().getDrawable(R.drawable.ic_baseline_clear_24));
toolbar.inflateMenu(R.menu.conversation_list_search); toolbar.inflateMenu(R.menu.conversation_list_search);
this.searchItem = toolbar.getMenu().findItem(R.id.action_filter_search); this.searchItem = toolbar.getMenu().findItem(R.id.action_filter_search);
@ -56,8 +55,8 @@ public class SearchToolbar extends LinearLayout {
searchView.setSubmitButtonEnabled(false); searchView.setSubmitButtonEnabled(false);
if (searchText != null) searchText.setHint(R.string.SearchToolbar_search); if (searchText != null) searchText.setHint(R.string.search);
else searchView.setQueryHint(getResources().getString(R.string.SearchToolbar_search)); else searchView.setQueryHint(getResources().getString(R.string.search));
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override @Override

View File

@ -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();
}
}
}

View File

@ -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"
}
}

View File

@ -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));
}
}
}

View File

@ -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

View File

@ -73,7 +73,7 @@ public class ContactAccessor {
} }
} }
// if (context.getString(R.string.note_to_self).toLowerCase().contains(constraint.toLowerCase()) && // if (context.getString(R.string.noteToSelf).toLowerCase().contains(constraint.toLowerCase()) &&
// !numberList.contains(TextSecurePreferences.getLocalNumber(context))) // !numberList.contains(TextSecurePreferences.getLocalNumber(context)))
// { // {
// numberList.add(TextSecurePreferences.getLocalNumber(context)); // numberList.add(TextSecurePreferences.getLocalNumber(context));

View File

@ -36,7 +36,7 @@ class ContactSelectionListLoader(context: Context, val mode: Int, val filter: St
list.addAll(getClosedGroups(contacts)) list.addAll(getClosedGroups(contacts))
} }
if (isFlagSet(DisplayMode.FLAG_OPEN_GROUPS)) { if (isFlagSet(DisplayMode.FLAG_OPEN_GROUPS)) {
list.addAll(getOpenGroups(contacts)) list.addAll(getCommunities(contacts))
} }
if (isFlagSet(DisplayMode.FLAG_CONTACTS)) { if (isFlagSet(DisplayMode.FLAG_CONTACTS)) {
list.addAll(getContacts(contacts)) list.addAll(getContacts(contacts))
@ -45,19 +45,19 @@ class ContactSelectionListLoader(context: Context, val mode: Int, val filter: St
} }
private fun getContacts(contacts: List<Recipient>): List<ContactSelectionListItem> { private fun getContacts(contacts: List<Recipient>): List<ContactSelectionListItem> {
return getItems(contacts, context.getString(R.string.fragment_contact_selection_contacts_title)) { return getItems(contacts, context.getString(R.string.contactContacts)) {
!it.isGroupRecipient !it.isGroupRecipient
} }
} }
private fun getClosedGroups(contacts: List<Recipient>): List<ContactSelectionListItem> { private fun getClosedGroups(contacts: List<Recipient>): List<ContactSelectionListItem> {
return getItems(contacts, context.getString(R.string.fragment_contact_selection_closed_groups_title)) { return getItems(contacts, context.getString(R.string.conversationsGroups)) {
it.address.isClosedGroup it.address.isClosedGroup
} }
} }
private fun getOpenGroups(contacts: List<Recipient>): List<ContactSelectionListItem> { private fun getCommunities(contacts: List<Recipient>): List<ContactSelectionListItem> {
return getItems(contacts, context.getString(R.string.fragment_contact_selection_open_groups_title)) { return getItems(contacts, context.getString(R.string.conversationsCommunities)) {
it.address.isCommunity it.address.isCommunity
} }
} }

View File

@ -18,10 +18,10 @@ public final class ContactUtil {
String contactName = ContactUtil.getDisplayName(contact); String contactName = ContactUtil.getDisplayName(contact);
if (!TextUtils.isEmpty(contactName)) { if (!TextUtils.isEmpty(contactName)) {
return context.getString(R.string.MessageNotifier_contact_message, EmojiStrings.BUST_IN_SILHOUETTE, contactName); return EmojiStrings.BUST_IN_SILHOUETTE + " " + contactName;
} }
return SpanUtil.italic(context.getString(R.string.MessageNotifier_unknown_contact_message)); return SpanUtil.italic(context.getString(R.string.unknown));
} }
private static @NonNull String getDisplayName(@Nullable Contact contact) { private static @NonNull String getDisplayName(@Nullable Contact contact) {

View File

@ -159,7 +159,7 @@ public class ContactsCursorLoader extends CursorLoader {
private Cursor getGroupsHeaderCursor() { private Cursor getGroupsHeaderCursor() {
MatrixCursor groupHeader = new MatrixCursor(CONTACT_PROJECTION, 1); MatrixCursor groupHeader = new MatrixCursor(CONTACT_PROJECTION, 1);
groupHeader.addRow(new Object[]{ getContext().getString(R.string.ContactsCursorLoader_groups), groupHeader.addRow(new Object[]{ getContext().getString(R.string.conversationsGroups),
"", "",
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE,
"", "",
@ -221,16 +221,6 @@ public class ContactsCursorLoader extends CursorLoader {
return groupContacts; return groupContacts;
} }
private Cursor getNewNumberCursor() {
MatrixCursor newNumberCursor = new MatrixCursor(CONTACT_PROJECTION, 1);
newNumberCursor.addRow(new Object[] { getContext().getString(R.string.contact_selection_list__unknown_contact),
filter,
ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM,
"\u21e2",
NEW_TYPE });
return newNumberCursor;
}
private static boolean isCursorListEmpty(List<Cursor> list) { private static boolean isCursorListEmpty(List<Cursor> list) {
int sum = 0; int sum = 0;
for (Cursor cursor : list) { for (Cursor cursor : list) {

View File

@ -35,7 +35,7 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana
super.onCreate(savedInstanceState, isReady) super.onCreate(savedInstanceState, isReady)
binding = ActivitySelectContactsBinding.inflate(layoutInflater) binding = ActivitySelectContactsBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
supportActionBar!!.title = resources.getString(R.string.activity_select_contacts_title) supportActionBar!!.title = resources.getString(R.string.membersInvite)
usersToExclude = intent.getStringArrayExtra(usersToExcludeKey)?.toSet() ?: setOf() usersToExclude = intent.getStringArrayExtra(usersToExcludeKey)?.toSet() ?: setOf()
val emptyStateText = intent.getStringExtra(emptyStateTextKey) val emptyStateText = intent.getStringExtra(emptyStateTextKey)

View File

@ -14,7 +14,6 @@ import com.bumptech.glide.RequestManager
class UserView : LinearLayout { class UserView : LinearLayout {
private lateinit var binding: ViewUserBinding private lateinit var binding: ViewUserBinding
var openGroupThreadID: Long = -1 // FIXME: This is a bit ugly
enum class ActionIndicator { enum class ActionIndicator {
None, None,
@ -47,11 +46,13 @@ class UserView : LinearLayout {
// region Updating // region Updating
fun bind(user: Recipient, glide: RequestManager, actionIndicator: ActionIndicator, isSelected: Boolean = false) { fun bind(user: Recipient, glide: RequestManager, actionIndicator: ActionIndicator, isSelected: Boolean = false) {
val isLocalUser = user.isLocalNumber val isLocalUser = user.isLocalNumber
fun getUserDisplayName(publicKey: String): String { fun getUserDisplayName(publicKey: String): String {
if (isLocalUser) return context.getString(R.string.MessageRecord_you) if (isLocalUser) return context.getString(R.string.you)
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithAccountID(publicKey) val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithAccountID(publicKey)
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
} }
val address = user.address.serialize() val address = user.address.serialize()
binding.profilePictureView.update(user) binding.profilePictureView.update(user)
binding.actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24) binding.actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24)
@ -84,8 +85,6 @@ class UserView : LinearLayout {
} }
} }
fun unbind() { fun unbind() { binding.profilePictureView.recycle() }
binding.profilePictureView.recycle()
}
// endregion // endregion
} }

View File

@ -11,21 +11,26 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewConversationActionBarBinding import network.loki.messenger.databinding.ViewConversationActionBarBinding
import network.loki.messenger.databinding.ViewConversationSettingBinding import network.loki.messenger.databinding.ViewConversationSettingBinding
import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.ExpiryMode.AfterRead
import org.session.libsession.LocalisedTimeUtil
import org.session.libsession.messaging.messages.ExpirationConfiguration import org.session.libsession.messaging.messages.ExpirationConfiguration
import org.session.libsession.messaging.open_groups.OpenGroup import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.utilities.ExpirationUtil import org.session.libsession.utilities.ExpirationUtil
import org.session.libsession.utilities.StringSubstitutionConstants.DISAPPEARING_MESSAGES_TYPE_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.TIME_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.TIME_LARGE_KEY
import org.session.libsession.utilities.modifyLayoutParams import org.session.libsession.utilities.modifyLayoutParams
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.database.LokiAPIDatabase import org.thoughtcrime.securesms.database.LokiAPIDatabase
import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.ui.getSubbedString
import java.util.Locale
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class ConversationActionBarView @JvmOverloads constructor( class ConversationActionBarView @JvmOverloads constructor(
@ -82,7 +87,7 @@ class ConversationActionBarView @JvmOverloads constructor(
fun update(recipient: Recipient, openGroup: OpenGroup? = null, config: ExpirationConfiguration? = null) { fun update(recipient: Recipient, openGroup: OpenGroup? = null, config: ExpirationConfiguration? = null) {
binding.profilePictureView.update(recipient) binding.profilePictureView.update(recipient)
binding.conversationTitleView.text = recipient.takeUnless { it.isLocalNumber }?.toShortString() ?: context.getString(R.string.note_to_self) binding.conversationTitleView.text = recipient.takeUnless { it.isLocalNumber }?.toShortString() ?: context.getString(R.string.noteToSelf)
updateSubtitle(recipient, openGroup, config) updateSubtitle(recipient, openGroup, config)
binding.conversationTitleContainer.modifyLayoutParams<MarginLayoutParams> { binding.conversationTitleContainer.modifyLayoutParams<MarginLayoutParams> {
@ -92,37 +97,56 @@ class ConversationActionBarView @JvmOverloads constructor(
fun updateSubtitle(recipient: Recipient, openGroup: OpenGroup? = null, config: ExpirationConfiguration? = null) { fun updateSubtitle(recipient: Recipient, openGroup: OpenGroup? = null, config: ExpirationConfiguration? = null) {
val settings = mutableListOf<ConversationSetting>() val settings = mutableListOf<ConversationSetting>()
// Specify the disappearing messages subtitle if we should
if (config?.isEnabled == true) { if (config?.isEnabled == true) {
val prefix = when (config.expiryMode) { // Get the type of disappearing message and the abbreviated duration..
is ExpiryMode.AfterRead -> R.string.expiration_type_disappear_after_read val dmTypeString = when (config.expiryMode) {
else -> R.string.expiration_type_disappear_after_send is AfterRead -> context.getString(R.string.read)
}.let(context::getString) else -> context.getString(R.string.send)
}
val durationAbbreviated = ExpirationUtil.getExpirationAbbreviatedDisplayValue(config.expiryMode.expirySeconds)
// ..then substitute into the string..
val subtitleTxt = context.getSubbedString(R.string.disappearingMessagesDisappear,
DISAPPEARING_MESSAGES_TYPE_KEY to dmTypeString,
TIME_KEY to durationAbbreviated
)
// .. and apply to the subtitle.
settings += ConversationSetting( settings += ConversationSetting(
"$prefix - ${ExpirationUtil.getExpirationAbbreviatedDisplayValue(context, config.expiryMode.expirySeconds)}", subtitleTxt,
ConversationSettingType.EXPIRATION, ConversationSettingType.EXPIRATION,
R.drawable.ic_timer, R.drawable.ic_timer,
resources.getString(R.string.AccessibilityId_disappearing_messages_type_and_time) resources.getString(R.string.AccessibilityId_disappearingMessagesDisappear)
) )
} }
if (recipient.isMuted) { if (recipient.isMuted) {
settings += ConversationSetting( settings += ConversationSetting(
recipient.mutedUntil.takeUnless { it == Long.MAX_VALUE } recipient.mutedUntil.takeUnless { it == Long.MAX_VALUE }
?.let { context.getString(R.string.ConversationActivity_muted_until_date, DateUtils.getFormattedDateTime(it, "EEE, MMM d, yyyy HH:mm", Locale.getDefault())) } ?.let {
?: context.getString(R.string.ConversationActivity_muted_forever), val mutedDuration = (it - System.currentTimeMillis()).milliseconds
val durationString = LocalisedTimeUtil.getDurationWithSingleLargestTimeUnit(context, mutedDuration)
context.getSubbedString(R.string.notificationsMuteFor, TIME_LARGE_KEY to durationString)
}
?: context.getString(R.string.notificationsMuted),
ConversationSettingType.NOTIFICATION, ConversationSettingType.NOTIFICATION,
R.drawable.ic_outline_notifications_off_24 R.drawable.ic_outline_notifications_off_24
) )
} }
if (recipient.isGroupRecipient) { if (recipient.isGroupRecipient) {
val title = if (recipient.isCommunityRecipient) { val title = if (recipient.isCommunityRecipient) {
val userCount = openGroup?.let { lokiApiDb.getUserCount(it.room, it.server) } ?: 0 val userCount = openGroup?.let { lokiApiDb.getUserCount(it.room, it.server) } ?: 0
context.getString(R.string.ConversationActivity_active_member_count, userCount) resources.getQuantityString(R.plurals.membersActive, userCount, userCount)
} else { } else {
val userCount = groupDb.getGroupMemberAddresses(recipient.address.toGroupString(), true).size val userCount = groupDb.getGroupMemberAddresses(recipient.address.toGroupString(), true).size
context.getString(R.string.ConversationActivity_member_count, userCount) resources.getQuantityString(R.plurals.members, userCount, userCount)
} }
settings += ConversationSetting(title, ConversationSettingType.MEMBER_COUNT) settings += ConversationSetting(title, ConversationSettingType.MEMBER_COUNT)
} }
settingsAdapter.submitList(settings) settingsAdapter.submitList(settings)
binding.settingsTabLayout.isVisible = settings.size > 1 binding.settingsTabLayout.isVisible = settings.size > 1
} }

View File

@ -12,10 +12,14 @@ import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.ExpirationUtil import org.session.libsession.utilities.ExpirationUtil
import org.session.libsession.utilities.SSKEnvironment.MessageExpirationManagerProtocol import org.session.libsession.utilities.SSKEnvironment.MessageExpirationManagerProtocol
import org.session.libsession.utilities.StringSubstitutionConstants.DISAPPEARING_MESSAGES_TYPE_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.TIME_KEY
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.getExpirationTypeDisplayValue import org.session.libsession.utilities.getExpirationTypeDisplayValue
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.ui.getSubbedCharSequence
import org.thoughtcrime.securesms.ui.getSubbedString
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import javax.inject.Inject import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@ -43,22 +47,18 @@ class DisappearingMessages @Inject constructor(
} }
fun showFollowSettingDialog(context: Context, message: MessageRecord) = context.showSessionDialog { fun showFollowSettingDialog(context: Context, message: MessageRecord) = context.showSessionDialog {
title(R.string.dialog_disappearing_messages_follow_setting_title) title(R.string.disappearingMessagesFollowSetting)
text(if (message.expiresIn == 0L) { text(if (message.expiresIn == 0L) {
context.getString(R.string.dialog_disappearing_messages_follow_setting_off_body) context.getText(R.string.disappearingMessagesFollowSettingOff)
} else { } else {
context.getString( context.getSubbedCharSequence(R.string.disappearingMessagesFollowSettingOn,
R.string.dialog_disappearing_messages_follow_setting_on_body, TIME_KEY to ExpirationUtil.getExpirationDisplayValue(context, message.expiresIn.milliseconds),
ExpirationUtil.getExpirationDisplayValue( DISAPPEARING_MESSAGES_TYPE_KEY to context.getExpirationTypeDisplayValue(message.isNotDisappearAfterRead))
context,
message.expiresIn.milliseconds
),
context.getExpirationTypeDisplayValue(message.isNotDisappearAfterRead)
)
}) })
dangerButton( dangerButton(
text = if (message.expiresIn == 0L) R.string.dialog_disappearing_messages_follow_setting_confirm else R.string.dialog_disappearing_messages_follow_setting_set, text = if (message.expiresIn == 0L) R.string.confirm else R.string.set,
contentDescription = if (message.expiresIn == 0L) R.string.AccessibilityId_confirm else R.string.AccessibilityId_set_button contentDescription = if (message.expiresIn == 0L) R.string.AccessibilityId_confirm else R.string.AccessibilityId_setButton
) { ) {
set(message.threadId, message.recipient.address, message.expiryMode, message.recipient.isClosedGroupRecipient) set(message.threadId, message.recipient.address, message.expiryMode, message.recipient.isClosedGroupRecipient)
} }

View File

@ -52,7 +52,7 @@ class DisappearingMessagesActivity: PassphraseRequiredActionBarActivity() {
viewModel.event.collect { viewModel.event.collect {
when (it) { when (it) {
Event.SUCCESS -> finish() Event.SUCCESS -> finish()
Event.FAIL -> showToast(getString(R.string.DisappearingMessagesActivity_settings_not_updated)) Event.FAIL -> showToast(getString(R.string.communityErrorDescription))
} }
} }
} }
@ -72,9 +72,9 @@ class DisappearingMessagesActivity: PassphraseRequiredActionBarActivity() {
} }
private fun setUpToolbar() { private fun setUpToolbar() {
setSupportActionBar(binding.toolbar) setSupportActionBar(binding.searchToolbar)
supportActionBar?.apply { supportActionBar?.apply {
title = getString(R.string.activity_disappearing_messages_title) title = getString(R.string.disappearingMessages)
setDisplayHomeAsUpEnabled(true) setDisplayHomeAsUpEnabled(true)
setHomeButtonEnabled(true) setHomeButtonEnabled(true)
} }

View File

@ -24,8 +24,8 @@ data class State(
val showDebugOptions: Boolean = false val showDebugOptions: Boolean = false
) { ) {
val subtitle get() = when { val subtitle get() = when {
isGroup || isNoteToSelf -> GetString(R.string.activity_disappearing_messages_subtitle_sent) isGroup || isNoteToSelf -> GetString(R.string.disappearingMessagesDisappearAfterSendDescription)
else -> GetString(R.string.activity_disappearing_messages_subtitle) else -> GetString(R.string.disappearingMessagesDescription1)
} }
val typeOptionsHidden get() = isNoteToSelf || (isGroup && isNewConfigEnabled) val typeOptionsHidden get() = isNoteToSelf || (isGroup && isNewConfigEnabled)
@ -51,25 +51,25 @@ enum class ExpiryType(
) { ) {
NONE( NONE(
{ ExpiryMode.NONE }, { ExpiryMode.NONE },
R.string.expiration_off, R.string.off,
contentDescription = R.string.AccessibilityId_disable_disappearing_messages, contentDescription = R.string.AccessibilityId_disappearingMessagesOff,
), ),
LEGACY( LEGACY(
ExpiryMode::Legacy, ExpiryMode::Legacy,
R.string.expiration_type_disappear_legacy, R.string.expiration_type_disappear_legacy,
contentDescription = R.string.expiration_type_disappear_legacy_description contentDescription = R.string.AccessibilityId_disappearingMessagesLegacy
), ),
AFTER_READ( AFTER_READ(
ExpiryMode::AfterRead, ExpiryMode::AfterRead,
R.string.expiration_type_disappear_after_read, R.string.disappearingMessagesDisappearAfterRead,
R.string.expiration_type_disappear_after_read_description, R.string.disappearingMessagesDisappearAfterReadDescription,
R.string.AccessibilityId_disappear_after_read_option R.string.AccessibilityId_disappearingMessagesDisappearAfterRead
), ),
AFTER_SEND( AFTER_SEND(
ExpiryMode::AfterSend, ExpiryMode::AfterSend,
R.string.expiration_type_disappear_after_send, R.string.disappearingMessagesDisappearAfterSend,
R.string.expiration_type_disappear_after_send_description, R.string.disappearingMessagesDisappearAfterSendDescription,
R.string.AccessibilityId_disappear_after_send_option R.string.AccessibilityId_disappearingMessagesDisappearAfterSent
); );
fun mode(seconds: Long) = if (seconds != 0L) createMode(seconds) else ExpiryMode.NONE fun mode(seconds: Long) = if (seconds != 0L) createMode(seconds) else ExpiryMode.NONE

View File

@ -13,8 +13,8 @@ import kotlin.time.Duration.Companion.seconds
fun State.toUiState() = UiState( fun State.toUiState() = UiState(
cards = listOfNotNull( cards = listOfNotNull(
typeOptions()?.let { ExpiryOptionsCardData(GetString(R.string.activity_disappearing_messages_delete_type), it) }, typeOptions()?.let { ExpiryOptionsCardData(GetString(R.string.disappearingMessagesDeleteType), it) },
timeOptions()?.let { ExpiryOptionsCardData(GetString(R.string.activity_disappearing_messages_timer), it) } timeOptions()?.let { ExpiryOptionsCardData(GetString(R.string.disappearingMessagesTimer), it) }
), ),
showGroupFooter = isGroup && isNewConfigEnabled, showGroupFooter = isGroup && isNewConfigEnabled,
showSetButton = isSelfAdmin showSetButton = isSelfAdmin
@ -66,11 +66,14 @@ private fun State.typeOption(
) )
private fun debugTimes(isDebug: Boolean) = if (isDebug) listOf(10.seconds, 30.seconds, 1.minutes) else emptyList() private fun debugTimes(isDebug: Boolean) = if (isDebug) listOf(10.seconds, 30.seconds, 1.minutes) else emptyList()
private fun debugModes(isDebug: Boolean, type: ExpiryType) = private fun debugModes(isDebug: Boolean, type: ExpiryType) =
debugTimes(isDebug).map { type.mode(it.inWholeSeconds) } debugTimes(isDebug).map { type.mode(it.inWholeSeconds) }
private fun State.debugOptions(): List<ExpiryRadioOption> = private fun State.debugOptions(): List<ExpiryRadioOption> =
debugModes(showDebugOptions, nextType).map { timeOption(it, subtitle = GetString("for testing purposes")) } debugModes(showDebugOptions, nextType).map { timeOption(it, subtitle = GetString("for testing purposes")) }
// Standard list of available disappearing message times
private val afterSendTimes = listOf(12.hours, 1.days, 7.days, 14.days) private val afterSendTimes = listOf(12.hours, 1.days, 7.days, 14.days)
private val afterReadTimes = buildList { private val afterReadTimes = buildList {

View File

@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.conversation.disappearingmessages.ui package org.thoughtcrime.securesms.conversation.disappearingmessages.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@ -16,21 +15,20 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.ExpiryMode
import org.thoughtcrime.securesms.ui.Callbacks import org.thoughtcrime.securesms.ui.Callbacks
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
import org.thoughtcrime.securesms.ui.NoOpCallbacks import org.thoughtcrime.securesms.ui.NoOpCallbacks
import org.thoughtcrime.securesms.ui.OptionsCard import org.thoughtcrime.securesms.ui.OptionsCard
import org.thoughtcrime.securesms.ui.RadioOption import org.thoughtcrime.securesms.ui.RadioOption
import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.components.SlimOutlineButton import org.thoughtcrime.securesms.ui.components.SlimOutlineButton
import org.thoughtcrime.securesms.ui.contentDescription import org.thoughtcrime.securesms.ui.contentDescription
import org.thoughtcrime.securesms.ui.fadingEdges import org.thoughtcrime.securesms.ui.fadingEdges
import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
import org.thoughtcrime.securesms.ui.theme.LocalType import org.thoughtcrime.securesms.ui.theme.LocalType
typealias ExpiryCallbacks = Callbacks<ExpiryMode> typealias ExpiryCallbacks = Callbacks<ExpiryMode>
typealias ExpiryRadioOption = RadioOption<ExpiryMode> typealias ExpiryRadioOption = RadioOption<ExpiryMode>
@Composable @Composable
@ -59,7 +57,9 @@ fun DisappearingMessages(
} }
if (state.showGroupFooter) Text( if (state.showGroupFooter) Text(
text = stringResource(R.string.activity_disappearing_messages_group_footer), text = stringResource(R.string.disappearingMessagesDescription) +
"\n" +
stringResource(R.string.disappearingMessagesOnlyAdmins),
style = LocalType.current.extraSmall, style = LocalType.current.extraSmall,
fontWeight = FontWeight(400), fontWeight = FontWeight(400),
color = LocalColors.current.textSecondary, color = LocalColors.current.textSecondary,
@ -72,9 +72,9 @@ fun DisappearingMessages(
} }
if (state.showSetButton) SlimOutlineButton( if (state.showSetButton) SlimOutlineButton(
stringResource(R.string.disappearing_messages_set_button_title), stringResource(R.string.set),
modifier = Modifier modifier = Modifier
.contentDescription(R.string.AccessibilityId_set_button) .contentDescription(R.string.AccessibilityId_setButton)
.align(Alignment.CenterHorizontally) .align(Alignment.CenterHorizontally)
.padding(bottom = LocalDimensions.current.spacing), .padding(bottom = LocalDimensions.current.spacing),
onClick = callbacks::onSetClick onClick = callbacks::onSetClick

View File

@ -15,6 +15,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@ -41,12 +42,14 @@ internal fun StartConversationScreen(
accountId: String, accountId: String,
delegate: StartConversationDelegate delegate: StartConversationDelegate
) { ) {
val context = LocalContext.current
Column(modifier = Modifier.background( Column(modifier = Modifier.background(
LocalColors.current.backgroundSecondary, LocalColors.current.backgroundSecondary,
shape = MaterialTheme.shapes.small shape = MaterialTheme.shapes.small
)) { )) {
BasicAppBar( BasicAppBar(
title = stringResource(R.string.dialog_start_conversation_title), title = stringResource(R.string.conversationsStart),
backgroundColor = Color.Transparent, // transparent to show the rounded shape of the container backgroundColor = Color.Transparent, // transparent to show the rounded shape of the container
actions = { AppBarCloseIcon(onClose = delegate::onDialogClosePressed) } actions = { AppBarCloseIcon(onClose = delegate::onDialogClosePressed) }
) )
@ -57,30 +60,31 @@ internal fun StartConversationScreen(
Column( Column(
modifier = Modifier.verticalScroll(rememberScrollState()) modifier = Modifier.verticalScroll(rememberScrollState())
) { ) {
val newMessageTitleTxt:String = context.resources.getQuantityString(R.plurals.messageNew, 1, 1)
ItemButton( ItemButton(
textId = R.string.messageNew, text = newMessageTitleTxt,
icon = R.drawable.ic_message, icon = R.drawable.ic_message,
modifier = Modifier.contentDescription(R.string.AccessibilityId_new_direct_message), modifier = Modifier.contentDescription(R.string.AccessibilityId_messageNew),
onClick = delegate::onNewMessageSelected) onClick = delegate::onNewMessageSelected)
Divider(startIndent = LocalDimensions.current.dividerIndent) Divider(startIndent = LocalDimensions.current.dividerIndent)
ItemButton( ItemButton(
textId = R.string.activity_create_group_title, textId = R.string.groupCreate,
icon = R.drawable.ic_group, icon = R.drawable.ic_group,
modifier = Modifier.contentDescription(R.string.AccessibilityId_create_group), modifier = Modifier.contentDescription(R.string.AccessibilityId_groupCreate),
onClick = delegate::onCreateGroupSelected onClick = delegate::onCreateGroupSelected
) )
Divider(startIndent = LocalDimensions.current.dividerIndent) Divider(startIndent = LocalDimensions.current.dividerIndent)
ItemButton( ItemButton(
textId = R.string.dialog_join_community_title, textId = R.string.communityJoin,
icon = R.drawable.ic_globe, icon = R.drawable.ic_globe,
modifier = Modifier.contentDescription(R.string.AccessibilityId_join_community), modifier = Modifier.contentDescription(R.string.AccessibilityId_communityJoin),
onClick = delegate::onJoinCommunitySelected onClick = delegate::onJoinCommunitySelected
) )
Divider(startIndent = LocalDimensions.current.dividerIndent) Divider(startIndent = LocalDimensions.current.dividerIndent)
ItemButton( ItemButton(
textId = R.string.activity_settings_invite_button_title, textId = R.string.sessionInviteAFriend,
icon = R.drawable.ic_invite_friend, icon = R.drawable.ic_invite_friend,
Modifier.contentDescription(R.string.AccessibilityId_invite_friend_button), Modifier.contentDescription(R.string.AccessibilityId_sessionInviteAFriendButton),
onClick = delegate::onInviteFriend onClick = delegate::onInviteFriend
) )
Column( Column(
@ -99,7 +103,7 @@ internal fun StartConversationScreen(
Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing)) Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing))
QrImage( QrImage(
string = accountId, string = accountId,
Modifier.contentDescription(R.string.AccessibilityId_qr_code), Modifier.contentDescription(R.string.AccessibilityId_qrCode),
icon = R.drawable.session icon = R.drawable.session
) )
} }

View File

@ -14,10 +14,13 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import com.squareup.phrase.Phrase
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
import org.thoughtcrime.securesms.ui.components.AppBarCloseIcon import org.thoughtcrime.securesms.ui.components.AppBarCloseIcon
import org.thoughtcrime.securesms.ui.components.BackAppBar import org.thoughtcrime.securesms.ui.components.BackAppBar
import org.thoughtcrime.securesms.ui.components.SlimOutlineButton import org.thoughtcrime.securesms.ui.components.SlimOutlineButton
@ -43,7 +46,7 @@ internal fun InviteFriend(
shape = MaterialTheme.shapes.small shape = MaterialTheme.shapes.small
)) { )) {
BackAppBar( BackAppBar(
title = stringResource(R.string.invite_a_friend), title = stringResource(R.string.sessionInviteAFriend),
backgroundColor = Color.Transparent, // transparent to show the rounded shape of the container backgroundColor = Color.Transparent, // transparent to show the rounded shape of the container
onBack = onBack, onBack = onBack,
actions = { AppBarCloseIcon(onClose = onClose) } actions = { AppBarCloseIcon(onClose = onClose) }
@ -55,7 +58,7 @@ internal fun InviteFriend(
Text( Text(
accountId, accountId,
modifier = Modifier modifier = Modifier
.contentDescription(R.string.AccessibilityId_account_id) .contentDescription(R.string.AccessibilityId_shareAccountId)
.fillMaxWidth() .fillMaxWidth()
.border() .border()
.padding(LocalDimensions.current.spacing), .padding(LocalDimensions.current.spacing),
@ -66,7 +69,10 @@ internal fun InviteFriend(
Spacer(modifier = Modifier.height(LocalDimensions.current.xsSpacing)) Spacer(modifier = Modifier.height(LocalDimensions.current.xsSpacing))
Text( Text(
stringResource(R.string.invite_your_friend_to_chat_with_you_on_session_by_sharing_your_account_id_with_them), stringResource(R.string.shareAccountIdDescription).let { txt ->
val c = LocalContext.current
Phrase.from(txt).put(APP_NAME_KEY, c.getString(R.string.app_name)).format().toString()
},
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
style = LocalType.current.small, style = LocalType.current.small,
color = LocalColors.current.textSecondary, color = LocalColors.current.textSecondary,

View File

@ -32,6 +32,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
@ -61,7 +62,7 @@ import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider
import org.thoughtcrime.securesms.ui.theme.ThemeColors import org.thoughtcrime.securesms.ui.theme.ThemeColors
import kotlin.math.max import kotlin.math.max
private val TITLES = listOf(R.string.enter_account_id, R.string.qrScan) private val TITLES = listOf(R.string.accountIdEnter, R.string.qrScan)
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable @Composable
@ -79,8 +80,12 @@ internal fun NewMessage(
LocalColors.current.backgroundSecondary, LocalColors.current.backgroundSecondary,
shape = MaterialTheme.shapes.small shape = MaterialTheme.shapes.small
)) { )) {
// `messageNew` is now a plurals string so get the singular version
val context = LocalContext.current
val newMessageTitleTxt:String = context.resources.getQuantityString(R.plurals.messageNew, 1, 1)
BackAppBar( BackAppBar(
title = stringResource(R.string.messageNew), title = newMessageTitleTxt,
backgroundColor = Color.Transparent, // transparent to show the rounded shape of the container backgroundColor = Color.Transparent, // transparent to show the rounded shape of the container
onBack = onBack, onBack = onBack,
actions = { AppBarCloseIcon(onClose = onClose) } actions = { AppBarCloseIcon(onClose = onClose) }
@ -88,7 +93,7 @@ internal fun NewMessage(
SessionTabRow(pagerState, TITLES) SessionTabRow(pagerState, TITLES)
HorizontalPager(pagerState) { HorizontalPager(pagerState) {
when (TITLES[it]) { when (TITLES[it]) {
R.string.enter_account_id -> EnterAccountId(state, callbacks, onHelp) R.string.accountIdEnter -> EnterAccountId(state, callbacks, onHelp)
R.string.qrScan -> QRScannerScreen(qrErrors, onScan = callbacks::onScanQrCode) R.string.qrScan -> QRScannerScreen(qrErrors, onScan = callbacks::onScanQrCode)
} }
} }
@ -116,7 +121,7 @@ private fun EnterAccountId(
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
// There is a known issue with the ime padding on android versions below 30 // There is a known issue with the ime padding on android versions below 30
/// So on these older versions we need to resort to some manual padding based on the visible height // So on these older versions we need to resort to some manual padding based on the visible height
// when the keyboard is up // when the keyboard is up
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
val keyboardHeight by keyboardHeight() val keyboardHeight by keyboardHeight()
@ -149,9 +154,9 @@ private fun EnterAccountId(
Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsSpacing)) Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsSpacing))
BorderlessButtonWithIcon( BorderlessButtonWithIcon(
text = stringResource(R.string.messageNewDescription), text = stringResource(R.string.messageNewDescriptionMobile),
modifier = Modifier modifier = Modifier
.contentDescription(R.string.AccessibilityId_help_desk_link) .contentDescription(R.string.AccessibilityId_messageNewDescriptionMobile)
.padding(horizontal = LocalDimensions.current.mediumSpacing) .padding(horizontal = LocalDimensions.current.mediumSpacing)
.fillMaxWidth(), .fillMaxWidth(),
style = LocalType.current.small, style = LocalType.current.small,

View File

@ -4,6 +4,8 @@ import android.app.Application
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import java.util.concurrent.TimeoutException
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
@ -19,8 +21,6 @@ import org.session.libsession.snode.SnodeAPI
import org.session.libsignal.utilities.PublicKeyValidation import org.session.libsignal.utilities.PublicKeyValidation
import org.session.libsignal.utilities.timeout import org.session.libsignal.utilities.timeout
import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.GetString
import java.util.concurrent.TimeoutException
import javax.inject.Inject
@HiltViewModel @HiltViewModel
internal class NewMessageViewModel @Inject constructor( internal class NewMessageViewModel @Inject constructor(
@ -41,7 +41,6 @@ internal class NewMessageViewModel @Inject constructor(
override fun onChange(value: String) { override fun onChange(value: String) {
loadOnsJob?.cancel() loadOnsJob?.cancel()
loadOnsJob = null loadOnsJob = null
_state.update { it.copy(newMessageIdOrOns = value, isTextErrorColor = false, loading = false) } _state.update { it.copy(newMessageIdOrOns = value, isTextErrorColor = false, loading = false) }
} }
@ -59,7 +58,7 @@ internal class NewMessageViewModel @Inject constructor(
if (PublicKeyValidation.isValid(value, isPrefixRequired = false) && PublicKeyValidation.hasValidPrefix(value)) { if (PublicKeyValidation.isValid(value, isPrefixRequired = false) && PublicKeyValidation.hasValidPrefix(value)) {
onPublicKey(value) onPublicKey(value)
} else { } else {
_qrErrors.tryEmit(application.getString(R.string.this_qr_code_does_not_contain_an_account_id)) _qrErrors.tryEmit(application.getString(R.string.qrNotAccountId))
} }
} }
@ -99,7 +98,7 @@ internal class NewMessageViewModel @Inject constructor(
private fun Exception.toMessage() = when (this) { private fun Exception.toMessage() = when (this) {
is SnodeAPI.Error.Generic -> application.getString(R.string.onsErrorNotRecognized) is SnodeAPI.Error.Generic -> application.getString(R.string.onsErrorNotRecognized)
is TimeoutException -> application.getString(R.string.onsErrorUnableToSearch) is TimeoutException -> application.getString(R.string.onsErrorUnableToSearch)
else -> application.getString(R.string.fragment_enter_public_key_error_message) else -> application.getString(R.string.accountIdErrorInvalid)
} }
} }
@ -112,4 +111,4 @@ internal data class State(
val isNextButtonEnabled: Boolean get() = newMessageIdOrOns.isNotBlank() val isNextButtonEnabled: Boolean get() = newMessageIdOrOns.isNotBlank()
} }
internal data class Success(val publicKey: String) internal data class Success(val publicKey: String)

View File

@ -7,6 +7,7 @@ import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Resources import android.content.res.Resources
import android.database.Cursor import android.database.Cursor
import android.graphics.Rect import android.graphics.Rect
@ -18,10 +19,7 @@ import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.provider.MediaStore import android.provider.MediaStore
import android.text.SpannableStringBuilder
import android.text.SpannedString
import android.text.TextUtils import android.text.TextUtils
import android.text.style.StyleSpan
import android.util.Pair import android.util.Pair
import android.util.TypedValue import android.util.TypedValue
import android.view.ActionMode import android.view.ActionMode
@ -35,8 +33,12 @@ import android.widget.Toast
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.text.set import androidx.compose.runtime.getValue
import androidx.core.text.toSpannable import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.content.ContextCompat
import androidx.core.view.drawToBitmap
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
@ -51,6 +53,8 @@ import androidx.loader.content.Loader
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.annimon.stream.Stream import com.annimon.stream.Stream
import com.bumptech.glide.Glide
import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
@ -64,7 +68,6 @@ import network.loki.messenger.databinding.ActivityConversationV2Binding
import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.ExpiryMode
import nl.komponents.kovenant.ui.successUi import nl.komponents.kovenant.ui.successUi
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.messages.ExpirationConfiguration import org.session.libsession.messaging.messages.ExpirationConfiguration
import org.session.libsession.messaging.messages.applyExpiryMode import org.session.libsession.messaging.messages.applyExpiryMode
import org.session.libsession.messaging.messages.control.DataExtractionNotification import org.session.libsession.messaging.messages.control.DataExtractionNotification
@ -84,6 +87,10 @@ import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Address.Companion.fromSerialized import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.MediaTypes import org.session.libsession.utilities.MediaTypes
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.CONVERSATION_NAME_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY
import org.session.libsession.utilities.Stub import org.session.libsession.utilities.Stub
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.concurrent.SimpleTask import org.session.libsession.utilities.concurrent.SimpleTask
@ -97,6 +104,7 @@ import org.session.libsignal.utilities.guava.Optional
import org.session.libsignal.utilities.hexEncodedPrivateKey import org.session.libsignal.utilities.hexEncodedPrivateKey
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.SessionDialogBuilder
import org.thoughtcrime.securesms.attachments.ScreenshotObserver import org.thoughtcrime.securesms.attachments.ScreenshotObserver
import org.thoughtcrime.securesms.audio.AudioRecorder import org.thoughtcrime.securesms.audio.AudioRecorder
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel
@ -155,7 +163,6 @@ import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mediasend.MediaSendActivity import org.thoughtcrime.securesms.mediasend.MediaSendActivity
import org.thoughtcrime.securesms.mms.AudioSlide import org.thoughtcrime.securesms.mms.AudioSlide
import org.thoughtcrime.securesms.mms.GifSlide import org.thoughtcrime.securesms.mms.GifSlide
import com.bumptech.glide.Glide
import org.thoughtcrime.securesms.mms.ImageSlide import org.thoughtcrime.securesms.mms.ImageSlide
import org.thoughtcrime.securesms.mms.MediaConstraints import org.thoughtcrime.securesms.mms.MediaConstraints
import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.mms.Slide
@ -165,19 +172,21 @@ import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment
import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.ui.OpenURLAlertDialog
import org.thoughtcrime.securesms.ui.theme.SessionMaterialTheme
import org.thoughtcrime.securesms.util.ActivityDispatcher import org.thoughtcrime.securesms.util.ActivityDispatcher
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.MediaUtil import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.NetworkUtils import org.thoughtcrime.securesms.util.NetworkUtils
import org.thoughtcrime.securesms.util.SaveAttachmentTask import org.thoughtcrime.securesms.util.SaveAttachmentTask
import org.thoughtcrime.securesms.util.drawToBitmap
import org.thoughtcrime.securesms.util.isScrolledToBottom import org.thoughtcrime.securesms.util.isScrolledToBottom
import org.thoughtcrime.securesms.util.isScrolledToWithin30dpOfBottom import org.thoughtcrime.securesms.util.isScrolledToWithin30dpOfBottom
import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.show import org.thoughtcrime.securesms.util.show
import org.thoughtcrime.securesms.util.toPx import org.thoughtcrime.securesms.util.toPx
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.LinkedList
import java.util.Locale import java.util.Locale
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -188,6 +197,8 @@ import kotlin.math.abs
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.math.sqrt import kotlin.math.sqrt
import kotlin.time.Duration.Companion.minutes
private const val TAG = "ConversationActivityV2" private const val TAG = "ConversationActivityV2"
@ -231,6 +242,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
.get(LinkPreviewViewModel::class.java) .get(LinkPreviewViewModel::class.java)
} }
private var openLinkDialogUrl: String? by mutableStateOf(null)
private val threadId: Long by lazy { private val threadId: Long by lazy {
var threadId = intent.getLongExtra(THREAD_ID, -1L) var threadId = intent.getLongExtra(THREAD_ID, -1L)
if (threadId == -1L) { if (threadId == -1L) {
@ -279,8 +292,17 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
var searchViewItem: MenuItem? = null var searchViewItem: MenuItem? = null
private val bufferedLastSeenChannel = Channel<Long>(capacity = 512, onBufferOverflow = BufferOverflow.DROP_OLDEST) private val bufferedLastSeenChannel = Channel<Long>(capacity = 512, onBufferOverflow = BufferOverflow.DROP_OLDEST)
private var emojiPickerVisible = false private var emojiPickerVisible = false
// Queue of timestamps used to rate-limit emoji reactions
private val emojiRateLimiterQueue = LinkedList<Long>()
// Constants used to enforce the given maximum emoji reactions allowed per minute (emoji reactions
// that occur above this limit will result in a "Slow down" toast rather than adding the reaction).
private val EMOJI_REACTIONS_ALLOWED_PER_MINUTE = 20
private val ONE_MINUTE_IN_MILLISECONDS = 1.minutes.inWholeMilliseconds
private val isScrolledToBottom: Boolean private val isScrolledToBottom: Boolean
get() = binding.conversationRecyclerView.isScrolledToBottom get() = binding.conversationRecyclerView.isScrolledToBottom
@ -385,12 +407,33 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
// endregion // endregion
fun showOpenUrlDialog(url: String){
openLinkDialogUrl = url
}
// region Lifecycle // region Lifecycle
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady) super.onCreate(savedInstanceState, isReady)
binding = ActivityConversationV2Binding.inflate(layoutInflater) binding = ActivityConversationV2Binding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
// set the compose dialog content
binding.dialogOpenUrl.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
SessionMaterialTheme {
if(!openLinkDialogUrl.isNullOrEmpty()){
OpenURLAlertDialog(
url = openLinkDialogUrl!!,
onDismissRequest = {
openLinkDialogUrl = null
}
)
}
}
}
}
// messageIdToScroll // messageIdToScroll
messageToScrollTimestamp.set(intent.getLongExtra(SCROLL_MESSAGE_ID, -1)) messageToScrollTimestamp.set(intent.getLongExtra(SCROLL_MESSAGE_ID, -1))
messageToScrollAuthor.set(intent.getParcelableExtra(SCROLL_MESSAGE_AUTHOR)) messageToScrollAuthor.set(intent.getParcelableExtra(SCROLL_MESSAGE_AUTHOR))
@ -704,7 +747,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
override fun onFailure(e: ExecutionException?) { override fun onFailure(e: ExecutionException?) {
Toast.makeText(this@ConversationActivityV2, R.string.activity_conversation_attachment_prep_failed, Toast.LENGTH_LONG).show() Toast.makeText(this@ConversationActivityV2, R.string.attachmentsErrorLoad, Toast.LENGTH_LONG).show()
} }
}) })
return return
@ -755,9 +798,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// called from onCreate // called from onCreate
private fun setUpBlockedBanner() { private fun setUpBlockedBanner() {
val recipient = viewModel.recipient?.takeUnless { it.isGroupRecipient } ?: return val recipient = viewModel.recipient?.takeUnless { it.isGroupRecipient } ?: return
val accountID = recipient.address.toString() binding.blockedBannerTextView.text = applicationContext.getString(R.string.blockBlockedDescription)
val name = sessionContactDb.getContactWithAccountID(accountID)?.displayName(Contact.ContactContext.REGULAR) ?: accountID
binding.blockedBannerTextView.text = resources.getString(R.string.activity_conversation_blocked_banner_text, name)
binding.blockedBanner.isVisible = recipient.isBlocked binding.blockedBanner.isVisible = recipient.isBlocked
binding.blockedBanner.setOnClickListener { viewModel.unblock() } binding.blockedBanner.setOnClickListener { viewModel.unblock() }
} }
@ -770,8 +811,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
binding.outdatedBanner.isVisible = shouldShowLegacy binding.outdatedBanner.isVisible = shouldShowLegacy
if (shouldShowLegacy) { if (shouldShowLegacy) {
binding.outdatedBannerTextView.text =
resources.getString(R.string.activity_conversation_outdated_client_banner_text, legacyRecipient!!.name) val txt = Phrase.from(applicationContext, R.string.disappearingMessagesLegacy)
.put(NAME_KEY, legacyRecipient!!.name)
.format()
binding?.outdatedBannerTextView?.text = txt
} }
} }
@ -1056,34 +1100,48 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
updateUnreadCountIndicator() updateUnreadCountIndicator()
} }
// Update placeholder / control messages in a conversation
private fun updatePlaceholder() { private fun updatePlaceholder() {
val recipient = viewModel.recipient ?: return Log.w("Loki", "recipient was null in placeholder update") val recipient = viewModel.recipient ?: return Log.w("Loki", "recipient was null in placeholder update")
val blindedRecipient = viewModel.blindedRecipient val blindedRecipient = viewModel.blindedRecipient
val openGroup = viewModel.openGroup val openGroup = viewModel.openGroup
val (textResource, insertParam) = when { // Get the correct placeholder text for this type of empty conversation
recipient.isLocalNumber -> R.string.activity_conversation_empty_state_note_to_self to null val isNoteToSelf = recipient.isLocalNumber
openGroup != null && !openGroup.canWrite -> R.string.activity_conversation_empty_state_read_only to recipient.toShortString() val txtCS: CharSequence = when {
blindedRecipient?.blocksCommunityMessageRequests == true -> R.string.activity_conversation_empty_state_blocks_community_requests to recipient.toShortString() recipient.isLocalNumber -> getString(R.string.noteToSelfEmpty)
else -> R.string.activity_conversation_empty_state_default to recipient.toShortString()
// If this is a community which we cannot write to
openGroup != null && !openGroup.canWrite -> {
Phrase.from(applicationContext, R.string.conversationsEmpty)
.put(CONVERSATION_NAME_KEY, openGroup.name)
.format()
}
// If we're trying to message someone who has blocked community message requests
blindedRecipient?.blocksCommunityMessageRequests == true -> {
Phrase.from(applicationContext, R.string.messageRequestsTurnedOff)
.put(NAME_KEY, recipient.toShortString())
.format()
}
recipient.isGroupRecipient -> {
// If this is a group or community that we CAN send messages to
Phrase.from(applicationContext, R.string.groupNoMessages)
.put(GROUP_NAME_KEY, recipient.toShortString())
.format()
}
else -> {
Log.w(TAG, "Something else happened in updatePlaceholder - we're not sure what.")
""
}
} }
val showPlaceholder = adapter.itemCount == 0 val showPlaceholder = adapter.itemCount == 0
binding.placeholderText.isVisible = showPlaceholder binding.placeholderText.isVisible = showPlaceholder
if (showPlaceholder) { if (showPlaceholder) {
if (insertParam != null) { binding.placeholderText.text = txtCS
val span = getText(textResource) as SpannedString
val annotations = span.getSpans(0, span.length, StyleSpan::class.java)
val boldSpan = annotations.first()
val spannedParam = insertParam.toSpannable()
spannedParam[0 until spannedParam.length] = StyleSpan(boldSpan.style)
val originalStart = span.getSpanStart(boldSpan)
val originalEnd = span.getSpanEnd(boldSpan)
val newString = SpannableStringBuilder(span)
.replace(originalStart, originalEnd, spannedParam)
binding.placeholderText.text = newString
} else {
binding.placeholderText.setText(textResource)
}
} }
} }
@ -1117,11 +1175,21 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
override fun block(deleteThread: Boolean) { override fun block(deleteThread: Boolean) {
val recipient = viewModel.recipient ?: return Log.w("Loki", "Recipient was null for block action")
showSessionDialog { showSessionDialog {
title(R.string.RecipientPreferenceActivity_block_this_contact_question) title(R.string.block)
text(R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact) text(
dangerButton(R.string.RecipientPreferenceActivity_block, R.string.AccessibilityId_block_confirm) { Phrase.from(context, R.string.blockDescription)
.put(NAME_KEY, recipient.name)
.format()
)
dangerButton(R.string.block, R.string.AccessibilityId_blockConfirm) {
viewModel.block() viewModel.block()
// Block confirmation toast added as per SS-64
val txt = Phrase.from(context, R.string.blockBlockedUser).put(NAME_KEY, recipient.name).format().toString()
Toast.makeText(context, txt, Toast.LENGTH_LONG).show()
if (deleteThread) { if (deleteThread) {
viewModel.deleteThread() viewModel.deleteThread()
finish() finish()
@ -1135,7 +1203,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val clip = ClipData.newPlainText("Account ID", accountId) val clip = ClipData.newPlainText("Account ID", accountId)
val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager
manager.setPrimaryClip(clip) manager.setPrimaryClip(clip)
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.copied, Toast.LENGTH_SHORT).show()
} }
override fun copyOpenGroupUrl(thread: Recipient) { override fun copyOpenGroupUrl(thread: Recipient) {
@ -1147,7 +1215,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val clip = ClipData.newPlainText("Community URL", openGroup.joinURL) val clip = ClipData.newPlainText("Community URL", openGroup.joinURL)
val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager
manager.setPrimaryClip(clip) manager.setPrimaryClip(clip)
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.copied, Toast.LENGTH_SHORT).show()
} }
override fun showDisappearingMessages(thread: Recipient) { override fun showDisappearingMessages(thread: Recipient) {
@ -1160,13 +1228,20 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
override fun unblock() { override fun unblock() {
val recipient = viewModel.recipient ?: return Log.w("Loki", "Recipient was null for unblock action")
if (!recipient.isContactRecipient) {
return Log.w("Loki", "Cannot unblock a user who is not a contact recipient - aborting unblock attempt.")
}
showSessionDialog { showSessionDialog {
title(R.string.ConversationActivity_unblock_this_contact_question) title(R.string.blockUnblock)
text(R.string.ConversationActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact) text(
dangerButton( Phrase.from(context, R.string.blockUnblockName)
R.string.ConversationActivity_unblock, .put(NAME_KEY, recipient.name)
R.string.AccessibilityId_block_confirm .format()
) { viewModel.unblock() } )
dangerButton(R.string.blockUnblock, R.string.AccessibilityId_unblockConfirm) { viewModel.unblock() }
cancelButton() cancelButton()
} }
} }
@ -1177,10 +1252,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
if (actionMode != null) { if (actionMode != null) {
onDeselect(message, position, actionMode) onDeselect(message, position, actionMode)
} else { } else {
// NOTE: // NOTE: We have to use onContentClick (rather than a click listener directly on
// We have to use onContentClick (rather than a click listener directly on
// the view) so as to not interfere with all the other gestures. Do not add // the view) so as to not interfere with all the other gestures. Do not add
// onClickListeners directly to message content views. // onClickListeners directly to message content views!
view.onContentClick(event) view.onContentClick(event)
} }
} }
@ -1279,7 +1353,45 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
} }
// Method to add an emoji to a queue and remove it a short while later - this is used as a
// rate-limiting mechanism and is called from the `sendEmojiReaction` method, below.
fun canPerformEmojiReaction(timestamp: Long): Boolean {
// If the emoji reaction queue is full..
if (emojiRateLimiterQueue.size >= EMOJI_REACTIONS_ALLOWED_PER_MINUTE) {
// ..grab the timestamp of the oldest emoji reaction.
val headTimestamp = emojiRateLimiterQueue.peekFirst()
if (headTimestamp == null) {
Log.w(TAG, "Could not get emoji react head timestamp - should never happen, but we'll allow the emoji reaction.")
return true
}
// With the queue full, if the earliest emoji reaction occurred less than 1 minute ago
// then we reject it..
if (System.currentTimeMillis() - headTimestamp <= ONE_MINUTE_IN_MILLISECONDS) {
return false
} else {
// ..otherwise if the earliest emoji reaction was more than a minute ago we'll
// remove that early reaction to move the timestamp at index 1 into index 0, add
// our new timestamp and return true to accept the emoji reaction.
emojiRateLimiterQueue.removeFirst()
emojiRateLimiterQueue.addLast(timestamp)
return true
}
} else {
// If the queue isn't already full then we add the new timestamp to the back of the queue and allow the emoji reaction
emojiRateLimiterQueue.addLast(timestamp)
return true
}
}
private fun sendEmojiReaction(emoji: String, originalMessage: MessageRecord) { private fun sendEmojiReaction(emoji: String, originalMessage: MessageRecord) {
// Only allow the emoji reaction if we aren't currently rate limited
if (!canPerformEmojiReaction(System.currentTimeMillis())) {
Toast.makeText(this, getString(R.string.emojiReactsCoolDown), Toast.LENGTH_SHORT).show()
return
}
// Create the message // Create the message
val recipient = viewModel.recipient ?: return Log.w(TAG, "Could not locate recipient when sending emoji reaction") val recipient = viewModel.recipient ?: return Log.w(TAG, "Could not locate recipient when sending emoji reaction")
val reactionMessage = VisibleMessage() val reactionMessage = VisibleMessage()
@ -1325,6 +1437,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
} }
// Method to remove a emoji reaction from a message.
// Note: We do not count emoji removal towards the emojiRateLimiterQueue.
private fun sendEmojiRemoval(emoji: String, originalMessage: MessageRecord) { private fun sendEmojiRemoval(emoji: String, originalMessage: MessageRecord) {
val recipient = viewModel.recipient ?: return val recipient = viewModel.recipient ?: return
val message = VisibleMessage() val message = VisibleMessage()
@ -1591,9 +1705,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val isNoteToSelf = (recipient.isContactRecipient && recipient.address.toString() == userPublicKey) val isNoteToSelf = (recipient.isContactRecipient && recipient.address.toString() == userPublicKey)
if (seed in text && !isNoteToSelf && !hasPermissionToSendSeed) { if (seed in text && !isNoteToSelf && !hasPermissionToSendSeed) {
showSessionDialog { showSessionDialog {
title(R.string.dialog_send_seed_title) title(R.string.warning)
text(R.string.dialog_send_seed_explanation) text(R.string.recoveryPasswordWarningSendDescription)
button(R.string.dialog_send_seed_send_button_title) { sendTextOnlyMessage(true) } button(R.string.send) { sendTextOnlyMessage(true) }
cancelButton() cancelButton()
} }
@ -1673,9 +1787,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val hasSeenGIFMetaDataWarning: Boolean = textSecurePreferences.hasSeenGIFMetaDataWarning() val hasSeenGIFMetaDataWarning: Boolean = textSecurePreferences.hasSeenGIFMetaDataWarning()
if (!hasSeenGIFMetaDataWarning) { if (!hasSeenGIFMetaDataWarning) {
showSessionDialog { showSessionDialog {
title(R.string.giphy_permission_title) title(R.string.giphyWarning)
text(R.string.giphy_permission_message) text(Phrase.from(context, R.string.giphyWarningDescription).put(APP_NAME_KEY, getString(R.string.app_name)).format())
button(R.string.continue_2) { button(R.string.theContinue) {
textSecurePreferences.setHasSeenGIFMetaDataWarning() textSecurePreferences.setHasSeenGIFMetaDataWarning()
selectGif() selectGif()
} }
@ -1718,11 +1832,27 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val mediaPreppedListener = object : ListenableFuture.Listener<Boolean> { val mediaPreppedListener = object : ListenableFuture.Listener<Boolean> {
override fun onSuccess(result: Boolean?) { override fun onSuccess(result: Boolean?) {
if (result == null) {
Log.w(TAG, "Media prepper returned a null result - bailing.")
return
}
// If the attachment was too large or MediaConstraints.isSatisfied failed for some
// other reason then we reset the attachment manager & shown buttons then bail..
if (!result) {
attachmentManager.clear()
if (isShowingAttachmentOptions) { toggleAttachmentOptions() }
return
}
// ..otherwise we can attempt to send the attachment(s).
// Note: The only multi-attachment message type is when sending images - all others
// attempt send the attachment immediately upon file selection.
sendAttachments(attachmentManager.buildSlideDeck().asAttachments(), null) sendAttachments(attachmentManager.buildSlideDeck().asAttachments(), null)
} }
override fun onFailure(e: ExecutionException?) { override fun onFailure(e: ExecutionException?) {
Toast.makeText(this@ConversationActivityV2, R.string.activity_conversation_attachment_prep_failed, Toast.LENGTH_LONG).show() Toast.makeText(this@ConversationActivityV2, R.string.attachmentsErrorLoad, Toast.LENGTH_LONG).show()
} }
} }
when (requestCode) { when (requestCode) {
@ -1805,8 +1935,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} else { } else {
Permissions.with(this) Permissions.with(this)
.request(Manifest.permission.RECORD_AUDIO) .request(Manifest.permission.RECORD_AUDIO)
.withRationaleDialog(getString(R.string.ConversationActivity_to_send_audio_messages_allow_signal_access_to_your_microphone), R.drawable.ic_baseline_mic_48) .withRationaleDialog(getString(R.string.permissionsMicrophoneAccessRequired), R.drawable.ic_baseline_mic_48)
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_requires_the_microphone_permission_in_order_to_send_audio_messages)) .withPermanentDenialDialog(Phrase.from(applicationContext, R.string.permissionsMicrophoneAccessRequired)
.put(APP_NAME_KEY, getString(R.string.app_name))
.format().toString())
.execute() .execute()
} }
} }
@ -1876,7 +2008,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
override fun onFailure(e: ExecutionException) { override fun onFailure(e: ExecutionException) {
Toast.makeText(this@ConversationActivityV2, R.string.ConversationActivity_unable_to_record_audio, Toast.LENGTH_LONG).show() Toast.makeText(this@ConversationActivityV2, R.string.audioUnableToRecord, Toast.LENGTH_LONG).show()
} }
}) })
} }
@ -1927,10 +2059,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
private fun showDeleteLocallyUI(messages: Set<MessageRecord>) { private fun showDeleteLocallyUI(messages: Set<MessageRecord>) {
val messageCount = 1
showSessionDialog { showSessionDialog {
title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount)) title(resources.getQuantityString(R.plurals.deleteMessage, messages.count(), messages.count()))
text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount)) text(resources.getString(R.string.deleteMessagesDescriptionDevice))
button(R.string.delete) { messages.forEach(viewModel::deleteLocally); endActionMode() } button(R.string.delete) { messages.forEach(viewModel::deleteLocally); endActionMode() }
cancelButton(::endActionMode) cancelButton(::endActionMode)
} }
@ -1950,13 +2081,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// If the recipient is a community OR a Note-to-Self then we delete the message for everyone // If the recipient is a community OR a Note-to-Self then we delete the message for everyone
if (recipient.isCommunityRecipient || recipient.isLocalNumber) { if (recipient.isCommunityRecipient || recipient.isLocalNumber) {
val messageCount = 1 // Only used for plurals string
showSessionDialog { showSessionDialog {
title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount)) title(resources.getQuantityString(R.plurals.deleteMessage, messages.count(), messages.count()))
text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount)) text(resources.getString(R.string.deleteMessageDescriptionEveryone))
button(R.string.delete) { button(R.string.delete) { messages.forEach(viewModel::deleteForEveryone); endActionMode() }
messages.forEach(viewModel::deleteForEveryone); endActionMode()
}
cancelButton { endActionMode() } cancelButton { endActionMode() }
} }
// Otherwise if this is a 1-on-1 conversation we may decided to delete just for ourselves or delete for everyone // Otherwise if this is a 1-on-1 conversation we may decided to delete just for ourselves or delete for everyone
@ -1981,13 +2109,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
else // Finally, if this is a closed group and you are deleting someone else's message(s) then we can only delete locally. else // Finally, if this is a closed group and you are deleting someone else's message(s) then we can only delete locally.
{ {
val messageCount = 1
showSessionDialog { showSessionDialog {
title(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount)) title(resources.getQuantityString(R.plurals.deleteMessage, messages.count(), messages.count()))
text(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount)) text(resources.getString(R.string.deleteMessageDescriptionDevice))
button(R.string.delete) { dangerButton(R.string.delete) { messages.forEach(viewModel::deleteLocally); endActionMode() }
messages.forEach(viewModel::deleteLocally); endActionMode()
}
cancelButton(::endActionMode) cancelButton(::endActionMode)
} }
} }
@ -1995,18 +2120,18 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
override fun banUser(messages: Set<MessageRecord>) { override fun banUser(messages: Set<MessageRecord>) {
showSessionDialog { showSessionDialog {
title(R.string.ConversationFragment_ban_selected_user) title(R.string.banUser)
text("This will ban the selected user from this room. It won't ban them from other rooms.") text(R.string.communityBanDescription)
button(R.string.ban) { viewModel.banUser(messages.first().individualRecipient); endActionMode() } button(R.string.banUser) { viewModel.banUser(messages.first().individualRecipient); endActionMode() }
cancelButton(::endActionMode) cancelButton(::endActionMode)
} }
} }
override fun banAndDeleteAll(messages: Set<MessageRecord>) { override fun banAndDeleteAll(messages: Set<MessageRecord>) {
showSessionDialog { showSessionDialog {
title(R.string.ConversationFragment_ban_selected_user) title(R.string.banUser)
text("This will ban the selected user from this room and delete all messages sent by them. It won't ban them from other rooms or delete the messages they sent there.") text(R.string.communityBanDeleteDescription)
button(R.string.ban) { viewModel.banAndDeleteAll(messages.first()); endActionMode() } button(R.string.banUser) { viewModel.banAndDeleteAll(messages.first()); endActionMode() }
cancelButton(::endActionMode) cancelButton(::endActionMode)
} }
} }
@ -2042,7 +2167,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
if (TextUtils.isEmpty(result)) { return } if (TextUtils.isEmpty(result)) { return }
val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
manager.setPrimaryClip(ClipData.newPlainText("Message Content", result)) manager.setPrimaryClip(ClipData.newPlainText("Message Content", result))
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.copied, Toast.LENGTH_SHORT).show()
endActionMode() endActionMode()
} }
@ -2051,7 +2176,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val clip = ClipData.newPlainText("Account ID", accountID) val clip = ClipData.newPlainText("Account ID", accountID)
val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
manager.setPrimaryClip(clip) manager.setPrimaryClip(clip)
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.copied, Toast.LENGTH_SHORT).show()
endActionMode() endActionMode()
} }
@ -2093,41 +2218,95 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
endActionMode() endActionMode()
} }
private fun saveAttachments(message: MmsMessageRecord) {
val attachments: List<SaveAttachmentTask.Attachment?> = Stream.of(message.slideDeck.slides)
.filter { s: Slide -> s.uri != null && (s.hasImage() || s.hasVideo() || s.hasAudio() || s.hasDocument()) }
.map { s: Slide -> SaveAttachmentTask.Attachment(s.uri!!, s.contentType, message.dateReceived, s.fileName.orNull()) }
.toList()
if (attachments.isNotEmpty()) {
val saveTask = SaveAttachmentTask(this)
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, *attachments.toTypedArray())
if (!message.isOutgoing) { sendMediaSavedNotification() }
return
}
// Implied else that there were no attachment(s)
Toast.makeText(this, resources.getString(R.string.attachmentsSaveError), Toast.LENGTH_LONG).show()
}
private fun hasPermission(permission: String): Boolean {
val result = ContextCompat.checkSelfPermission(this, permission)
return result == PackageManager.PERMISSION_GRANTED
}
override fun saveAttachment(messages: Set<MessageRecord>) { override fun saveAttachment(messages: Set<MessageRecord>) {
val message = messages.first() as MmsMessageRecord val message = messages.first() as MmsMessageRecord
// Do not allow the user to download a file attachment before it has finished downloading // Note: The save option is only added to the menu in ConversationReactionOverlay.getMenuActionItems
// if the attachment has finished downloading, so we don't really have to check for message.isMediaPending
// here - but we'll do it anyway and bail should that be the case as a defensive programming strategy.
if (message.isMediaPending) { if (message.isMediaPending) {
Toast.makeText(this, resources.getString(R.string.conversation_activity__wait_until_attachment_has_finished_downloading), Toast.LENGTH_LONG).show() Log.w(TAG, "Somehow we were asked to download an attachment before it had finished downloading - aborting download.")
return return
} }
SaveAttachmentTask.showWarningDialog(this) { // Before saving an attachment, regardless of Android API version or permissions, we always want to ensure
// that we've warned the user just _once_ that any attachments they save can be accessed by other apps.
val haveWarned = TextSecurePreferences.getHaveWarnedUserAboutSavingAttachments(this)
if (haveWarned) {
// On Android versions below 30 we require the WRITE_EXTERNAL_STORAGE permission to save attachments.
if (Build.VERSION.SDK_INT < 30) {
// Save the attachment(s) then bail if we already have permission to do so
if (hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
saveAttachments(message)
return
} else {
/* If we don't have the permission then do nothing - which means we continue on to the SaveAttachmentTask part below where we ask for permissions */
}
} else {
// On more modern versions of Android on API 30+ WRITE_EXTERNAL_STORAGE is no longer used and we can just
// save files to the public directories like "Downloads", "Pictures" etc.
saveAttachments(message)
return
}
}
// ..otherwise we must ask for it first (only on Android APIs up to 28).
SaveAttachmentTask.showOneTimeWarningDialogOrSave(this) {
Permissions.with(this) Permissions.with(this)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE) .request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.maxSdkVersion(Build.VERSION_CODES.P) .maxSdkVersion(Build.VERSION_CODES.P) // P is 28
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied)) .withPermanentDenialDialog(Phrase.from(applicationContext, R.string.permissionsStorageSaveDenied)
.put(APP_NAME_KEY, getString(R.string.app_name))
.format().toString())
.onAnyDenied { .onAnyDenied {
endActionMode() endActionMode()
Toast.makeText(this@ConversationActivityV2, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show()
// If permissions were denied inform the user that we can't proceed without them and offer to take the user to Settings
showSessionDialog {
title(R.string.permissionsRequired)
val txt = Phrase.from(applicationContext, R.string.permissionsStorageSaveDenied)
.put(APP_NAME_KEY, getString(R.string.app_name))
.format().toString()
text(txt)
// Take the user directly to the settings app for Session to grant the permission if they
// initially denied it but then have a change of heart when they realise they can't
// proceed without it.
dangerButton(R.string.theContinue) {
val intent = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val uri = Uri.fromParts("package", packageName, null)
intent.setData(uri)
startActivity(intent)
}
button(R.string.cancel)
}
} }
.onAllGranted { .onAllGranted {
endActionMode() endActionMode()
val attachments: List<SaveAttachmentTask.Attachment?> = Stream.of(message.slideDeck.slides) saveAttachments(message)
.filter { s: Slide -> s.uri != null && (s.hasImage() || s.hasVideo() || s.hasAudio() || s.hasDocument()) }
.map { s: Slide -> SaveAttachmentTask.Attachment(s.uri!!, s.contentType, message.dateReceived, s.fileName.orNull()) }
.toList()
if (attachments.isNotEmpty()) {
val saveTask = SaveAttachmentTask(this)
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, *attachments.toTypedArray())
if (!message.isOutgoing) {
sendMediaSavedNotification()
}
return@onAllGranted
}
Toast.makeText(this,
resources.getQuantityString(R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card, 1),
Toast.LENGTH_LONG).show()
} }
.execute() .execute()
} }
@ -2182,6 +2361,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
searchViewModel.onMissingResult() } searchViewModel.onMissingResult() }
} }
} }
binding.searchBottomBar.setData(result.position, result.getResults().size) binding.searchBottomBar.setData(result.position, result.getResults().size)
}) })
} }
@ -2191,6 +2371,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
binding.searchBottomBar.visibility = View.VISIBLE binding.searchBottomBar.visibility = View.VISIBLE
binding.searchBottomBar.setData(0, 0) binding.searchBottomBar.setData(0, 0)
binding.inputBar.visibility = View.INVISIBLE binding.inputBar.visibility = View.INVISIBLE
} }
fun onSearchClosed() { fun onSearchClosed() {

View File

@ -12,6 +12,8 @@ import androidx.core.util.getOrDefault
import androidx.core.util.set import androidx.core.util.set
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.recyclerview.widget.RecyclerView.ViewHolder
import java.util.concurrent.atomic.AtomicLong
import kotlin.math.min
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
@ -20,6 +22,7 @@ import kotlinx.coroutines.launch
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY
import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate
@ -29,8 +32,8 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import com.bumptech.glide.RequestManager import com.bumptech.glide.RequestManager
import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity
import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.showSessionDialog
import java.util.concurrent.atomic.AtomicLong import org.thoughtcrime.securesms.ui.getSubbedCharSequence
import kotlin.math.min import org.thoughtcrime.securesms.ui.getSubbedString
class ConversationAdapter( class ConversationAdapter(
context: Context, context: Context,
@ -154,9 +157,13 @@ class ConversationAdapter(
if (message.isCallLog && message.isFirstMissedCall) { if (message.isCallLog && message.isFirstMissedCall) {
viewHolder.view.setOnClickListener { viewHolder.view.setOnClickListener {
context.showSessionDialog { context.showSessionDialog {
title(R.string.CallNotificationBuilder_first_call_title) val titleTxt = context.getSubbedString(R.string.callsMissedCallFrom, NAME_KEY to message.individualRecipient.name!!)
text(R.string.CallNotificationBuilder_first_call_message) title(titleTxt)
button(R.string.activity_settings_title) {
val bodyTxt = context.getSubbedCharSequence(R.string.callsYouMissedCallPermissions, NAME_KEY to message.individualRecipient.name!!)
text(bodyTxt)
button(R.string.sessionSettings) {
Intent(context, PrivacySettingsActivity::class.java) Intent(context, PrivacySettingsActivity::class.java)
.let(context::startActivity) .let(context::startActivity)
} }
@ -190,7 +197,7 @@ class ConversationAdapter(
private fun getMessageBefore(position: Int, cursor: Cursor): MessageRecord? { private fun getMessageBefore(position: Int, cursor: Cursor): MessageRecord? {
// The message that's visually before the current one is actually after the current // The message that's visually before the current one is actually after the current
// one for the cursor because the layout is reversed // one for the cursor because the layout is reversed
if (isReversed && !cursor.moveToPosition(position + 1)) { return null } if (isReversed && !cursor.moveToPosition(position + 1)) { return null }
if (!isReversed && !cursor.moveToPosition(position - 1)) { return null } if (!isReversed && !cursor.moveToPosition(position - 1)) { return null }
return messageDB.readerFor(cursor).current return messageDB.readerFor(cursor).current

View File

@ -22,7 +22,11 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.doOnLayout import androidx.core.view.doOnLayout
import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat
import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import java.util.Locale
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -30,7 +34,9 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.LocalisedTimeUtil.toShortTwoPartString
import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.StringSubstitutionConstants.TIME_LARGE_KEY
import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber
import org.session.libsession.utilities.ThemeUtil import org.session.libsession.utilities.ThemeUtil
import org.thoughtcrime.securesms.components.emoji.EmojiImageView import org.thoughtcrime.securesms.components.emoji.EmojiImageView
@ -46,14 +52,6 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get
import org.thoughtcrime.securesms.repository.ConversationRepository import org.thoughtcrime.securesms.repository.ConversationRepository
import org.thoughtcrime.securesms.util.AnimationCompleteListener import org.thoughtcrime.securesms.util.AnimationCompleteListener
import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.DateUtils
import java.util.Locale
import javax.inject.Inject
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
@AndroidEntryPoint @AndroidEntryPoint
class ConversationReactionOverlay : FrameLayout { class ConversationReactionOverlay : FrameLayout {
@ -529,11 +527,11 @@ class ConversationReactionOverlay : FrameLayout {
?: return emptyList() ?: return emptyList()
val userPublicKey = getLocalNumber(context)!! val userPublicKey = getLocalNumber(context)!!
// Select message // Select message
items += ActionItem(R.attr.menu_select_icon, R.string.conversation_context__menu_select, { handleActionItemClicked(Action.SELECT) }, R.string.AccessibilityId_select) items += ActionItem(R.attr.menu_select_icon, R.string.select, { handleActionItemClicked(Action.SELECT) }, R.string.AccessibilityId_select)
// Reply // Reply
val canWrite = openGroup == null || openGroup.canWrite val canWrite = openGroup == null || openGroup.canWrite
if (canWrite && !message.isPending && !message.isFailed && !message.isOpenGroupInvitation) { if (canWrite && !message.isPending && !message.isFailed && !message.isOpenGroupInvitation) {
items += ActionItem(R.attr.menu_reply_icon, R.string.conversation_context__menu_reply, { handleActionItemClicked(Action.REPLY) }, R.string.AccessibilityId_reply_message) items += ActionItem(R.attr.menu_reply_icon, R.string.reply, { handleActionItemClicked(Action.REPLY) }, R.string.AccessibilityId_reply)
} }
// Copy message text // Copy message text
if (!containsControlMessage && hasText) { if (!containsControlMessage && hasText) {
@ -541,34 +539,42 @@ class ConversationReactionOverlay : FrameLayout {
} }
// Copy Account ID // Copy Account ID
if (recipient.isGroupRecipient && !recipient.isCommunityRecipient && message.recipient.address.toString() != userPublicKey) { if (recipient.isGroupRecipient && !recipient.isCommunityRecipient && message.recipient.address.toString() != userPublicKey) {
items += ActionItem(R.attr.menu_copy_icon, R.string.activity_conversation_menu_copy_account_id, { handleActionItemClicked(Action.COPY_ACCOUNT_ID) }) items += ActionItem(R.attr.menu_copy_icon, R.string.accountIDCopy, { handleActionItemClicked(Action.COPY_ACCOUNT_ID) })
} }
// Delete message // Delete message
if (userCanDeleteSelectedItems(context, message, openGroup, userPublicKey, blindedPublicKey)) { if (userCanDeleteSelectedItems(context, message, openGroup, userPublicKey, blindedPublicKey)) {
items += ActionItem(R.attr.menu_trash_icon, R.string.delete, { handleActionItemClicked(Action.DELETE) }, items += ActionItem(R.attr.menu_trash_icon, R.string.delete, { handleActionItemClicked(Action.DELETE) },
R.string.AccessibilityId_delete_message, message.subtitle, ThemeUtil.getThemedColor(context, R.attr.danger)) R.string.AccessibilityId_deleteMessage, message.subtitle, ThemeUtil.getThemedColor(context, R.attr.danger))
} }
// Ban user // Ban user
if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey)) { if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey)) {
items += ActionItem(R.attr.menu_block_icon, R.string.conversation_context__menu_ban_user, { handleActionItemClicked(Action.BAN_USER) }) items += ActionItem(R.attr.menu_block_icon, R.string.banUser, { handleActionItemClicked(Action.BAN_USER) })
} }
// Ban and delete all // Ban and delete all
if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey)) { if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey)) {
items += ActionItem(R.attr.menu_trash_icon, R.string.conversation_context__menu_ban_and_delete_all, { handleActionItemClicked(Action.BAN_AND_DELETE_ALL) }) items += ActionItem(R.attr.menu_trash_icon, R.string.banDeleteAll, { handleActionItemClicked(Action.BAN_AND_DELETE_ALL) })
} }
// Message detail // Message detail
items += ActionItem(R.attr.menu_info_icon, R.string.conversation_context__menu_message_details, { handleActionItemClicked(Action.VIEW_INFO) }) items += ActionItem(R.attr.menu_info_icon, R.string.messageInfo, { handleActionItemClicked(Action.VIEW_INFO) })
// Resend // Resend
if (message.isFailed) { if (message.isFailed) {
items += ActionItem(R.attr.menu_reply_icon, R.string.conversation_context__menu_resend_message, { handleActionItemClicked(Action.RESEND) }) items += ActionItem(R.attr.menu_reply_icon, R.string.resend, { handleActionItemClicked(Action.RESEND) })
} }
// Resync // Resync
if (message.isSyncFailed) { if (message.isSyncFailed) {
items += ActionItem(R.attr.menu_reply_icon, R.string.conversation_context__menu_resync_message, { handleActionItemClicked(Action.RESYNC) }) items += ActionItem(R.attr.menu_reply_icon, R.string.resync, { handleActionItemClicked(Action.RESYNC) })
} }
// Save media // Save media..
if (message.isMms && (message as MediaMmsMessageRecord).containsMediaSlide()) { if (message.isMms) {
items += ActionItem(R.attr.menu_save_icon, R.string.conversation_context_image__save_attachment, { handleActionItemClicked(Action.DOWNLOAD) }, R.string.AccessibilityId_save_attachment) // ..but only provide the save option if the there is a media attachment which has finished downloading.
val mmsMessage = message as MediaMmsMessageRecord
if (mmsMessage.containsMediaSlide() && !mmsMessage.isMediaPending) {
items += ActionItem(R.attr.menu_save_icon,
R.string.save,
{ handleActionItemClicked(Action.DOWNLOAD) },
R.string.AccessibilityId_save
)
}
} }
backgroundView.visibility = VISIBLE backgroundView.visibility = VISIBLE
foregroundView.visibility = VISIBLE foregroundView.visibility = VISIBLE
@ -704,10 +710,6 @@ class ConversationReactionOverlay : FrameLayout {
} }
} }
private fun Duration.to2partString(): String? =
toComponents { days, hours, minutes, seconds, nanoseconds -> listOf(days.days, hours.hours, minutes.minutes, seconds.seconds) }
.filter { it.inWholeSeconds > 0L }.take(2).takeIf { it.isNotEmpty() }?.joinToString(" ")
private val MessageRecord.subtitle: ((Context) -> CharSequence?)? private val MessageRecord.subtitle: ((Context) -> CharSequence?)?
get() = if (expiresIn <= 0) { get() = if (expiresIn <= 0) {
null null
@ -715,6 +717,10 @@ private val MessageRecord.subtitle: ((Context) -> CharSequence?)?
(expiresIn - (SnodeAPI.nowWithOffset - (expireStarted.takeIf { it > 0 } ?: timestamp))) (expiresIn - (SnodeAPI.nowWithOffset - (expireStarted.takeIf { it > 0 } ?: timestamp)))
.coerceAtLeast(0L) .coerceAtLeast(0L)
.milliseconds .milliseconds
.to2partString() .toShortTwoPartString()
?.let { context.getString(R.string.auto_deletes_in, it) } .let {
Phrase.from(context, R.string.disappearingMessagesCountdownBigMobile)
.put(TIME_LARGE_KEY, it)
.format().toString()
}
} }

View File

@ -58,7 +58,7 @@ class DeleteOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListen
} }
if (!recipient.isGroupRecipient && !contact.isNullOrEmpty()) { if (!recipient.isGroupRecipient && !contact.isNullOrEmpty()) {
binding.deleteForEveryoneTextView.text = binding.deleteForEveryoneTextView.text =
resources.getString(R.string.delete_message_for_me_and_recipient, contact) resources.getString(R.string.clearMessagesForEveryone, contact)
} }
binding.deleteForEveryoneTextView.isVisible = !recipient.isClosedGroupRecipient binding.deleteForEveryoneTextView.isVisible = !recipient.isClosedGroupRecipient
binding.deleteForMeTextView.setOnClickListener(this) binding.deleteForMeTextView.setOnClickListener(this)

View File

@ -100,7 +100,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
super.onCreate(savedInstanceState, ready) super.onCreate(savedInstanceState, ready)
title = resources.getString(R.string.conversation_context__menu_message_details) title = resources.getString(R.string.messageInfo)
viewModel.timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L) viewModel.timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L)
@ -313,7 +313,7 @@ fun ExpandButton(modifier: Modifier = Modifier, onClick: () -> Unit) {
) { ) {
Icon( Icon(
painter = painterResource(id = R.drawable.ic_expand), painter = painterResource(id = R.drawable.ic_expand),
contentDescription = stringResource(id = R.string.expand), contentDescription = stringResource(id = R.string.AccessibilityId_expand),
modifier = Modifier.clickable { onClick() }, modifier = Modifier.clickable { onClick() },
) )
} }
@ -331,7 +331,7 @@ fun PreviewMessageDetails(
imageAttachments = listOf( imageAttachments = listOf(
Attachment( Attachment(
fileDetails = listOf( fileDetails = listOf(
TitledText(R.string.message_details_header__file_id, "Screen Shot 2023-07-06 at 11.35.50 am.png") TitledText(R.string.attachmentsFileId, "Screen Shot 2023-07-06 at 11.35.50 am.png")
), ),
fileName = "Screen Shot 2023-07-06 at 11.35.50 am.png", fileName = "Screen Shot 2023-07-06 at 11.35.50 am.png",
uri = Uri.parse(""), uri = Uri.parse(""),
@ -339,7 +339,7 @@ fun PreviewMessageDetails(
), ),
Attachment( Attachment(
fileDetails = listOf( fileDetails = listOf(
TitledText(R.string.message_details_header__file_id, "Screen Shot 2023-07-06 at 11.35.50 am.png") TitledText(R.string.attachmentsFileId, "Screen Shot 2023-07-06 at 11.35.50 am.png")
), ),
fileName = "Screen Shot 2023-07-06 at 11.35.50 am.png", fileName = "Screen Shot 2023-07-06 at 11.35.50 am.png",
uri = Uri.parse(""), uri = Uri.parse(""),
@ -347,7 +347,7 @@ fun PreviewMessageDetails(
), ),
Attachment( Attachment(
fileDetails = listOf( fileDetails = listOf(
TitledText(R.string.message_details_header__file_id, "Screen Shot 2023-07-06 at 11.35.50 am.png") TitledText(R.string.attachmentsFileId, "Screen Shot 2023-07-06 at 11.35.50 am.png")
), ),
fileName = "Screen Shot 2023-07-06 at 11.35.50 am.png", fileName = "Screen Shot 2023-07-06 at 11.35.50 am.png",
uri = Uri.parse(""), uri = Uri.parse(""),
@ -356,14 +356,14 @@ fun PreviewMessageDetails(
), ),
nonImageAttachmentFileDetails = listOf( nonImageAttachmentFileDetails = listOf(
TitledText(R.string.message_details_header__file_id, "Screen Shot 2023-07-06 at 11.35.50 am.png"), TitledText(R.string.attachmentsFileId, "Screen Shot 2023-07-06 at 11.35.50 am.png"),
TitledText(R.string.message_details_header__file_type, "image/png"), TitledText(R.string.attachmentsFileType, "image/png"),
TitledText(R.string.message_details_header__file_size, "195.6kB"), TitledText(R.string.attachmentsFileSize, "195.6kB"),
TitledText(R.string.message_details_header__resolution, "342x312"), TitledText(R.string.attachmentsResolution, "342x312"),
), ),
sent = TitledText(R.string.message_details_header__sent, "6:12 AM Tue, 09/08/2022"), sent = TitledText(R.string.sent, "6:12 AM Tue, 09/08/2022"),
received = TitledText(R.string.message_details_header__received, "6:12 AM Tue, 09/08/2022"), received = TitledText(R.string.received, "6:12 AM Tue, 09/08/2022"),
error = TitledText(R.string.message_details_header__error, "Message failed to send"), error = TitledText(R.string.error, "Message failed to send"),
senderInfo = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54"), senderInfo = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54"),
) )
) )

View File

@ -78,9 +78,9 @@ class MessageDetailsViewModel @Inject constructor(
MessageDetailsState( MessageDetailsState(
attachments = slides.map(::Attachment), attachments = slides.map(::Attachment),
record = record, record = record,
sent = dateSent.let(::Date).toString().let { TitledText(R.string.message_details_header__sent, it) }, sent = dateSent.let(::Date).toString().let { TitledText(R.string.sent, it) },
received = dateReceived.let(::Date).toString().let { TitledText(R.string.message_details_header__received, it) }, received = dateReceived.let(::Date).toString().let { TitledText(R.string.received, it) },
error = lokiMessageDatabase.getErrorMessage(id)?.let { TitledText(R.string.message_details_header__error, it) }, error = lokiMessageDatabase.getErrorMessage(id)?.let { TitledText(R.string.theError, it) },
senderInfo = individualRecipient.run { name?.let { TitledText(it, address.serialize()) } }, senderInfo = individualRecipient.run { name?.let { TitledText(it, address.serialize()) } },
sender = individualRecipient, sender = individualRecipient,
thread = threadDb.getRecipientForThreadId(threadId)!!, thread = threadDb.getRecipientForThreadId(threadId)!!,
@ -90,14 +90,14 @@ class MessageDetailsViewModel @Inject constructor(
private val Slide.details: List<TitledText> private val Slide.details: List<TitledText>
get() = listOfNotNull( get() = listOfNotNull(
fileName.orNull()?.let { TitledText(R.string.message_details_header__file_id, it) }, fileName.orNull()?.let { TitledText(R.string.attachmentsFileId, it) },
TitledText(R.string.message_details_header__file_type, asAttachment().contentType), TitledText(R.string.attachmentsFileType, asAttachment().contentType),
TitledText(R.string.message_details_header__file_size, Util.getPrettyFileSize(fileSize)), TitledText(R.string.attachmentsFileSize, Util.getPrettyFileSize(fileSize)),
takeIf { it is ImageSlide } takeIf { it is ImageSlide }
?.let(Slide::asAttachment) ?.let(Slide::asAttachment)
?.run { "${width}x$height" } ?.run { "${width}x$height" }
?.let { TitledText(R.string.message_details_header__resolution, it) }, ?.let { TitledText(R.string.attachmentsResolution, it) },
attachmentDb.duration(this)?.let { TitledText(R.string.message_details_header__duration, it) }, attachmentDb.duration(this)?.let { TitledText(R.string.attachmentsDuration, it) },
) )
private fun AttachmentDatabase.duration(slide: Slide): String? = private fun AttachmentDatabase.duration(slide: Slide): String? =
@ -157,7 +157,7 @@ data class MessageDetailsState(
val sender: Recipient? = null, val sender: Recipient? = null,
val thread: Recipient? = null, val thread: Recipient? = null,
) { ) {
val fromTitle = GetString(R.string.message_details_header__from) val fromTitle = GetString(R.string.from)
val canReply = record?.isOpenGroupInvitation != true val canReply = record?.isOpenGroupInvitation != true
} }

View File

@ -15,9 +15,12 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.squareup.phrase.Phrase
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.FragmentModalUrlBottomSheetBinding import network.loki.messenger.databinding.FragmentModalUrlBottomSheetBinding
import org.thoughtcrime.securesms.util.UiModeUtilities import org.session.libsession.utilities.StringSubstitutionConstants.URL_KEY
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.ui.getSubbedString
class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(), View.OnClickListener { class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(), View.OnClickListener {
private lateinit var binding: FragmentModalUrlBottomSheetBinding private lateinit var binding: FragmentModalUrlBottomSheetBinding
@ -29,7 +32,8 @@ class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val explanation = resources.getString(R.string.dialog_open_url_explanation, url) if (context == null) { return Log.w("MUBS", "Context is null") }
val explanation = requireContext().getSubbedString(R.string.urlOpenDescription, URL_KEY to url)
val spannable = SpannableStringBuilder(explanation) val spannable = SpannableStringBuilder(explanation)
val startIndex = explanation.indexOf(url) val startIndex = explanation.indexOf(url)
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + url.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + url.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
@ -44,7 +48,7 @@ class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(),
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
requireContext().startActivity(intent) requireContext().startActivity(intent)
} catch (e: Exception) { } catch (e: Exception) {
Toast.makeText(context, R.string.invalid_url, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.communityEnterUrlErrorInvalid, Toast.LENGTH_SHORT).show()
} }
dismiss() dismiss()
} }
@ -53,7 +57,7 @@ class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(),
val clip = ClipData.newPlainText("URL", url) val clip = ClipData.newPlainText("URL", url)
val manager = requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager val manager = requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
manager.setPrimaryClip(clip) manager.setPrimaryClip(clip)
Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), R.string.copied, Toast.LENGTH_SHORT).show()
dismiss() dismiss()
} }

View File

@ -17,22 +17,12 @@
package org.thoughtcrime.securesms.conversation.v2 package org.thoughtcrime.securesms.conversation.v2
import android.content.Context import android.content.Context
import android.graphics.Typeface
import android.net.Uri import android.net.Uri
import android.text.Spannable
import android.text.SpannableString
import android.text.TextUtils import android.text.TextUtils
import android.text.style.StyleSpan
import android.view.View import android.view.View
import com.annimon.stream.Stream import com.annimon.stream.Stream
import com.google.android.mms.pdu_alt.CharacterSets
import com.google.android.mms.pdu_alt.EncodedStringValue
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.components.ComposeText
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.UnsupportedEncodingException
import java.util.Collections import java.util.Collections
import org.session.libsignal.utilities.Log
object Util { object Util {
private val TAG: String = Log.tag(Util::class.java) private val TAG: String = Log.tag(Util::class.java)
@ -92,22 +82,6 @@ object Util {
return sb.toString() return sb.toString()
} }
fun isEmpty(value: Array<EncodedStringValue?>?): Boolean {
return value == null || value.size == 0
}
fun isEmpty(value: ComposeText?): Boolean {
return value == null || value.text == null || TextUtils.isEmpty(value.textTrimmed)
}
fun isEmpty(collection: Collection<*>?): Boolean {
return collection == null || collection.isEmpty()
}
fun isEmpty(charSequence: CharSequence?): Boolean {
return charSequence == null || charSequence.length == 0
}
fun wait(lock: Any, timeout: Long) { fun wait(lock: Any, timeout: Long) {
try { try {
(lock as Object).wait(timeout) (lock as Object).wait(timeout)
@ -123,8 +97,7 @@ object Util {
return results return results
} }
val elements = val elements = source.split(delimiter.toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
source.split(delimiter.toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
Collections.addAll(results, *elements) Collections.addAll(results, *elements)
return results return results

View File

@ -11,10 +11,12 @@ import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.squareup.phrase.Phrase
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.AlbumThumbnailViewBinding import network.loki.messenger.databinding.AlbumThumbnailViewBinding
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.utilities.StringSubstitutionConstants.COUNT_KEY
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.MediaPreviewActivity import org.thoughtcrime.securesms.MediaPreviewActivity
import org.thoughtcrime.securesms.components.CornerMask import org.thoughtcrime.securesms.components.CornerMask
@ -97,7 +99,10 @@ class AlbumThumbnailView : RelativeLayout {
binding.albumCellContainer.findViewById<TextView>(R.id.album_cell_overflow_text)?.let { overflowText -> binding.albumCellContainer.findViewById<TextView>(R.id.album_cell_overflow_text)?.let { overflowText ->
// overflowText will be null if !overflowed // overflowText will be null if !overflowed
overflowText.isVisible = overflowed // more than max album size overflowText.isVisible = overflowed // more than max album size
overflowText.text = context.getString(R.string.AlbumThumbnailView_plus, slides.size - MAX_ALBUM_DISPLAY_SIZE) val txt = Phrase.from(context, R.string.andMore)
.put(COUNT_KEY, slides.size - MAX_ALBUM_DISPLAY_SIZE)
.format()
overflowText.text = txt
} }
this.slideSize = slides.size this.slideSize = slides.size
} }
@ -110,10 +115,9 @@ class AlbumThumbnailView : RelativeLayout {
// endregion // endregion
fun layoutRes(slideCount: Int) = when (slideCount) { fun layoutRes(slideCount: Int) = when (slideCount) {
1 -> R.layout.album_thumbnail_1 // single 1 -> R.layout.album_thumbnail_1 // single
2 -> R.layout.album_thumbnail_2// two sidebyside 2 -> R.layout.album_thumbnail_2 // two side-by-side
else -> R.layout.album_thumbnail_3 // three stacked with additional text else -> R.layout.album_thumbnail_3 // three stacked with additional text
} }

View File

@ -11,9 +11,11 @@ import androidx.fragment.app.DialogFragment
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.createSessionDialog import org.thoughtcrime.securesms.createSessionDialog
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.ui.getSubbedCharSequence
/** Shown upon sending a message to a user that's blocked. */ /** Shown upon sending a message to a user that's blocked. */
class BlockedDialog(private val recipient: Recipient, private val context: Context) : DialogFragment() { class BlockedDialog(private val recipient: Recipient, private val context: Context) : DialogFragment() {
@ -24,14 +26,14 @@ class BlockedDialog(private val recipient: Recipient, private val context: Conte
val contact = contactDB.getContactWithAccountID(accountID) val contact = contactDB.getContactWithAccountID(accountID)
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: accountID val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: accountID
val explanation = resources.getString(R.string.dialog_blocked_explanation, name) val explanationCS = context.getSubbedCharSequence(R.string.blockUnblockName, NAME_KEY to name)
val spannable = SpannableStringBuilder(explanation) val spannable = SpannableStringBuilder(explanationCS)
val startIndex = explanation.indexOf(name) val startIndex = explanationCS.indexOf(name)
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
title(resources.getString(R.string.dialog_blocked_title, name)) title(resources.getString(R.string.blockUnblock))
text(spannable) text(spannable)
button(R.string.ConversationActivity_unblock) { unblock() } dangerButton(R.string.blockUnblock, R.string.AccessibilityId_unblockConfirm) { unblock() }
cancelButton { dismiss() } cancelButton { dismiss() }
} }

View File

@ -7,12 +7,14 @@ import android.text.Spannable
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.style.StyleSpan import android.text.style.StyleSpan
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.jobs.AttachmentDownloadJob import org.session.libsession.messaging.jobs.AttachmentDownloadJob
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.StringSubstitutionConstants.CONVERSATION_NAME_KEY
import org.thoughtcrime.securesms.createSessionDialog import org.thoughtcrime.securesms.createSessionDialog
import org.thoughtcrime.securesms.database.SessionContactDatabase import org.thoughtcrime.securesms.database.SessionContactDatabase
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
@ -29,15 +31,19 @@ class DownloadDialog(private val recipient: Recipient) : DialogFragment() {
val accountID = recipient.address.toString() val accountID = recipient.address.toString()
val contact = contactDB.getContactWithAccountID(accountID) val contact = contactDB.getContactWithAccountID(accountID)
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: accountID val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: accountID
title(resources.getString(R.string.dialog_download_title, name))
val explanation = resources.getString(R.string.dialog_download_explanation, name) title(getString(R.string.attachmentsAutoDownloadModalTitle))
val explanation = Phrase.from(context, R.string.attachmentsAutoDownloadModalDescription)
.put(CONVERSATION_NAME_KEY, recipient.name)
.format()
val spannable = SpannableStringBuilder(explanation) val spannable = SpannableStringBuilder(explanation)
val startIndex = explanation.indexOf(name) val startIndex = explanation.indexOf(name)
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
text(spannable) text(spannable)
button(R.string.dialog_download_button_title, R.string.AccessibilityId_download_media) { trust() } button(R.string.download, R.string.AccessibilityId_download) { trust() }
cancelButton { dismiss() } cancelButton { dismiss() }
} }

View File

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.conversation.v2.dialogs package org.thoughtcrime.securesms.conversation.v2.dialogs
import org.thoughtcrime.securesms.createSessionDialog
import android.app.Dialog import android.app.Dialog
import android.graphics.Typeface import android.graphics.Typeface
import android.os.Bundle import android.os.Bundle
@ -8,11 +9,13 @@ import android.text.SpannableStringBuilder
import android.text.style.StyleSpan import android.text.style.StyleSpan
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.squareup.phrase.Phrase
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.utilities.OpenGroupUrlParser import org.session.libsession.utilities.OpenGroupUrlParser
import org.session.libsession.utilities.StringSubstitutionConstants.COMMUNITY_NAME_KEY
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.ThreadUtils import org.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.createSessionDialog
import org.thoughtcrime.securesms.groups.OpenGroupManager import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
@ -20,14 +23,18 @@ import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
class JoinOpenGroupDialog(private val name: String, private val url: String) : DialogFragment() { class JoinOpenGroupDialog(private val name: String, private val url: String) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
title(resources.getString(R.string.dialog_join_open_group_title, name)) title(resources.getString(R.string.communityJoin))
val explanation = resources.getString(R.string.dialog_join_open_group_explanation, name) val explanation = Phrase.from(context, R.string.communityJoinDescription).put(COMMUNITY_NAME_KEY, name).format()
val spannable = SpannableStringBuilder(explanation) val spannable = SpannableStringBuilder(explanation)
val startIndex = explanation.indexOf(name) var startIndex = explanation.indexOf(name)
if (startIndex < 0) {
Log.w("JoinOpenGroupDialog", "Could not find $name in explanation dialog: $explanation")
startIndex = 0 // Limit the startIndex to zero if not found (will be -1) to prevent a crash
}
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
text(spannable) text(spannable)
cancelButton { dismiss() } cancelButton { dismiss() }
button(R.string.open_group_invitation_view__join_accessibility_description) { join() } button(R.string.join) { join() }
} }
private fun join() { private fun join() {
@ -39,7 +46,7 @@ class JoinOpenGroupDialog(private val name: String, private val url: String) : D
MessagingModuleConfiguration.shared.storage.onOpenGroupAdded(openGroup.server, openGroup.room) MessagingModuleConfiguration.shared.storage.onOpenGroupAdded(openGroup.server, openGroup.room)
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(activity) ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(activity)
} catch (e: Exception) { } catch (e: Exception) {
Toast.makeText(activity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show() Toast.makeText(activity, R.string.communityErrorDescription, Toast.LENGTH_SHORT).show()
} }
} }
dismiss() dismiss()

View File

@ -4,18 +4,22 @@ import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.utilities.NonTranslatableStringConstants.APP_NAME
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.createSessionDialog import org.thoughtcrime.securesms.createSessionDialog
import org.thoughtcrime.securesms.ui.getSubbedCharSequence
/** Shown the first time the user inputs a URL that could generate a link preview, to /** Shown the first time the user inputs a URL that could generate a link preview, to
* let them know that Session offers the ability to send and receive link previews. */ * let them know that Session offers the ability to send and receive link previews. */
class LinkPreviewDialog(private val onEnabled: () -> Unit) : DialogFragment() { class LinkPreviewDialog(private val onEnabled: () -> Unit) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
title(R.string.dialog_link_preview_title) title(R.string.linkPreviewsEnable)
text(R.string.dialog_link_preview_explanation) val txt = context.getSubbedCharSequence(R.string.linkPreviewsFirstDescription, APP_NAME_KEY to APP_NAME)
button(R.string.dialog_link_preview_enable_button_title) { enable() } text(txt)
cancelButton { dismiss() } button(R.string.enable) { enable() }
cancelButton { dismiss() }
} }
private fun enable() { private fun enable() {

View File

@ -79,9 +79,9 @@ class InputBar @JvmOverloads constructor(
var voiceMessageDurationMS = 0L var voiceMessageDurationMS = 0L
var voiceRecorderState = VoiceRecorderState.Idle var voiceRecorderState = VoiceRecorderState.Idle
private val attachmentsButton = InputBarButton(context, R.drawable.ic_plus_24).apply { contentDescription = context.getString(R.string.AccessibilityId_attachments_button)} private val attachmentsButton = InputBarButton(context, R.drawable.ic_plus_24).apply { contentDescription = context.getString(R.string.AccessibilityId_attachmentsButton)}
val microphoneButton = InputBarButton(context, R.drawable.ic_microphone).apply { contentDescription = context.getString(R.string.AccessibilityId_microphone_button)} val microphoneButton = InputBarButton(context, R.drawable.ic_microphone).apply { contentDescription = context.getString(R.string.AccessibilityId_voiceMessageNew)}
private val sendButton = InputBarButton(context, R.drawable.ic_arrow_up, true).apply { contentDescription = context.getString(R.string.AccessibilityId_send_message_button)} private val sendButton = InputBarButton(context, R.drawable.ic_arrow_up, true).apply { contentDescription = context.getString(R.string.AccessibilityId_send)}
init { init {
// Attachments button // Attachments button

View File

@ -19,7 +19,6 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewInputBarRecordingBinding import network.loki.messenger.databinding.ViewInputBarRecordingBinding
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.animateSizeChange import org.thoughtcrime.securesms.util.animateSizeChange
import org.thoughtcrime.securesms.util.disableClipping import org.thoughtcrime.securesms.util.disableClipping
import org.thoughtcrime.securesms.util.toPx import org.thoughtcrime.securesms.util.toPx
@ -106,8 +105,7 @@ class InputBarRecordingView : RelativeLayout {
timerJob = scope.launch { timerJob = scope.launch {
while (isActive) { while (isActive) {
val duration = (Date().time - startTimestamp) / 1000L val duration = (Date().time - startTimestamp) / 1000L
binding.recordingViewDurationTextView.text = DateUtils.formatElapsedTime(duration) binding.recordingViewDurationTextView.text = android.text.format.DateUtils.formatElapsedTime(duration)
delay(500) delay(500)
} }
} }

View File

@ -40,6 +40,8 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
val edKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair()!! val edKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!
val blindedPublicKey = openGroup?.publicKey?.let { SodiumUtilities.blindedKeyPair(it, edKeyPair)?.publicKey?.asBytes } val blindedPublicKey = openGroup?.publicKey?.let { SodiumUtilities.blindedKeyPair(it, edKeyPair)?.publicKey?.asBytes }
?.let { AccountId(IdPrefix.BLINDED, it) }?.hexString ?.let { AccountId(IdPrefix.BLINDED, it) }?.hexString
// Embedded function
fun userCanDeleteSelectedItems(): Boolean { fun userCanDeleteSelectedItems(): Boolean {
val allSentByCurrentUser = selectedItems.all { it.isOutgoing } val allSentByCurrentUser = selectedItems.all { it.isOutgoing }
val allReceivedByCurrentUser = selectedItems.all { !it.isOutgoing } val allReceivedByCurrentUser = selectedItems.all { !it.isOutgoing }
@ -47,6 +49,8 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
if (allSentByCurrentUser) { return true } if (allSentByCurrentUser) { return true }
return OpenGroupManager.isUserModerator(context, openGroup.groupId, userPublicKey, blindedPublicKey) return OpenGroupManager.isUserModerator(context, openGroup.groupId, userPublicKey, blindedPublicKey)
} }
// Embedded function
fun userCanBanSelectedUsers(): Boolean { fun userCanBanSelectedUsers(): Boolean {
if (openGroup == null) { return false } if (openGroup == null) { return false }
val anySentByCurrentUser = selectedItems.any { it.isOutgoing } val anySentByCurrentUser = selectedItems.any { it.isOutgoing }
@ -55,6 +59,9 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
if (selectedUsers.size > 1) { return false } if (selectedUsers.size > 1) { return false }
return OpenGroupManager.isUserModerator(context, openGroup.groupId, userPublicKey, blindedPublicKey) return OpenGroupManager.isUserModerator(context, openGroup.groupId, userPublicKey, blindedPublicKey)
} }
// Delete message // Delete message
menu.findItem(R.id.menu_context_delete_message).isVisible = userCanDeleteSelectedItems() menu.findItem(R.id.menu_context_delete_message).isVisible = userCanDeleteSelectedItems()
// Ban user // Ban user

View File

@ -16,10 +16,13 @@ import androidx.appcompat.widget.SearchView.OnQueryTextListener
import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import com.squareup.phrase.Phrase
import java.io.IOException
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.sending_receiving.leave import org.session.libsession.messaging.sending_receiving.leave
import org.session.libsession.utilities.GroupUtil.doubleDecodeGroupID import org.session.libsession.utilities.GroupUtil.doubleDecodeGroupID
import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.guava.Optional import org.session.libsignal.utilities.guava.Optional
@ -38,7 +41,6 @@ import org.thoughtcrime.securesms.service.WebRtcCallService
import org.thoughtcrime.securesms.showMuteDialog import org.thoughtcrime.securesms.showMuteDialog
import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.util.BitmapUtil import org.thoughtcrime.securesms.util.BitmapUtil
import java.io.IOException
object ConversationMenuHelper { object ConversationMenuHelper {
@ -50,11 +52,11 @@ object ConversationMenuHelper {
) { ) {
// Prepare // Prepare
menu.clear() menu.clear()
val isOpenGroup = thread.isCommunityRecipient val isCommunity = thread.isCommunityRecipient
// Base menu (options that should always be present) // Base menu (options that should always be present)
inflater.inflate(R.menu.menu_conversation, menu) inflater.inflate(R.menu.menu_conversation, menu)
// Expiring messages // Expiring messages
if (!isOpenGroup && (thread.hasApprovedMe() || thread.isClosedGroupRecipient || thread.isLocalNumber)) { if (!isCommunity && (thread.hasApprovedMe() || thread.isClosedGroupRecipient || thread.isLocalNumber)) {
inflater.inflate(R.menu.menu_conversation_expiration, menu) inflater.inflate(R.menu.menu_conversation_expiration, menu)
} }
// One-on-one chat menu allows copying the account id // One-on-one chat menu allows copying the account id
@ -74,7 +76,7 @@ object ConversationMenuHelper {
inflater.inflate(R.menu.menu_conversation_closed_group, menu) inflater.inflate(R.menu.menu_conversation_closed_group, menu)
} }
// Open group menu // Open group menu
if (isOpenGroup) { if (isCommunity) {
inflater.inflate(R.menu.menu_conversation_open_group, menu) inflater.inflate(R.menu.menu_conversation_open_group, menu)
} }
// Muting // Muting
@ -162,9 +164,9 @@ object ConversationMenuHelper {
if (!TextSecurePreferences.isCallNotificationsEnabled(context)) { if (!TextSecurePreferences.isCallNotificationsEnabled(context)) {
context.showSessionDialog { context.showSessionDialog {
title(R.string.ConversationActivity_call_title) title(R.string.callsPermissionsRequired)
text(R.string.ConversationActivity_call_prompt) text(R.string.callsPermissionsRequiredDescription)
button(R.string.activity_settings_title, R.string.AccessibilityId_settings) { button(R.string.sessionSettings, R.string.AccessibilityId_sessionSettings) {
Intent(context, PrivacySettingsActivity::class.java).let(context::startActivity) Intent(context, PrivacySettingsActivity::class.java).let(context::startActivity)
} }
cancelButton() cancelButton()
@ -178,7 +180,6 @@ object ConversationMenuHelper {
Intent(context, WebRtcCallActivity::class.java) Intent(context, WebRtcCallActivity::class.java)
.apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK } .apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK }
.let(context::startActivity) .let(context::startActivity)
} }
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
@ -215,7 +216,7 @@ object ConversationMenuHelper {
.setIntent(ShortcutLauncherActivity.createIntent(context, thread.address)) .setIntent(ShortcutLauncherActivity.createIntent(context, thread.address))
.build() .build()
if (ShortcutManagerCompat.requestPinShortcut(context, shortcutInfo, null)) { if (ShortcutManagerCompat.requestPinShortcut(context, shortcutInfo, null)) {
Toast.makeText(context, context.resources.getString(R.string.ConversationActivity_added_to_home_screen), Toast.LENGTH_LONG).show() Toast.makeText(context, context.resources.getString(R.string.conversationsAddedToHome), Toast.LENGTH_LONG).show()
} }
} }
}.execute() }.execute()
@ -272,15 +273,24 @@ object ConversationMenuHelper {
val accountID = TextSecurePreferences.getLocalNumber(context) val accountID = TextSecurePreferences.getLocalNumber(context)
val isCurrentUserAdmin = admins.any { it.toString() == accountID } val isCurrentUserAdmin = admins.any { it.toString() == accountID }
val message = if (isCurrentUserAdmin) { val message = if (isCurrentUserAdmin) {
"Because you are the creator of this group it will be deleted for everyone. This cannot be undone." Phrase.from(context, R.string.groupLeaveDescriptionAdmin)
.put(GROUP_NAME_KEY, group.title)
.format().toString()
} else { } else {
context.resources.getString(R.string.ConversationActivity_are_you_sure_you_want_to_leave_this_group) Phrase.from(context, R.string.groupLeaveDescription)
.put(GROUP_NAME_KEY, group.title)
.format().toString()
} }
fun onLeaveFailed() = Toast.makeText(context, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show() fun onLeaveFailed() {
val txt = Phrase.from(context, R.string.groupLeaveErrorFailed)
.put(GROUP_NAME_KEY, group.title)
.format().toString()
Toast.makeText(context, txt, Toast.LENGTH_LONG).show()
}
context.showSessionDialog { context.showSessionDialog {
title(R.string.ConversationActivity_leave_group) title(R.string.groupLeave)
text(message) text(message)
button(R.string.yes) { button(R.string.yes) {
try { try {
@ -309,7 +319,7 @@ object ConversationMenuHelper {
} }
private fun mute(context: Context, thread: Recipient) { private fun mute(context: Context, thread: Recipient) {
showMuteDialog(ContextThemeWrapper(context, context.theme)) { until -> showMuteDialog(ContextThemeWrapper(context, context.theme)) { until: Long ->
DatabaseComponent.get(context).recipientDatabase().setMuted(thread, until) DatabaseComponent.get(context).recipientDatabase().setMuted(thread, until)
} }
} }

View File

@ -8,17 +8,19 @@ import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewControlMessageBinding import network.loki.messenger.databinding.ViewControlMessageBinding
import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.ExpiryMode
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.messages.ExpirationConfiguration import org.session.libsession.messaging.messages.ExpirationConfiguration
import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY
import org.thoughtcrime.securesms.conversation.disappearingmessages.DisappearingMessages import org.thoughtcrime.securesms.conversation.disappearingmessages.DisappearingMessages
import org.thoughtcrime.securesms.conversation.disappearingmessages.expiryMode import org.thoughtcrime.securesms.conversation.disappearingmessages.expiryMode
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class ControlMessageView : LinearLayout { class ControlMessageView : LinearLayout {
@ -75,8 +77,10 @@ class ControlMessageView : LinearLayout {
} }
} }
message.isMessageRequestResponse -> { message.isMessageRequestResponse -> {
binding.textView.text = context.getString(R.string.message_requests_accepted) binding.textView.text = context.getString(R.string.messageRequestsAccepted)
binding.root.contentDescription=context.getString(R.string.AccessibilityId_message_request_config_message) binding.root.contentDescription = Phrase.from(context, R.string.messageRequestYouHaveAccepted)
.put(NAME_KEY, message.individualRecipient.name)
.format()
} }
message.isCallLog -> { message.isCallLog -> {
val drawable = when { val drawable = when {

View File

@ -21,7 +21,7 @@ class DeletedMessageView : LinearLayout {
// region Updating // region Updating
fun bind(message: MessageRecord, @ColorInt textColor: Int) { fun bind(message: MessageRecord, @ColorInt textColor: Int) {
assert(message.isDeleted) assert(message.isDeleted)
binding.deleteTitleTextView.text = context.getString(R.string.deleted_message) binding.deleteTitleTextView.text = context.resources.getQuantityString(R.plurals.deleteMessageDeleted, 1, 1)
binding.deleteTitleTextView.setTextColor(textColor) binding.deleteTitleTextView.setTextColor(textColor)
binding.deletedMessageViewIconImageView.imageTintList = ColorStateList.valueOf(textColor) binding.deletedMessageViewIconImageView.imageTintList = ColorStateList.valueOf(textColor)
} }

View File

@ -12,8 +12,10 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.setPadding import androidx.core.view.setPadding
import com.google.android.flexbox.JustifyContent import com.google.android.flexbox.JustifyContent
import com.squareup.phrase.Phrase
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewEmojiReactionsBinding import network.loki.messenger.databinding.ViewEmojiReactionsBinding
import org.session.libsession.utilities.StringSubstitutionConstants.COUNT_KEY
import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber
import org.session.libsession.utilities.ThemeUtil import org.session.libsession.utilities.ThemeUtil
import org.thoughtcrime.securesms.components.emoji.EmojiImageView import org.thoughtcrime.securesms.components.emoji.EmojiImageView
@ -199,7 +201,7 @@ class EmojiReactionsView : ConstraintLayout, OnTouchListener {
} else { } else {
emojiView.visibility = GONE emojiView.visibility = GONE
spacer.visibility = GONE spacer.visibility = GONE
countView.text = context.getString(R.string.ReactionsConversationView_plus, reaction.count) countView.text = Phrase.from(context, R.string.andMore).put(COUNT_KEY, reaction.count.toInt()).format()
} }
if (reaction.userWasSender && !isCompact) { if (reaction.userWasSender && !isCompact) {
root.background = ContextCompat.getDrawable(context, R.drawable.reaction_pill_background_selected) root.background = ContextCompat.getDrawable(context, R.drawable.reaction_pill_background_selected)

View File

@ -6,16 +6,16 @@ import android.graphics.Rect
import android.util.AttributeSet import android.util.AttributeSet
import android.view.MotionEvent import android.view.MotionEvent
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.bumptech.glide.RequestManager
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewLinkPreviewBinding import network.loki.messenger.databinding.ViewLinkPreviewBinding
import org.session.libsession.utilities.getColorFromAttr import org.session.libsession.utilities.getColorFromAttr
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.components.CornerMask import org.thoughtcrime.securesms.components.CornerMask
import org.thoughtcrime.securesms.conversation.v2.ModalUrlBottomSheet import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import com.bumptech.glide.RequestManager
import org.thoughtcrime.securesms.mms.ImageSlide import org.thoughtcrime.securesms.mms.ImageSlide
class LinkPreviewView : LinearLayout { class LinkPreviewView : LinearLayout {
@ -84,10 +84,11 @@ class LinkPreviewView : LinearLayout {
} }
} }
fun openURL() { // Method to show the open or copy URL dialog
val url = this.url ?: return private fun openURL() {
val activity = context as AppCompatActivity val url = this.url ?: return Log.w("LinkPreviewView", "Cannot open a null URL")
ModalUrlBottomSheet(url).show(activity.supportFragmentManager, "Open URL Dialog") val activity = context as? ConversationActivityV2
activity?.showOpenUrlDialog(url)
} }
// endregion // endregion
} }

View File

@ -75,13 +75,13 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
val quoteIsLocalUser = localNumber != null && authorPublicKey == localNumber val quoteIsLocalUser = localNumber != null && authorPublicKey == localNumber
val authorDisplayName = val authorDisplayName =
if (quoteIsLocalUser) context.getString(R.string.QuoteView_you) if (quoteIsLocalUser) context.getString(R.string.you)
else author?.displayName(Contact.contextForRecipient(thread)) ?: "${authorPublicKey.take(4)}...${authorPublicKey.takeLast(4)}" else author?.displayName(Contact.contextForRecipient(thread)) ?: "${authorPublicKey.take(4)}...${authorPublicKey.takeLast(4)}"
binding.quoteViewAuthorTextView.text = authorDisplayName binding.quoteViewAuthorTextView.text = authorDisplayName
binding.quoteViewAuthorTextView.setTextColor(getTextColor(isOutgoingMessage)) binding.quoteViewAuthorTextView.setTextColor(getTextColor(isOutgoingMessage))
// Body // Body
binding.quoteViewBodyTextView.text = if (isOpenGroupInvitation) binding.quoteViewBodyTextView.text = if (isOpenGroupInvitation)
resources.getString(R.string.open_group_invitation_view__open_group_invitation) resources.getString(R.string.communityInvitation)
else MentionUtilities.highlightMentions( else MentionUtilities.highlightMentions(
text = (body ?: "").toSpannable(), text = (body ?: "").toSpannable(),
isOutgoingMessage = isOutgoingMessage, isOutgoingMessage = isOutgoingMessage,
@ -106,7 +106,7 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
attachments.audioSlide != null -> { attachments.audioSlide != null -> {
binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone) binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone)
binding.quoteViewAttachmentPreviewImageView.isVisible = true binding.quoteViewAttachmentPreviewImageView.isVisible = true
binding.quoteViewBodyTextView.text = resources.getString(R.string.Slide_audio) binding.quoteViewBodyTextView.text = resources.getString(R.string.audio)
} }
attachments.documentSlide != null -> { attachments.documentSlide != null -> {
binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_document_large_light) binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_document_large_light)
@ -120,7 +120,7 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
.root.setRoundedCorners(toPx(4, resources)) .root.setRoundedCorners(toPx(4, resources))
binding.quoteViewAttachmentThumbnailImageView.root.setImageResource(glide, slide, false) binding.quoteViewAttachmentThumbnailImageView.root.setImageResource(glide, slide, false)
binding.quoteViewAttachmentThumbnailImageView.root.isVisible = true binding.quoteViewAttachmentThumbnailImageView.root.isVisible = true
binding.quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.Slide_video) else resources.getString(R.string.Slide_image) binding.quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.video) else resources.getString(R.string.image)
} }
} }
} }

View File

@ -5,12 +5,13 @@ import android.util.AttributeSet
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.squareup.phrase.Phrase
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewUntrustedAttachmentBinding import network.loki.messenger.databinding.ViewUntrustedAttachmentBinding
import org.session.libsession.utilities.StringSubstitutionConstants.FILE_TYPE_KEY
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.conversation.v2.dialogs.DownloadDialog import org.thoughtcrime.securesms.conversation.v2.dialogs.DownloadDialog
import org.thoughtcrime.securesms.util.ActivityDispatcher import org.thoughtcrime.securesms.util.ActivityDispatcher
import java.util.Locale
class UntrustedAttachmentView: LinearLayout { class UntrustedAttachmentView: LinearLayout {
private val binding: ViewUntrustedAttachmentBinding by lazy { ViewUntrustedAttachmentBinding.bind(this) } private val binding: ViewUntrustedAttachmentBinding by lazy { ViewUntrustedAttachmentBinding.bind(this) }
@ -30,13 +31,17 @@ class UntrustedAttachmentView: LinearLayout {
// region Updating // region Updating
fun bind(attachmentType: AttachmentType, @ColorInt textColor: Int) { fun bind(attachmentType: AttachmentType, @ColorInt textColor: Int) {
val (iconRes, stringRes) = when (attachmentType) { val (iconRes, stringRes) = when (attachmentType) {
AttachmentType.AUDIO -> R.drawable.ic_microphone to R.string.Slide_audio AttachmentType.AUDIO -> R.drawable.ic_microphone to R.string.audio
AttachmentType.DOCUMENT -> R.drawable.ic_document_large_light to R.string.document AttachmentType.DOCUMENT -> R.drawable.ic_document_large_light to R.string.files
AttachmentType.MEDIA -> R.drawable.ic_image_white_24dp to R.string.media AttachmentType.MEDIA -> R.drawable.ic_image_white_24dp to R.string.media
} }
val iconDrawable = ContextCompat.getDrawable(context,iconRes)!! val iconDrawable = ContextCompat.getDrawable(context,iconRes)!!
iconDrawable.mutate().setTint(textColor) iconDrawable.mutate().setTint(textColor)
val text = context.getString(R.string.UntrustedAttachmentView_download_attachment, context.getString(stringRes).toLowerCase(Locale.ROOT))
val text = Phrase.from(context, R.string.attachmentsTapToDownload)
.put(FILE_TYPE_KEY, context.getString(stringRes))
.format()
binding.untrustedAttachmentTitle.text = text
binding.untrustedAttachmentIcon.setImageDrawable(iconDrawable) binding.untrustedAttachmentIcon.setImageDrawable(iconDrawable)
binding.untrustedAttachmentTitle.text = text binding.untrustedAttachmentTitle.text = text

View File

@ -12,34 +12,29 @@ import android.util.AttributeSet
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import androidx.core.text.getSpans import androidx.core.text.getSpans
import androidx.core.text.toSpannable import androidx.core.text.toSpannable
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestManager
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewVisibleMessageContentBinding import network.loki.messenger.databinding.ViewVisibleMessageContentBinding
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.utilities.ThemeUtil import org.session.libsession.utilities.ThemeUtil
import org.session.libsession.utilities.getColorFromAttr import org.session.libsession.utilities.getColorFromAttr
import org.session.libsession.utilities.modifyLayoutParams import org.session.libsession.utilities.modifyLayoutParams
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.conversation.v2.ModalUrlBottomSheet
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
import org.thoughtcrime.securesms.conversation.v2.utilities.ModalURLSpan import org.thoughtcrime.securesms.conversation.v2.utilities.ModalURLSpan
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.database.model.SmsMessageRecord import org.thoughtcrime.securesms.database.model.SmsMessageRecord
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestManager
import org.thoughtcrime.securesms.util.GlowViewUtilities import org.thoughtcrime.securesms.util.GlowViewUtilities
import org.thoughtcrime.securesms.util.SearchUtil import org.thoughtcrime.securesms.util.SearchUtil
import org.thoughtcrime.securesms.util.getAccentColor import org.thoughtcrime.securesms.util.getAccentColor
@ -117,7 +112,7 @@ class VisibleMessageContentView : ConstraintLayout {
binding.quoteView.root.isVisible = true binding.quoteView.root.isVisible = true
val quote = message.quote!! val quote = message.quote!!
val quoteText = if (quote.isOriginalMissing) { val quoteText = if (quote.isOriginalMissing) {
context.getString(R.string.QuoteView_original_missing) context.getString(R.string.messageErrorOriginal)
} else { } else {
quote.text quote.text
} }
@ -292,8 +287,8 @@ class VisibleMessageContentView : ConstraintLayout {
body.getSpans<URLSpan>(0, body.length).toList().forEach { urlSpan -> body.getSpans<URLSpan>(0, body.length).toList().forEach { urlSpan ->
val updatedUrl = urlSpan.url.let { it.toHttpUrlOrNull().toString() } val updatedUrl = urlSpan.url.let { it.toHttpUrlOrNull().toString() }
val replacementSpan = ModalURLSpan(updatedUrl) { url -> val replacementSpan = ModalURLSpan(updatedUrl) { url ->
val activity = context as AppCompatActivity val activity = context as? ConversationActivityV2
ModalUrlBottomSheet(url).show(activity.supportFragmentManager, "Open URL Dialog") activity?.showOpenUrlDialog(url)
} }
val start = body.getSpanStart(urlSpan) val start = body.getSpanStart(urlSpan)
val end = body.getSpanEnd(urlSpan) val end = body.getSpanEnd(urlSpan)

View File

@ -27,6 +27,13 @@ import androidx.core.os.bundleOf
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.marginBottom import androidx.core.view.marginBottom
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import java.util.Date
import java.util.Locale
import javax.inject.Inject
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sqrt
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewEmojiReactionsBinding import network.loki.messenger.databinding.ViewEmojiReactionsBinding
import network.loki.messenger.databinding.ViewVisibleMessageBinding import network.loki.messenger.databinding.ViewVisibleMessageBinding
@ -58,13 +65,6 @@ import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.disableClipping import org.thoughtcrime.securesms.util.disableClipping
import org.thoughtcrime.securesms.util.toDp import org.thoughtcrime.securesms.util.toDp
import org.thoughtcrime.securesms.util.toPx import org.thoughtcrime.securesms.util.toPx
import java.util.Date
import java.util.Locale
import javax.inject.Inject
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sqrt
private const val TAG = "VisibleMessageView" private const val TAG = "VisibleMessageView"
@ -269,8 +269,7 @@ class VisibleMessageView : FrameLayout {
// Method to display or hide the status of a message. // Method to display or hide the status of a message.
// Note: Although most commonly used to display the delivery status of a message, we also use the // Note: Although most commonly used to display the delivery status of a message, we also use the
// message status area to display the disappearing messages state - so in this latter case we'll // message status area to display the disappearing messages state - so in this latter case we'll
// be displaying the "Sent" and the animating clock icon for outgoing messages or "Read" and the // be displaying either "Sent" or "Read" and the animating clock icon.
// animated clock icon for incoming messages.
private fun showStatusMessage(message: MessageRecord) { private fun showStatusMessage(message: MessageRecord) {
// We'll start by hiding everything and then only make visible what we need // We'll start by hiding everything and then only make visible what we need
binding.messageStatusTextView.isVisible = false binding.messageStatusTextView.isVisible = false
@ -384,37 +383,47 @@ class VisibleMessageView : FrameLayout {
message.isFailed -> message.isFailed ->
MessageStatusInfo(R.drawable.ic_delivery_status_failed, MessageStatusInfo(R.drawable.ic_delivery_status_failed,
getThemedColor(context, R.attr.danger), getThemedColor(context, R.attr.danger),
R.string.delivery_status_failed R.string.messageStatusFailedToSend
) )
message.isSyncFailed -> message.isSyncFailed ->
MessageStatusInfo( MessageStatusInfo(
R.drawable.ic_delivery_status_failed, R.drawable.ic_delivery_status_failed,
context.getColor(R.color.accent_orange), context.getColor(R.color.accent_orange),
R.string.delivery_status_sync_failed R.string.messageStatusFailedToSync
) )
message.isPending -> message.isPending ->
MessageStatusInfo( // Non-mms messages display 'Sending'..
R.drawable.ic_delivery_status_sending, if (!message.isMms) {
context.getColorFromAttr(R.attr.message_status_color), MessageStatusInfo(
R.string.delivery_status_sending R.drawable.ic_delivery_status_sending,
) context.getColorFromAttr(R.attr.message_status_color),
R.string.sending
)
} else {
// ..and Mms messages display 'Uploading'.
MessageStatusInfo(
R.drawable.ic_delivery_status_sending,
context.getColorFromAttr(R.attr.message_status_color),
R.string.messageStatusUploading
)
}
message.isSyncing || message.isResyncing -> message.isSyncing || message.isResyncing ->
MessageStatusInfo( MessageStatusInfo(
R.drawable.ic_delivery_status_sending, R.drawable.ic_delivery_status_sending,
context.getColorFromAttr(R.attr.message_status_color), context.getColorFromAttr(R.attr.message_status_color),
R.string.delivery_status_sending // We COULD tell the user that we're `syncing` (R.string.delivery_status_syncing) but it will likely make more sense to them if we say "Sending" R.string.messageStatusSyncing
) )
message.isRead || message.isIncoming -> message.isRead || message.isIncoming ->
MessageStatusInfo( MessageStatusInfo(
R.drawable.ic_delivery_status_read, R.drawable.ic_delivery_status_read,
context.getColorFromAttr(R.attr.message_status_color), context.getColorFromAttr(R.attr.message_status_color),
R.string.delivery_status_read R.string.read
) )
message.isSent -> message.isSent ->
MessageStatusInfo( MessageStatusInfo(
R.drawable.ic_delivery_status_sent, R.drawable.ic_delivery_status_sent,
context.getColorFromAttr(R.attr.message_status_color), context.getColorFromAttr(R.attr.message_status_color),
R.string.delivery_status_sent R.string.disappearingMessagesSent
) )
else -> { else -> {
// The message isn't one we care about for message statuses we display to the user (i.e., // The message isn't one we care about for message statuses we display to the user (i.e.,

View File

@ -5,9 +5,11 @@ import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
import com.squareup.phrase.Phrase
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewSearchBottomBarBinding import network.loki.messenger.databinding.ViewSearchBottomBarBinding
import org.session.libsession.utilities.StringSubstitutionConstants.COUNT_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.TOTAL_COUNT_KEY
class SearchBottomBar : LinearLayout { class SearchBottomBar : LinearLayout {
private lateinit var binding: ViewSearchBottomBarBinding private lateinit var binding: ViewSearchBottomBarBinding
@ -35,7 +37,7 @@ class SearchBottomBar : LinearLayout {
} }
} }
if (count > 0) { if (count > 0) {
searchPosition.text = resources.getString(R.string.ConversationActivity_search_position, position + 1, count) searchPosition.text = resources.getQuantityString(R.plurals.searchMatches, count, position + 1, count)
} else { } else {
searchPosition.text = "" searchPosition.text = ""
} }

View File

@ -94,6 +94,8 @@ class SearchViewModel @Inject constructor(
} }
} }
public fun getActiveQuery() = activeQuery
class SearchResult(private val results: CursorList<MessageResult?>, val position: Int) : Closeable { class SearchResult(private val results: CursorList<MessageResult?>, val position: Int) : Closeable {
fun getResults(): List<MessageResult?> { fun getResults(): List<MessageResult?> {

View File

@ -16,6 +16,9 @@
*/ */
package org.thoughtcrime.securesms.conversation.v2.utilities; package org.thoughtcrime.securesms.conversation.v2.utilities;
import static com.google.android.gms.common.util.CollectionUtils.listOf;
import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
import android.Manifest; import android.Manifest;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
@ -30,10 +33,14 @@ import android.provider.OpenableColumns;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Pair; import android.util.Pair;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.squareup.phrase.Phrase;
import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import network.loki.messenger.R;
import org.session.libsession.utilities.recipients.Recipient; import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsignal.utilities.ListenableFuture; import org.session.libsignal.utilities.ListenableFuture;
import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Log;
@ -55,17 +62,13 @@ import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import network.loki.messenger.R;
public class AttachmentManager { public class AttachmentManager {
private final static String TAG = AttachmentManager.class.getSimpleName(); private final static String TAG = AttachmentManager.class.getSimpleName();
// Max attachment size is 10MB, above which we display a warning toast rather than sending the msg
private final long MAX_ATTACHMENTS_FILE_SIZE_BYTES = 10 * 1024 * 1024;
private final @NonNull Context context; private final @NonNull Context context;
private final @NonNull AttachmentListener attachmentListener; private final @NonNull AttachmentListener attachmentListener;
@ -252,13 +255,31 @@ public class AttachmentManager {
} else { } else {
builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE); builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE);
} }
builder.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
.withRationaleDialog(activity.getString(R.string.ConversationActivity_to_send_photos_and_video_allow_signal_access_to_storage), R.drawable.ic_baseline_photo_library_24) Context c = activity.getApplicationContext();
String needStoragePermissionTxt = Phrase.from(c, R.string.permissionsStorageSend).put(APP_NAME_KEY, c.getString(R.string.app_name)).format().toString();
String storagePermissionDeniedTxt = Phrase.from(c, R.string.permissionsStorageSaveDenied)
.put(APP_NAME_KEY, c.getString(R.string.app_name))
.format().toString();
builder.withPermanentDenialDialog(storagePermissionDeniedTxt)
.withRationaleDialog(needStoragePermissionTxt, R.drawable.ic_baseline_photo_library_24)
.onAllGranted(() -> selectMediaType(activity, "*/*", null, requestCode)) // Note: We can use startActivityForResult w/ the ACTION_OPEN_DOCUMENT or ACTION_OPEN_DOCUMENT_TREE intent if we need to modernise this. .onAllGranted(() -> selectMediaType(activity, "*/*", null, requestCode)) // Note: We can use startActivityForResult w/ the ACTION_OPEN_DOCUMENT or ACTION_OPEN_DOCUMENT_TREE intent if we need to modernise this.
.execute(); .execute();
} }
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body) { public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body) {
Context c = activity.getApplicationContext();
String needStoragePermissionTxt = Phrase.from(c, R.string.permissionsStorageSend)
.put(APP_NAME_KEY, c.getString(R.string.app_name))
.format().toString();
String cameraPermissionDeniedTxt = Phrase.from(c, R.string.cameraGrantAccessDenied)
.put(APP_NAME_KEY, c.getString(R.string.app_name))
.format().toString();
Permissions.PermissionsBuilder builder = Permissions.with(activity); Permissions.PermissionsBuilder builder = Permissions.with(activity);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
builder = builder.request(Manifest.permission.READ_MEDIA_VIDEO) builder = builder.request(Manifest.permission.READ_MEDIA_VIDEO)
@ -266,8 +287,8 @@ public class AttachmentManager {
} else { } else {
builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE); builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE);
} }
builder.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio)) builder.withPermanentDenialDialog(cameraPermissionDeniedTxt)
.withRationaleDialog(activity.getString(R.string.ConversationActivity_to_send_photos_and_video_allow_signal_access_to_storage), R.drawable.ic_baseline_photo_library_24) .withRationaleDialog(needStoragePermissionTxt, R.drawable.ic_baseline_photo_library_24)
.onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode)) .onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode))
.execute(); .execute();
} }
@ -291,10 +312,19 @@ public class AttachmentManager {
} }
public void capturePhoto(Activity activity, int requestCode, Recipient recipient) { public void capturePhoto(Activity activity, int requestCode, Recipient recipient) {
String cameraPermissionDeniedTxt = Phrase.from(context, R.string.cameraGrantAccessDenied)
.put(APP_NAME_KEY, context.getString(R.string.app_name))
.format().toString();
String requireCameraPermissionTxt = Phrase.from(context, R.string.cameraGrantAccessDescription)
.put(APP_NAME_KEY, context.getString(R.string.app_name))
.format().toString();
Permissions.with(activity) Permissions.with(activity)
.request(Manifest.permission.CAMERA) .request(Manifest.permission.CAMERA)
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_camera_permission_in_order_to_take_photos_but_it_has_been_permanently_denied)) .withPermanentDenialDialog(cameraPermissionDeniedTxt)
.withRationaleDialog(activity.getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera),R.drawable.ic_baseline_photo_camera_24) .withRationaleDialog(requireCameraPermissionTxt, R.drawable.ic_baseline_photo_camera_24)
.onAllGranted(() -> { .onAllGranted(() -> {
Intent captureIntent = MediaSendActivity.buildCameraIntent(activity, recipient); Intent captureIntent = MediaSendActivity.buildCameraIntent(activity, recipient);
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) { if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
@ -326,7 +356,7 @@ public class AttachmentManager {
activity.startActivityForResult(intent, requestCode); activity.startActivityForResult(intent, requestCode);
} catch (ActivityNotFoundException anfe) { } catch (ActivityNotFoundException anfe) {
Log.w(TAG, "couldn't complete ACTION_GET_CONTENT intent, no activity found. falling back."); Log.w(TAG, "couldn't complete ACTION_GET_CONTENT intent, no activity found. falling back.");
Toast.makeText(activity, R.string.AttachmentManager_cant_open_media_selection, Toast.LENGTH_LONG).show(); Toast.makeText(activity, R.string.attachmentsErrorNoApp, Toast.LENGTH_LONG).show();
} }
} }
@ -334,9 +364,21 @@ public class AttachmentManager {
final @Nullable Slide slide, final @Nullable Slide slide,
final @NonNull MediaConstraints constraints) final @NonNull MediaConstraints constraints)
{ {
return slide == null || // Null attachment? Not satisfied.
constraints.isSatisfied(context, slide.asAttachment()) || if (slide == null) return false;
constraints.canResize(slide.asAttachment());
// Attachments are excessively large? Not satisfied.
// Note: This file size test must come BEFORE the `constraints.isSatisfied` check below because
// it is a more specific type of check.
if (slide.asAttachment().getSize() > MAX_ATTACHMENTS_FILE_SIZE_BYTES) {
Toast.makeText(context, R.string.attachmentsErrorSize, Toast.LENGTH_SHORT).show();
return false;
}
// Otherwise we return whether our constraints are satisfied OR if we can resize the attachment
// (in the case of one or more images) - either one will be acceptable, but if both aren't then
// we fail the constraint test.
return constraints.isSatisfied(context, slide.asAttachment()) || constraints.canResize(slide.asAttachment());
} }
public interface AttachmentListener { public interface AttachmentListener {

View File

@ -54,13 +54,14 @@ object MentionUtilities {
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
val openGroup by lazy { DatabaseComponent.get(context).storage().getOpenGroup(threadID) } val openGroup by lazy { DatabaseComponent.get(context).storage().getOpenGroup(threadID) }
// format the mention text // Format the mention text
if (matcher.find(startIndex)) { if (matcher.find(startIndex)) {
while (true) { while (true) {
val publicKey = text.subSequence(matcher.start() + 1, matcher.end()).toString() // +1 to get rid of the @ val publicKey = text.subSequence(matcher.start() + 1, matcher.end()).toString() // +1 to get rid of the @
val isYou = isYou(publicKey, userPublicKey, openGroup) val isYou = isYou(publicKey, userPublicKey, openGroup)
val userDisplayName: String? = if (isYou) { val userDisplayName: String? = if (isYou) {
context.getString(R.string.MessageRecord_you) context.getString(R.string.you)
} else { } else {
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithAccountID(publicKey) val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithAccountID(publicKey)
@Suppress("NAME_SHADOWING") val context = if (openGroup != null) Contact.ContactContext.OPEN_GROUP else Contact.ContactContext.REGULAR @Suppress("NAME_SHADOWING") val context = if (openGroup != null) Contact.ContactContext.OPEN_GROUP else Contact.ContactContext.REGULAR

View File

@ -8,7 +8,7 @@ import org.thoughtcrime.securesms.showSessionDialog
object NotificationUtils { object NotificationUtils {
fun showNotifyDialog(context: Context, thread: Recipient, notifyTypeHandler: (Int)->Unit) { fun showNotifyDialog(context: Context, thread: Recipient, notifyTypeHandler: (Int)->Unit) {
context.showSessionDialog { context.showSessionDialog {
title(R.string.RecipientPreferenceActivity_notification_settings) title(R.string.sessionNotifications)
singleChoiceItems( singleChoiceItems(
context.resources.getStringArray(R.array.notify_types), context.resources.getStringArray(R.array.notify_types),
thread.notifyType thread.notifyType

View File

@ -1,9 +1,13 @@
package org.thoughtcrime.securesms.conversation.v2.utilities package org.thoughtcrime.securesms.conversation.v2.utilities
import android.graphics.Rect import android.graphics.Rect
import android.graphics.Typeface
import android.text.Layout import android.text.Layout
import android.text.SpannableString
import android.text.Spanned
import android.text.StaticLayout import android.text.StaticLayout
import android.text.TextPaint import android.text.TextPaint
import android.text.style.StyleSpan
import android.view.MotionEvent import android.view.MotionEvent
import android.widget.TextView import android.widget.TextView
import androidx.core.text.getSpans import androidx.core.text.getSpans

View File

@ -3,14 +3,8 @@ package org.thoughtcrime.securesms.database;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri;
import androidx.annotation.Nullable;
import net.zetetic.database.sqlcipher.SQLiteDatabase; import net.zetetic.database.sqlcipher.SQLiteDatabase;
import network.loki.messenger.R;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -24,10 +18,10 @@ public class DraftDatabase extends Database {
public static final String DRAFT_VALUE = "value"; public static final String DRAFT_VALUE = "value";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " + public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
THREAD_ID + " INTEGER, " + DRAFT_TYPE + " TEXT, " + DRAFT_VALUE + " TEXT);"; THREAD_ID + " INTEGER, " + DRAFT_TYPE + " TEXT, " + DRAFT_VALUE + " TEXT);";
public static final String[] CREATE_INDEXS = { public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS draft_thread_index ON " + TABLE_NAME + " (" + THREAD_ID + ");", "CREATE INDEX IF NOT EXISTS draft_thread_index ON " + TABLE_NAME + " (" + THREAD_ID + ");",
}; };
public DraftDatabase(Context context, SQLCipherOpenHelper databaseHelper) { public DraftDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
@ -59,8 +53,8 @@ public class DraftDatabase extends Database {
for (long threadId : threadIds) { for (long threadId : threadIds) {
where.append(" OR ") where.append(" OR ")
.append(THREAD_ID) .append(THREAD_ID)
.append(" = ?"); .append(" = ?");
arguments.add(String.valueOf(threadId)); arguments.add(String.valueOf(threadId));
} }
@ -95,12 +89,10 @@ public class DraftDatabase extends Database {
} }
} }
// Class to save drafts of text (only) messages if the user is in the middle of writing a message
// and then the app loses focus or is closed.
public static class Draft { public static class Draft {
public static final String TEXT = "text"; public static final String TEXT = "text";
public static final String IMAGE = "image";
public static final String VIDEO = "video";
public static final String AUDIO = "audio";
public static final String QUOTE = "quote";
private final String type; private final String type;
private final String value; private final String value;
@ -117,48 +109,10 @@ public class DraftDatabase extends Database {
public String getValue() { public String getValue() {
return value; return value;
} }
String getSnippet(Context context) {
switch (type) {
case TEXT: return value;
case IMAGE: return context.getString(R.string.DraftDatabase_Draft_image_snippet);
case VIDEO: return context.getString(R.string.DraftDatabase_Draft_video_snippet);
case AUDIO: return context.getString(R.string.DraftDatabase_Draft_audio_snippet);
case QUOTE: return context.getString(R.string.DraftDatabase_Draft_quote_snippet);
default: return null;
}
}
} }
public static class Drafts extends LinkedList<Draft> { public static class Drafts extends LinkedList<Draft> {
private Draft getDraftOfType(String type) { // We don't do anything with drafts of a given type anymore (image, audio etc.) - we store TEXT
for (Draft draft : this) { // drafts, and any files or audio get sent to the recipient when added as a message.
if (type.equals(draft.getType())) {
return draft;
}
}
return null;
}
public String getSnippet(Context context) {
Draft textDraft = getDraftOfType(Draft.TEXT);
if (textDraft != null) {
return textDraft.getSnippet(context);
} else if (size() > 0) {
return get(0).getSnippet(context);
} else {
return "";
}
}
public @Nullable Uri getUriSnippet() {
Draft imageDraft = getDraftOfType(Draft.IMAGE);
if (imageDraft != null && imageDraft.getValue() != null) {
return Uri.parse(imageDraft.getValue());
}
return null;
}
} }
} }

View File

@ -633,7 +633,11 @@ open class Storage(
// Notify the user // Notify the user
val threadID = getOrCreateThreadIdFor(Address.fromSerialized(groupId)) val threadID = getOrCreateThreadIdFor(Address.fromSerialized(groupId))
threadDb.setDate(threadID, formationTimestamp) threadDb.setDate(threadID, formationTimestamp)
insertOutgoingInfoMessage(context, groupId, SignalServiceGroup.Type.CREATION, title, members.map { it.serialize() }, admins.map { it.serialize() }, threadID, formationTimestamp)
// Note: Commenting out this line prevents the timestamp of room creation being added to a new closed group,
// which in turn allows us to show the `groupNoMessages` control message text.
//insertOutgoingInfoMessage(context, groupId, SignalServiceGroup.Type.CREATION, title, members.map { it.serialize() }, admins.map { it.serialize() }, threadID, formationTimestamp)
// Don't create config group here, it's from a config update // Don't create config group here, it's from a config update
// Start polling // Start polling
ClosedGroupPollerV2.shared.startPolling(group.accountId) ClosedGroupPollerV2.shared.startPolling(group.accountId)

View File

@ -808,8 +808,8 @@ public class ThreadDatabase extends Database {
private @NonNull String getFormattedBodyFor(@NonNull MessageRecord messageRecord) { private @NonNull String getFormattedBodyFor(@NonNull MessageRecord messageRecord) {
if (messageRecord.isMms()) { if (messageRecord.isMms()) {
MmsMessageRecord record = (MmsMessageRecord) messageRecord; MmsMessageRecord record = (MmsMessageRecord) messageRecord;
if (record.getSharedContacts().size() > 0) { if (!record.getSharedContacts().isEmpty()) {
Contact contact = ((MmsMessageRecord) messageRecord).getSharedContacts().get(0); Contact contact = ((MmsMessageRecord)messageRecord).getSharedContacts().get(0);
return ContactUtil.getStringSummary(context, contact).toString(); return ContactUtil.getStringSummary(context, contact).toString();
} }
String attachmentString = record.getSlideDeck().getBody(); String attachmentString = record.getSlideDeck().getBody();

View File

@ -1,18 +1,20 @@
package org.thoughtcrime.securesms.database.helpers; package org.thoughtcrime.securesms.database.helpers;
import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
import android.app.NotificationChannel; import android.app.NotificationChannel;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import com.squareup.phrase.Phrase;
import java.io.File;
import net.zetetic.database.sqlcipher.SQLiteConnection; import net.zetetic.database.sqlcipher.SQLiteConnection;
import net.zetetic.database.sqlcipher.SQLiteDatabase; import net.zetetic.database.sqlcipher.SQLiteDatabase;
import net.zetetic.database.sqlcipher.SQLiteDatabaseHook; import net.zetetic.database.sqlcipher.SQLiteDatabaseHook;
import net.zetetic.database.sqlcipher.SQLiteOpenHelper; import net.zetetic.database.sqlcipher.SQLiteOpenHelper;
import network.loki.messenger.R;
import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.crypto.DatabaseSecret; import org.thoughtcrime.securesms.crypto.DatabaseSecret;
@ -39,13 +41,8 @@ import org.thoughtcrime.securesms.database.SessionContactDatabase;
import org.thoughtcrime.securesms.database.SessionJobDatabase; import org.thoughtcrime.securesms.database.SessionJobDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities; import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities;
import java.io.File;
import network.loki.messenger.R;
public class SQLCipherOpenHelper extends SQLiteOpenHelper { public class SQLCipherOpenHelper extends SQLiteOpenHelper {
@SuppressWarnings("unused") @SuppressWarnings("unused")
@ -250,18 +247,22 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
// Notify the user of the issue so they know they can downgrade until the issue is fixed // Notify the user of the issue so they know they can downgrade until the issue is fixed
NotificationManager notificationManager = context.getSystemService(NotificationManager.class); NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
String channelId = context.getString(R.string.NotificationChannel_failures); String channelId = context.getString(R.string.failures);
NotificationChannel channel = new NotificationChannel(channelId, channelId, NotificationManager.IMPORTANCE_HIGH); NotificationChannel channel = new NotificationChannel(channelId, channelId, NotificationManager.IMPORTANCE_HIGH);
channel.enableVibration(true); channel.enableVibration(true);
notificationManager.createNotificationChannel(channel); notificationManager.createNotificationChannel(channel);
CharSequence errorTxt = Phrase.from(context, R.string.databaseErrorGeneric)
.put(APP_NAME_KEY, R.string.app_name)
.format();
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId) NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_notification) .setSmallIcon(R.drawable.ic_notification)
.setColor(context.getResources().getColor(R.color.textsecure_primary)) .setColor(context.getResources().getColor(R.color.textsecure_primary))
.setCategory(NotificationCompat.CATEGORY_ERROR) .setCategory(NotificationCompat.CATEGORY_ERROR)
.setContentTitle(context.getString(R.string.ErrorNotifier_migration)) .setContentTitle(context.getString(R.string.errorDatabase))
.setContentText(context.getString(R.string.ErrorNotifier_migration_downgrade)) .setContentText(errorTxt)
.setAutoCancel(true); .setAutoCancel(true);
notificationManager.notify(5874, builder.build()); notificationManager.notify(5874, builder.build());

View File

@ -14,6 +14,8 @@ import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.recipients.Recipient; import org.session.libsession.utilities.recipients.Recipient;
import org.thoughtcrime.securesms.database.MediaDatabase; import org.thoughtcrime.securesms.database.MediaDatabase;
import org.thoughtcrime.securesms.dependencies.DatabaseComponent; import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.RelativeDay;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
@ -88,16 +90,18 @@ public class BucketedThreadMediaLoader extends AsyncTaskLoader<BucketedThreadMed
private final TimeBucket[] TIME_SECTIONS; private final TimeBucket[] TIME_SECTIONS;
public BucketedThreadMedia(@NonNull Context context) { public BucketedThreadMedia(@NonNull Context context) {
this.TODAY = new TimeBucket(context.getString(R.string.BucketedThreadMedia_Today), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -1), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, 1000)); String localisedTodayString = DateUtils.INSTANCE.getLocalisedRelativeDayString(RelativeDay.TODAY);
this.YESTERDAY = new TimeBucket(context.getString(R.string.BucketedThreadMedia_Yesterday), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -2), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -1)); String localisedYesterdayString = DateUtils.INSTANCE.getLocalisedRelativeDayString(RelativeDay.YESTERDAY);
this.THIS_WEEK = new TimeBucket(context.getString(R.string.BucketedThreadMedia_This_week), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -7), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -2));
this.THIS_MONTH = new TimeBucket(context.getString(R.string.BucketedThreadMedia_This_month), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -30), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -7)); this.TODAY = new TimeBucket(localisedTodayString, TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -1), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, 1000));
this.TIME_SECTIONS = new TimeBucket[]{TODAY, YESTERDAY, THIS_WEEK, THIS_MONTH}; this.YESTERDAY = new TimeBucket(localisedYesterdayString, TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -2), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -1));
this.OLDER = new MonthBuckets(); this.THIS_WEEK = new TimeBucket(context.getString(R.string.attachmentsThisWeek), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -7), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -2));
this.THIS_MONTH = new TimeBucket(context.getString(R.string.attachmentsThisMonth), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -30), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -7));
this.TIME_SECTIONS = new TimeBucket[] { TODAY, YESTERDAY, THIS_WEEK, THIS_MONTH };
this.OLDER = new MonthBuckets();
} }
public void add(MediaDatabase.MediaRecord mediaRecord) { public void add(MediaDatabase.MediaRecord mediaRecord) {
for (TimeBucket timeSection : TIME_SECTIONS) { for (TimeBucket timeSection : TIME_SECTIONS) {
if (timeSection.inRange(mediaRecord.getDate())) { if (timeSection.inRange(mediaRecord.getDate())) {

View File

@ -77,14 +77,6 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
@Override @Override
public SpannableString getDisplayBody(@NonNull Context context) { public SpannableString getDisplayBody(@NonNull Context context) {
if (MmsDatabase.Types.isFailedDecryptType(type)) {
return emphasisAdded(context.getString(R.string.MmsMessageRecord_bad_encrypted_mms_message));
} else if (MmsDatabase.Types.isDuplicateMessageType(type)) {
return emphasisAdded(context.getString(R.string.SmsMessageRecord_duplicate_message));
} else if (MmsDatabase.Types.isNoRemoteSessionType(type)) {
return emphasisAdded(context.getString(R.string.MmsMessageRecord_mms_message_encrypted_for_non_existing_session));
}
return super.getDisplayBody(context); return super.getDisplayBody(context);
} }
} }

View File

@ -57,15 +57,7 @@ public class SmsMessageRecord extends MessageRecord {
@Override @Override
public SpannableString getDisplayBody(@NonNull Context context) { public SpannableString getDisplayBody(@NonNull Context context) {
if (SmsDatabase.Types.isFailedDecryptType(type)) { return super.getDisplayBody(context);
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_bad_encrypted_message));
} else if (SmsDatabase.Types.isDuplicateMessageType(type)) {
return emphasisAdded(context.getString(R.string.SmsMessageRecord_duplicate_message));
} else if (SmsDatabase.Types.isNoRemoteSessionType(type)) {
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session));
} else {
return super.getDisplayBody(context);
}
} }
@Override @Override

View File

@ -17,21 +17,25 @@
*/ */
package org.thoughtcrime.securesms.database.model; package org.thoughtcrime.securesms.database.model;
import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
import static org.session.libsession.utilities.StringSubstitutionConstants.DISAPPEARING_MESSAGES_TYPE_KEY;
import static org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY;
import static org.session.libsession.utilities.StringSubstitutionConstants.TIME_KEY;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.text.Spannable; import android.text.Spannable;
import android.text.SpannableString; import android.text.SpannableString;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.style.StyleSpan; import android.text.style.StyleSpan;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.squareup.phrase.Phrase;
import org.session.libsession.utilities.ExpirationUtil; import org.session.libsession.utilities.ExpirationUtil;
import org.session.libsession.utilities.recipients.Recipient; import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.database.MmsSmsColumns; import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase;
import network.loki.messenger.R; import network.loki.messenger.R;
/** /**
@ -42,146 +46,179 @@ import network.loki.messenger.R;
*/ */
public class ThreadRecord extends DisplayRecord { public class ThreadRecord extends DisplayRecord {
private @Nullable final Uri snippetUri; private @Nullable final Uri snippetUri;
public @Nullable final MessageRecord lastMessage; public @Nullable final MessageRecord lastMessage;
private final long count; private final long count;
private final int unreadCount; private final int unreadCount;
private final int unreadMentionCount; private final int unreadMentionCount;
private final int distributionType; private final int distributionType;
private final boolean archived; private final boolean archived;
private final long expiresIn; private final long expiresIn;
private final long lastSeen; private final long lastSeen;
private final boolean pinned; private final boolean pinned;
private final int initialRecipientHash; private final int initialRecipientHash;
private final long dateSent;
public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri, public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri,
@Nullable MessageRecord lastMessage, @NonNull Recipient recipient, long date, long count, int unreadCount, @Nullable MessageRecord lastMessage, @NonNull Recipient recipient, long date, long count, int unreadCount,
int unreadMentionCount, long threadId, int deliveryReceiptCount, int status, int unreadMentionCount, long threadId, int deliveryReceiptCount, int status,
long snippetType, int distributionType, boolean archived, long expiresIn, long snippetType, int distributionType, boolean archived, long expiresIn,
long lastSeen, int readReceiptCount, boolean pinned) long lastSeen, int readReceiptCount, boolean pinned)
{ {
super(body, recipient, date, date, threadId, status, deliveryReceiptCount, snippetType, readReceiptCount); super(body, recipient, date, date, threadId, status, deliveryReceiptCount, snippetType, readReceiptCount);
this.snippetUri = snippetUri; this.snippetUri = snippetUri;
this.lastMessage = lastMessage; this.lastMessage = lastMessage;
this.count = count; this.count = count;
this.unreadCount = unreadCount; this.unreadCount = unreadCount;
this.unreadMentionCount = unreadMentionCount; this.unreadMentionCount = unreadMentionCount;
this.distributionType = distributionType; this.distributionType = distributionType;
this.archived = archived; this.archived = archived;
this.expiresIn = expiresIn; this.expiresIn = expiresIn;
this.lastSeen = lastSeen; this.lastSeen = lastSeen;
this.pinned = pinned; this.pinned = pinned;
this.initialRecipientHash = recipient.hashCode(); this.initialRecipientHash = recipient.hashCode();
} this.dateSent = date;
public @Nullable Uri getSnippetUri() {
return snippetUri;
}
@Override
public SpannableString getDisplayBody(@NonNull Context context) {
if (isGroupUpdateMessage()) {
return emphasisAdded(context.getString(R.string.ThreadRecord_group_updated));
} else if (isOpenGroupInvitation()) {
return emphasisAdded(context.getString(R.string.ThreadRecord_open_group_invitation));
} else if (SmsDatabase.Types.isFailedDecryptType(type)) {
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_bad_encrypted_message));
} else if (SmsDatabase.Types.isNoRemoteSessionType(type)) {
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session));
} else if (SmsDatabase.Types.isEndSessionType(type)) {
return emphasisAdded(context.getString(R.string.ThreadRecord_secure_session_reset));
} else if (MmsSmsColumns.Types.isLegacyType(type)) {
return emphasisAdded(context.getString(R.string.MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported));
} else if (MmsSmsColumns.Types.isDraftMessageType(type)) {
String draftText = context.getString(R.string.ThreadRecord_draft);
return emphasisAdded(draftText + " " + getBody(), 0, draftText.length());
} else if (SmsDatabase.Types.isOutgoingCall(type)) {
return emphasisAdded(context.getString(network.loki.messenger.R.string.ThreadRecord_called));
} else if (SmsDatabase.Types.isIncomingCall(type)) {
return emphasisAdded(context.getString(network.loki.messenger.R.string.ThreadRecord_called_you));
} else if (SmsDatabase.Types.isMissedCall(type)) {
return emphasisAdded(context.getString(network.loki.messenger.R.string.ThreadRecord_missed_call));
} else if (SmsDatabase.Types.isJoinedType(type)) {
return emphasisAdded(context.getString(R.string.ThreadRecord_s_is_on_signal, getRecipient().toShortString()));
} else if (SmsDatabase.Types.isExpirationTimerUpdate(type)) {
int seconds = (int) (getExpiresIn() / 1000);
if (seconds <= 0) {
return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_messages_disabled));
}
String time = ExpirationUtil.getExpirationDisplayValue(context, seconds);
return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_message_time_updated_to_s, time));
} else if (MmsSmsColumns.Types.isMediaSavedExtraction(type)) {
return emphasisAdded(context.getString(R.string.ThreadRecord_media_saved_by_s, getRecipient().toShortString()));
} else if (MmsSmsColumns.Types.isScreenshotExtraction(type)) {
return emphasisAdded(context.getString(R.string.ThreadRecord_s_took_a_screenshot, getRecipient().toShortString()));
} else if (SmsDatabase.Types.isIdentityUpdate(type)) {
if (getRecipient().isGroupRecipient()) return emphasisAdded(context.getString(R.string.ThreadRecord_safety_number_changed));
else return emphasisAdded(context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, getRecipient().toShortString()));
} else if (SmsDatabase.Types.isIdentityVerified(type)) {
return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_verified));
} else if (SmsDatabase.Types.isIdentityDefault(type)) {
return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_unverified));
} else if (MmsSmsColumns.Types.isMessageRequestResponse(type)) {
return emphasisAdded(context.getString(R.string.message_requests_accepted));
} else if (getCount() == 0) {
return new SpannableString(context.getString(R.string.ThreadRecord_empty_message));
} else {
if (TextUtils.isEmpty(getBody())) {
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_media_message)));
} else {
return new SpannableString(getBody());
}
} }
}
private SpannableString emphasisAdded(String sequence) { public @Nullable Uri getSnippetUri() {
return emphasisAdded(sequence, 0, sequence.length()); return snippetUri;
} }
private SpannableString emphasisAdded(String sequence, int start, int end) { private String getName() {
SpannableString spannable = new SpannableString(sequence); String name = getRecipient().getName();
spannable.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), if (name == null) {
start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); Log.w("ThreadRecord", "Got a null name - using: Unknown");
return spannable; name = "Unknown";
} }
return name;
}
public long getCount() { private String getDisappearingMsgExpiryTypeString(Context context) {
return count; MessageRecord lm = this.lastMessage;
} if (lm == null) {
Log.w("ThreadRecord", "Could not get last message to determine disappearing msg type.");
return "Unknown";
}
long expireStarted = lm.getExpireStarted();
public int getUnreadCount() { // Note: This works because expireStarted is 0 for messages which are 'Disappear after read'
return unreadCount; // while it's a touch higher than the sent timestamp for "Disappear after send". We could then
} // use `expireStarted == 0`, but that's not how it's done in UpdateMessageBuilder so to keep
// things the same I'll assume there's a reason for this and follow suit.
// Also: `this.lastMessage.getExpiresIn()` is available.
if (expireStarted >= dateSent) {
return context.getString(R.string.disappearingMessagesSent);
}
return context.getString(R.string.read);
}
public int getUnreadMentionCount() { @Override
return unreadMentionCount; public SpannableString getDisplayBody(@NonNull Context context) {
} if (isGroupUpdateMessage()) {
return emphasisAdded(context.getString(R.string.groupUpdated));
} else if (isOpenGroupInvitation()) {
return emphasisAdded(context.getString(R.string.communityInvitation));
} else if (MmsSmsColumns.Types.isLegacyType(type)) {
String txt = Phrase.from(context, R.string.messageErrorOld)
.put(APP_NAME_KEY, context.getString(R.string.app_name))
.format().toString();
return emphasisAdded(txt);
} else if (MmsSmsColumns.Types.isDraftMessageType(type)) {
String draftText = context.getString(R.string.draft);
return emphasisAdded(draftText + " " + getBody(), 0, draftText.length());
} else if (SmsDatabase.Types.isOutgoingCall(type)) {
String txt = Phrase.from(context, R.string.callsYouCalled)
.put(NAME_KEY, getName())
.format().toString();
return emphasisAdded(txt);
} else if (SmsDatabase.Types.isIncomingCall(type)) {
String txt = Phrase.from(context, R.string.callsCalledYou)
.put(NAME_KEY, getName())
.format().toString();
return emphasisAdded(txt);
} else if (SmsDatabase.Types.isMissedCall(type)) {
String txt = Phrase.from(context, R.string.callsMissedCallFrom)
.put(NAME_KEY, getName())
.format().toString();
return emphasisAdded(txt);
} else if (SmsDatabase.Types.isExpirationTimerUpdate(type)) {
int seconds = (int) (getExpiresIn() / 1000);
if (seconds <= 0) {
String txt = Phrase.from(context, R.string.disappearingMessagesTurnedOff)
.put(NAME_KEY, getName())
.format().toString();
return emphasisAdded(txt);
}
public long getDate() { // Implied that disappearing messages is enabled..
return getDateReceived(); String time = ExpirationUtil.getExpirationDisplayValue(context, seconds);
} String disappearAfterWhat = getDisappearingMsgExpiryTypeString(context); // Disappear after send or read?
String txt = Phrase.from(context, R.string.disappearingMessagesSet)
.put(NAME_KEY, getName())
.put(TIME_KEY, time)
.put(DISAPPEARING_MESSAGES_TYPE_KEY, disappearAfterWhat)
.format().toString();
return emphasisAdded(txt);
public boolean isArchived() { } else if (MmsSmsColumns.Types.isMediaSavedExtraction(type)) {
return archived; String txt = Phrase.from(context, R.string.attachmentsMediaSaved)
} .put(NAME_KEY, getName())
.format().toString();
return emphasisAdded(txt);
public int getDistributionType() { } else if (MmsSmsColumns.Types.isScreenshotExtraction(type)) {
return distributionType; String txt = Phrase.from(context, R.string.screenshotTaken)
} .put(NAME_KEY, getName())
.format().toString();
return emphasisAdded(txt);
public long getExpiresIn() { } else if (MmsSmsColumns.Types.isMessageRequestResponse(type)) {
return expiresIn; return emphasisAdded(context.getString(R.string.messageRequestsAccepted));
} } else if (getCount() == 0) {
return new SpannableString(context.getString(R.string.messageEmpty));
} else {
// This block hits when we receive a media message from an unaccepted contact - however,
// unaccepted contacts aren't allowed to send us media - so we'll return an empty string
// if it's JUST an image, or the body text that accompanied the image should any exist.
// We could return null here - but then we have to find all the usages of this
// `getDisplayBody` method and make sure it doesn't fall over if it has a null result.
if (TextUtils.isEmpty(getBody())) {
return new SpannableString("");
// Old behaviour was: return new SpannableString(emphasisAdded(context.getString(R.string.mediaMessage)));
} else {
return new SpannableString(getBody());
}
}
}
public long getLastSeen() { private SpannableString emphasisAdded(String sequence) {
return lastSeen; return emphasisAdded(sequence, 0, sequence.length());
} }
public boolean isPinned() { private SpannableString emphasisAdded(String sequence, int start, int end) {
return pinned; SpannableString spannable = new SpannableString(sequence);
} spannable.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC),
start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return spannable;
}
public int getInitialRecipientHash() { public long getCount() { return count; }
return initialRecipientHash;
} public int getUnreadCount() { return unreadCount; }
public int getUnreadMentionCount() { return unreadMentionCount; }
public long getDate() { return getDateReceived(); }
public boolean isArchived() { return archived; }
public int getDistributionType() { return distributionType; }
public long getExpiresIn() { return expiresIn; }
public long getLastSeen() { return lastSeen; }
public boolean isPinned() { return pinned; }
public int getInitialRecipientHash() { return initialRecipientHash; }
} }

View File

@ -15,8 +15,7 @@ enum class EmojiCategory(val priority: Int, val key: String, @AttrRes val icon:
PLACES(4, "Places", R.attr.emoji_category_places), PLACES(4, "Places", R.attr.emoji_category_places),
OBJECTS(5, "Objects", R.attr.emoji_category_objects), OBJECTS(5, "Objects", R.attr.emoji_category_objects),
SYMBOLS(6, "Symbols", R.attr.emoji_category_symbol), SYMBOLS(6, "Symbols", R.attr.emoji_category_symbol),
FLAGS(7, "Flags", R.attr.emoji_category_flags), FLAGS(7, "Flags", R.attr.emoji_category_flags);
EMOTICONS(8, "Emoticons", R.attr.emoji_category_emoticons);
@StringRes @StringRes
fun getCategoryLabel(): Int { fun getCategoryLabel(): Int {
@ -31,15 +30,14 @@ enum class EmojiCategory(val priority: Int, val key: String, @AttrRes val icon:
@StringRes @StringRes
fun getCategoryLabel(@AttrRes iconAttr: Int): Int { fun getCategoryLabel(@AttrRes iconAttr: Int): Int {
return when (iconAttr) { return when (iconAttr) {
R.attr.emoji_category_people -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__smileys_and_people R.attr.emoji_category_people -> R.string.emojiCategorySmileys
R.attr.emoji_category_nature -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__nature R.attr.emoji_category_nature -> R.string.emojiCategoryAnimals
R.attr.emoji_category_foods -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__food R.attr.emoji_category_foods -> R.string.emojiCategoryFood
R.attr.emoji_category_activity -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__activities R.attr.emoji_category_activity -> R.string.emojiCategoryActivities
R.attr.emoji_category_places -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__places R.attr.emoji_category_places -> R.string.emojiCategoryTravel
R.attr.emoji_category_objects -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__objects R.attr.emoji_category_objects -> R.string.emojiCategoryObjects
R.attr.emoji_category_symbol -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__symbols R.attr.emoji_category_symbol -> R.string.emojiCategorySymbols
R.attr.emoji_category_flags -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__flags R.attr.emoji_category_flags -> R.string.emojiCategoryFlags
R.attr.emoji_category_emoticons -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__emoticons
else -> throw AssertionError() else -> throw AssertionError()
} }
} }

View File

@ -110,10 +110,12 @@ class EmojiSource(
val parsedData: ParsedEmojiData = EmojiJsonParser.parse(it, ::getAssetsUri).getOrThrow() val parsedData: ParsedEmojiData = EmojiJsonParser.parse(it, ::getAssetsUri).getOrThrow()
return EmojiSource( return EmojiSource(
ScreenDensity.xhdpiRelativeDensityScaleFactor("xhdpi"), ScreenDensity.xhdpiRelativeDensityScaleFactor("xhdpi"),
parsedData.copy( parsedData.copy(
displayPages = parsedData.displayPages + PAGE_EMOTICONS, displayPages = parsedData.displayPages,
dataPages = parsedData.dataPages + PAGE_EMOTICONS dataPages = parsedData.dataPages
) )
) { uri: Uri -> EmojiPage.Asset(uri) } ) { uri: Uri -> EmojiPage.Asset(uri) }
} }
} }
@ -137,25 +139,3 @@ data class ObsoleteEmoji(val obsolete: String, val replaceWith: String)
data class EmojiMetrics(val rawHeight: Int, val rawWidth: Int, val perRow: Int) data class EmojiMetrics(val rawHeight: Int, val rawWidth: Int, val perRow: Int)
private fun getAssetsUri(name: String, format: String): Uri = Uri.parse("file:///android_asset/emoji/$name.$format") private fun getAssetsUri(name: String, format: String): Uri = Uri.parse("file:///android_asset/emoji/$name.$format")
private val PAGE_EMOTICONS: EmojiPageModel = StaticEmojiPageModel(
EmojiCategory.EMOTICONS,
arrayOf(
":-)", ";-)", "(-:", ":->", ":-D", "\\o/",
":-P", "B-)", ":-$", ":-*", "O:-)", "=-O",
"O_O", "O_o", "o_O", ":O", ":-!", ":-x",
":-|", ":-\\", ":-(", ":'(", ":-[", ">:-(",
"^.^", "^_^", "\\(\u02c6\u02da\u02c6)/",
"\u30fd(\u00b0\u25c7\u00b0 )\u30ce", "\u00af\\(\u00b0_o)/\u00af",
"\u00af\\_(\u30c4)_/\u00af", "(\u00ac_\u00ac)",
"(>_<)", "(\u2565\ufe4f\u2565)", "(\u261e\uff9f\u30ee\uff9f)\u261e",
"\u261c(\uff9f\u30ee\uff9f\u261c)", "\u261c(\u2312\u25bd\u2312)\u261e",
"(\u256f\u00b0\u25a1\u00b0)\u256f\ufe35", "\u253b\u2501\u253b",
"\u252c\u2500\u252c", "\u30ce(\u00b0\u2013\u00b0\u30ce)",
"(^._.^)\uff89", "\u0e05^\u2022\ufecc\u2022^\u0e05",
"\u0295\u2022\u1d25\u2022\u0294", "(\u2022_\u2022)",
" \u25a0-\u25a0\u00ac <(\u2022_\u2022) ", "(\u25a0_\u25a0\u00ac)",
"\u01aa(\u0693\u05f2)\u200e\u01aa\u200b\u200b"
),
null
)

View File

@ -19,6 +19,7 @@ import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayout;
import org.session.libsession.utilities.MediaTypes; import org.session.libsession.utilities.MediaTypes;
import org.session.libsession.utilities.NonTranslatableStringConstants;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity; import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.providers.BlobProvider;
@ -120,7 +121,7 @@ public class GiphyActivity extends PassphraseRequiredActionBarActivity
protected void onPostExecute(@Nullable Uri uri) { protected void onPostExecute(@Nullable Uri uri) {
if (uri == null) { if (uri == null) {
Toast.makeText(GiphyActivity.this, R.string.GiphyActivity_error_while_retrieving_full_resolution_gif, Toast.LENGTH_LONG).show(); Toast.makeText(GiphyActivity.this, R.string.errorUnknown, Toast.LENGTH_LONG).show();
} else if (viewHolder == finishingImage) { } else if (viewHolder == finishingImage) {
Intent intent = new Intent(); Intent intent = new Intent();
intent.setData(uri); intent.setData(uri);
@ -165,8 +166,8 @@ public class GiphyActivity extends PassphraseRequiredActionBarActivity
@Override @Override
public CharSequence getPageTitle(int position) { public CharSequence getPageTitle(int position) {
if (position == 0) return context.getString(R.string.GiphyFragmentPagerAdapter_gifs); if (position == 0) return NonTranslatableStringConstants.GIF;
else return context.getString(R.string.GiphyFragmentPagerAdapter_stickers); else return context.getString(R.string.stickers);
} }
} }

View File

@ -76,17 +76,20 @@ class CreateGroupFragment : Fragment() {
if (isLoading) return@setOnClickListener if (isLoading) return@setOnClickListener
val name = binding.nameEditText.text.trim() val name = binding.nameEditText.text.trim()
if (name.isEmpty()) { if (name.isEmpty()) {
return@setOnClickListener Toast.makeText(context, R.string.activity_create_closed_group_group_name_missing_error, Toast.LENGTH_LONG).show() return@setOnClickListener Toast.makeText(context, R.string.groupNameEnterPlease, Toast.LENGTH_LONG).show()
} }
// Limit the group name length if it exceeds the limit
if (name.length > resources.getInteger(R.integer.max_group_and_community_name_length_chars)) { if (name.length > resources.getInteger(R.integer.max_group_and_community_name_length_chars)) {
return@setOnClickListener Toast.makeText(context, R.string.activity_create_closed_group_group_name_too_long_error, Toast.LENGTH_LONG).show() return@setOnClickListener Toast.makeText(context, R.string.groupNameEnterShorter, Toast.LENGTH_LONG).show()
} }
val selectedMembers = adapter.selectedMembers val selectedMembers = adapter.selectedMembers
if (selectedMembers.isEmpty()) { if (selectedMembers.isEmpty()) {
return@setOnClickListener Toast.makeText(context, R.string.activity_create_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show() return@setOnClickListener Toast.makeText(context, R.string.groupCreateErrorNoMembers, Toast.LENGTH_LONG).show()
} }
if (selectedMembers.count() >= groupSizeLimit) { // Minus one because we're going to include self later if (selectedMembers.count() >= groupSizeLimit) { // Minus one because we're going to include self later
return@setOnClickListener Toast.makeText(context, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show() return@setOnClickListener Toast.makeText(context, R.string.groupAddMemberMaximum, Toast.LENGTH_LONG).show()
} }
val userPublicKey = TextSecurePreferences.getLocalNumber(requireContext())!! val userPublicKey = TextSecurePreferences.getLocalNumber(requireContext())!!
isLoading = true isLoading = true

View File

@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.groups
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.SpannableString
import android.text.style.StyleSpan
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
@ -16,7 +18,10 @@ import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader import androidx.loader.content.Loader
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import java.io.IOException
import javax.inject.Inject
import network.loki.messenger.R import network.loki.messenger.R
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.task import nl.komponents.kovenant.task
@ -26,6 +31,7 @@ import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.sending_receiving.groupSizeLimit import org.session.libsession.messaging.sending_receiving.groupSizeLimit
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.StringSubstitutionConstants.COUNT_KEY
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.ThemeUtil import org.session.libsession.utilities.ThemeUtil
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
@ -40,8 +46,6 @@ import org.thoughtcrime.securesms.groups.ClosedGroupManager.updateLegacyGroup
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import org.thoughtcrime.securesms.util.fadeIn import org.thoughtcrime.securesms.util.fadeIn
import org.thoughtcrime.securesms.util.fadeOut import org.thoughtcrime.securesms.util.fadeOut
import java.io.IOException
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() { class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
@ -107,17 +111,17 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
groupID = intent.getStringExtra(groupIDKey)!! groupID = intent.getStringExtra(groupIDKey)!!
val groupInfo = DatabaseComponent.get(this).groupDatabase().getGroup(groupID).get() val groupInfo = DatabaseComponent.get(this).groupDatabase().getGroup(groupID).get()
originalName = groupInfo.title originalName = groupInfo.title
isSelfAdmin = groupInfo.admins.any{ it.serialize() == TextSecurePreferences.getLocalNumber(this) } isSelfAdmin = groupInfo.admins.any { it.serialize() == TextSecurePreferences.getLocalNumber(this) }
name = originalName name = originalName
mainContentContainer = findViewById(R.id.mainContentContainer) mainContentContainer = findViewById(R.id.mainContentContainer)
cntGroupNameEdit = findViewById(R.id.cntGroupNameEdit) cntGroupNameEdit = findViewById(R.id.cntGroupNameEdit)
cntGroupNameDisplay = findViewById(R.id.cntGroupNameDisplay) cntGroupNameDisplay = findViewById(R.id.cntGroupNameDisplay)
edtGroupName = findViewById(R.id.edtGroupName) edtGroupName = findViewById(R.id.edtGroupName)
emptyStateContainer = findViewById(R.id.emptyStateContainer) emptyStateContainer = findViewById(R.id.emptyStateContainer)
lblGroupNameDisplay = findViewById(R.id.lblGroupNameDisplay) lblGroupNameDisplay = findViewById(R.id.lblGroupNameDisplay)
loaderContainer = findViewById(R.id.loaderContainer) loaderContainer = findViewById(R.id.loaderContainer)
findViewById<View>(R.id.addMembersClosedGroupButton).setOnClickListener { findViewById<View>(R.id.addMembersClosedGroupButton).setOnClickListener {
onAddMembersClick() onAddMembersClick()
@ -129,7 +133,19 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
} }
lblGroupNameDisplay.text = originalName lblGroupNameDisplay.text = originalName
cntGroupNameDisplay.setOnClickListener { isEditingName = true }
// Only allow admins to click on the name of closed groups to edit them..
if (isSelfAdmin) {
cntGroupNameDisplay.setOnClickListener { isEditingName = true }
}
else // ..and also hide the edit `drawableEnd` for non-admins.
{
// Note: compoundDrawables returns 4 drawables (drawablesStart/Top/End/Bottom) -
// so the `drawableEnd` component is at index 2, which we replace with null.
val cd = lblGroupNameDisplay.compoundDrawables
lblGroupNameDisplay.setCompoundDrawables(cd[0], cd[1], null, cd[3])
}
findViewById<View>(R.id.btnCancelGroupNameEdit).setOnClickListener { isEditingName = false } findViewById<View>(R.id.btnCancelGroupNameEdit).setOnClickListener { isEditingName = false }
findViewById<View>(R.id.btnSaveGroupNameEdit).setOnClickListener { saveName() } findViewById<View>(R.id.btnSaveGroupNameEdit).setOnClickListener { saveName() }
edtGroupName.setImeActionLabel(getString(R.string.save), EditorInfo.IME_ACTION_DONE) edtGroupName.setImeActionLabel(getString(R.string.save), EditorInfo.IME_ACTION_DONE)
@ -245,10 +261,10 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
private fun saveName() { private fun saveName() {
val name = edtGroupName.text.toString().trim() val name = edtGroupName.text.toString().trim()
if (name.isEmpty()) { if (name.isEmpty()) {
return Toast.makeText(this, R.string.activity_edit_closed_group_group_name_missing_error, Toast.LENGTH_SHORT).show() return Toast.makeText(this, R.string.groupNameEnterPlease, Toast.LENGTH_SHORT).show()
} }
if (name.length >= 64) { if (name.length >= 64) {
return Toast.makeText(this, R.string.activity_edit_closed_group_group_name_too_long_error, Toast.LENGTH_SHORT).show() return Toast.makeText(this, R.string.groupNameEnterShorter, Toast.LENGTH_SHORT).show()
} }
this.name = name this.name = name
lblGroupNameDisplay.text = name lblGroupNameDisplay.text = name
@ -283,20 +299,22 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
} }
if (members.isEmpty()) { if (members.isEmpty()) {
return Toast.makeText(this, R.string.activity_edit_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show() return Toast.makeText(this, R.string.groupCreateErrorNoMembers, Toast.LENGTH_LONG).show()
} }
val maxGroupMembers = if (isClosedGroup) groupSizeLimit else legacyGroupSizeLimit val maxGroupMembers = if (isClosedGroup) groupSizeLimit else legacyGroupSizeLimit
if (members.size >= maxGroupMembers) { if (members.size >= maxGroupMembers) {
return Toast.makeText(this, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show() return Toast.makeText(this, R.string.groupAddMemberMaximum, Toast.LENGTH_LONG).show()
} }
val userPublicKey = TextSecurePreferences.getLocalNumber(this)!! val userPublicKey = TextSecurePreferences.getLocalNumber(this)!!
val userAsRecipient = Recipient.from(this, Address.fromSerialized(userPublicKey), false) val userAsRecipient = Recipient.from(this, Address.fromSerialized(userPublicKey), false)
// There's presently no way in the UI to get into the state whereby you could remove yourself from the group when removing any other members
// (you can't unselect yourself - the only way to leave is to "Leave Group" from the menu) - but it's possible that this was not always
// the case - so we can leave this in as defensive code in-case something goes screwy.
if (!members.contains(userAsRecipient) && !members.map { it.address.toString() }.containsAll(originalMembers.minus(userPublicKey))) { if (!members.contains(userAsRecipient) && !members.map { it.address.toString() }.containsAll(originalMembers.minus(userPublicKey))) {
val message = "Can't leave while adding or removing other members." return Log.w("EditClosedGroup", "Can't leave group while adding or removing other members.")
return Toast.makeText(this@EditClosedGroupActivity, message, Toast.LENGTH_LONG).show()
} }
if (isClosedGroup) { if (isClosedGroup) {

View File

@ -12,6 +12,7 @@ import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -22,6 +23,7 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.OpenGroupUrlParser import org.session.libsession.utilities.OpenGroupUrlParser
import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.conversation.start.StartConversationDelegate import org.thoughtcrime.securesms.conversation.start.StartConversationDelegate
@ -47,6 +49,7 @@ class JoinCommunityFragment : Fragment() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding.backButton.setOnClickListener { delegate.onDialogBackPressed() } binding.backButton.setOnClickListener { delegate.onDialogBackPressed() }
binding.closeButton.setOnClickListener { delegate.onDialogClosePressed() } binding.closeButton.setOnClickListener { delegate.onDialogClosePressed() }
fun showLoader() { fun showLoader() {
binding.loader.visibility = View.VISIBLE binding.loader.visibility = View.VISIBLE
binding.loader.animate().setDuration(150).alpha(1.0f).start() binding.loader.animate().setDuration(150).alpha(1.0f).start()
@ -61,18 +64,23 @@ class JoinCommunityFragment : Fragment() {
} }
}) })
} }
fun joinCommunityIfPossible(url: String) { fun joinCommunityIfPossible(url: String) {
val openGroup = try { val openGroup = try {
OpenGroupUrlParser.parseUrl(url) OpenGroupUrlParser.parseUrl(url)
} catch (e: OpenGroupUrlParser.Error) { } catch (e: OpenGroupUrlParser.Error) {
when (e) { when (e) {
is OpenGroupUrlParser.Error.MalformedURL -> return Toast.makeText(activity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show() is OpenGroupUrlParser.Error.MalformedURL, OpenGroupUrlParser.Error.NoRoom -> {
is OpenGroupUrlParser.Error.InvalidPublicKey -> return Toast.makeText(activity, R.string.invalid_public_key, Toast.LENGTH_SHORT).show() return Toast.makeText(activity, context?.resources?.getString(R.string.communityJoinError), Toast.LENGTH_SHORT).show()
is OpenGroupUrlParser.Error.NoPublicKey -> return Toast.makeText(activity, R.string.invalid_public_key, Toast.LENGTH_SHORT).show() }
is OpenGroupUrlParser.Error.NoRoom -> return Toast.makeText(activity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show() is OpenGroupUrlParser.Error.InvalidPublicKey, OpenGroupUrlParser.Error.NoPublicKey -> {
return Toast.makeText(activity, R.string.communityEnterUrlErrorInvalidDescription, Toast.LENGTH_SHORT).show()
}
} }
} }
showLoader() showLoader()
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
try { try {
val sanitizedServer = openGroup.server.removeSuffix("/") val sanitizedServer = openGroup.server.removeSuffix("/")
@ -90,10 +98,11 @@ class JoinCommunityFragment : Fragment() {
delegate.onDialogClosePressed() delegate.onDialogClosePressed()
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("Loki", "Couldn't join open group.", e) Log.e("Loki", "Couldn't join community.", e)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
hideLoader() hideLoader()
Toast.makeText(activity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show() val txt = Phrase.from(context, R.string.groupErrorJoin).put(GROUP_NAME_KEY, url).format().toString()
Toast.makeText(activity, txt, Toast.LENGTH_SHORT).show()
} }
return@launch return@launch
} }
@ -107,8 +116,8 @@ class JoinCommunityFragment : Fragment() {
) )
val mediator = TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, pos -> val mediator = TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, pos ->
tab.text = when (pos) { tab.text = when (pos) {
0 -> getString(R.string.activity_join_public_chat_enter_community_url_tab_title) 0 -> getString(R.string.communityUrl)
1 -> getString(R.string.activity_join_public_chat_scan_qr_code_tab_title) 1 -> getString(R.string.qrScan)
else -> throw IllegalStateException() else -> throw IllegalStateException()
} }
} }

View File

@ -1,18 +1,20 @@
package org.thoughtcrime.securesms.groups package org.thoughtcrime.securesms.groups
import android.content.Context import android.content.Context
import android.widget.Toast
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import okhttp3.HttpUrl import com.squareup.phrase.Phrase
import java.util.concurrent.Executors
import network.loki.messenger.R
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.open_groups.GroupMemberRole
import org.session.libsession.messaging.open_groups.OpenGroup import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller
import org.session.libsession.utilities.StringSubstitutionConstants.COMMUNITY_NAME_KEY
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import java.util.concurrent.Executors
object OpenGroupManager { object OpenGroupManager {
private val executorService = Executors.newScheduledThreadPool(4) private val executorService = Executors.newScheduledThreadPool(4)
@ -111,35 +113,43 @@ object OpenGroupManager {
@WorkerThread @WorkerThread
fun delete(server: String, room: String, context: Context) { fun delete(server: String, room: String, context: Context) {
val storage = MessagingModuleConfiguration.shared.storage try {
val configFactory = MessagingModuleConfiguration.shared.configFactory val storage = MessagingModuleConfiguration.shared.storage
val threadDB = DatabaseComponent.get(context).threadDatabase() val configFactory = MessagingModuleConfiguration.shared.configFactory
val openGroupID = "${server.removeSuffix("/")}.$room" val threadDB = DatabaseComponent.get(context).threadDatabase()
val threadID = GroupManager.getOpenGroupThreadID(openGroupID, context) val openGroupID = "${server.removeSuffix("/")}.$room"
val recipient = threadDB.getRecipientForThreadId(threadID) ?: return val threadID = GroupManager.getOpenGroupThreadID(openGroupID, context)
threadDB.setThreadArchived(threadID) val recipient = threadDB.getRecipientForThreadId(threadID) ?: return
val groupID = recipient.address.serialize() threadDB.setThreadArchived(threadID)
// Stop the poller if needed val groupID = recipient.address.serialize()
val openGroups = storage.getAllOpenGroups().filter { it.value.server == server } // Stop the poller if needed
if (openGroups.isNotEmpty()) { val openGroups = storage.getAllOpenGroups().filter { it.value.server == server }
synchronized(pollUpdaterLock) { if (openGroups.isNotEmpty()) {
val poller = pollers[server] synchronized(pollUpdaterLock) {
poller?.stop() val poller = pollers[server]
pollers.remove(server) poller?.stop()
pollers.remove(server)
}
} }
configFactory.userGroups?.eraseCommunity(server, room)
configFactory.convoVolatile?.eraseCommunity(server, room)
// Delete
storage.removeLastDeletionServerID(room, server)
storage.removeLastMessageServerID(room, server)
storage.removeLastInboxMessageId(server)
storage.removeLastOutboxMessageId(server)
val lokiThreadDB = DatabaseComponent.get(context).lokiThreadDatabase()
lokiThreadDB.removeOpenGroupChat(threadID)
storage.deleteConversation(threadID) // Must be invoked on a background thread
GroupManager.deleteGroup(groupID, context) // Must be invoked on a background thread
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
}
catch (e: Exception) {
Log.e("Loki", "Failed to leave (delete) community", e)
val serverAndRoom = "$server.$room"
val txt = Phrase.from(context, R.string.communityLeaveError).put(COMMUNITY_NAME_KEY, serverAndRoom).format().toString()
Toast.makeText(context, txt, Toast.LENGTH_LONG).show()
} }
configFactory.userGroups?.eraseCommunity(server, room)
configFactory.convoVolatile?.eraseCommunity(server, room)
// Delete
storage.removeLastDeletionServerID(room, server)
storage.removeLastMessageServerID(room, server)
storage.removeLastInboxMessageId(server)
storage.removeLastOutboxMessageId(server)
val lokiThreadDB = DatabaseComponent.get(context).lokiThreadDatabase()
lokiThreadDB.removeOpenGroupChat(threadID)
storage.deleteConversation(threadID) // Must be invoked on a background thread
GroupManager.deleteGroup(groupID, context) // Must be invoked on a background thread
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
} }
@WorkerThread @WorkerThread

View File

@ -136,7 +136,7 @@ class ConversationView : LinearLayout {
} }
private fun getTitle(recipient: Recipient): String? = when { private fun getTitle(recipient: Recipient): String? = when {
recipient.isLocalNumber -> context.getString(R.string.note_to_self) recipient.isLocalNumber -> context.getString(R.string.noteToSelf)
else -> recipient.toShortString() // Internally uses the Contact API else -> recipient.toShortString() // Internally uses the Contact API
} }
@ -147,7 +147,7 @@ class ConversationView : LinearLayout {
private fun ThreadRecord.getSnippetPrefix(): CharSequence? = when { private fun ThreadRecord.getSnippetPrefix(): CharSequence? = when {
recipient.isLocalNumber || lastMessage?.isControlMessage == true -> null recipient.isLocalNumber || lastMessage?.isControlMessage == true -> null
lastMessage?.isOutgoing == true -> resources.getString(R.string.MessageRecord_you) lastMessage?.isOutgoing == true -> resources.getString(R.string.you)
else -> lastMessage?.individualRecipient?.toShortString() else -> lastMessage?.individualRecipient?.toShortString()
} }
// endregion // endregion

View File

@ -9,20 +9,25 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.squareup.phrase.Phrase
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.utilities.NonTranslatableStringConstants.WAVING_HAND_EMOJI
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.EMOJI_KEY
import org.thoughtcrime.securesms.ui.Divider import org.thoughtcrime.securesms.ui.Divider
import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.LocalDimensions
import org.thoughtcrime.securesms.ui.theme.LocalType
import org.thoughtcrime.securesms.ui.theme.PreviewTheme import org.thoughtcrime.securesms.ui.theme.PreviewTheme
import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider
import org.thoughtcrime.securesms.ui.theme.ThemeColors import org.thoughtcrime.securesms.ui.theme.ThemeColors
import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.theme.LocalType
@Composable @Composable
internal fun EmptyView(newAccount: Boolean) { internal fun EmptyView(newAccount: Boolean) {
@ -44,7 +49,13 @@ internal fun EmptyView(newAccount: Boolean) {
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
Text( Text(
stringResource(R.string.welcome_to_session), stringResource(R.string.onboardingBubbleWelcomeToSession).let { txt ->
val c = LocalContext.current
Phrase.from(txt)
.put(APP_NAME_KEY, c.getString(R.string.app_name))
.put(EMOJI_KEY, WAVING_HAND_EMOJI)
.format().toString()
},
style = LocalType.current.base, style = LocalType.current.base,
color = LocalColors.current.primary, color = LocalColors.current.primary,
textAlign = TextAlign.Center textAlign = TextAlign.Center

View File

@ -8,8 +8,11 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.format.DateUtils
import android.widget.Toast import android.widget.Toast
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.PluralsRes
import androidx.annotation.StringRes
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -17,6 +20,7 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@ -42,6 +46,9 @@ import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.ThreadUtils import org.session.libsignal.utilities.ThreadUtils
import org.session.libsignal.utilities.toHexString import org.session.libsignal.utilities.toHexString
import org.session.libsession.utilities.StringSubstitutionConstants.COUNT_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.conversation.start.StartConversationFragment import org.thoughtcrime.securesms.conversation.start.StartConversationFragment
@ -73,13 +80,22 @@ import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.ui.setThemedContent import org.thoughtcrime.securesms.ui.setThemedContent
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.IP2Country import org.thoughtcrime.securesms.util.IP2Country
import org.thoughtcrime.securesms.util.RelativeDay
import org.thoughtcrime.securesms.util.disableClipping import org.thoughtcrime.securesms.util.disableClipping
import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.show import org.thoughtcrime.securesms.util.show
import org.thoughtcrime.securesms.util.start import org.thoughtcrime.securesms.util.start
import java.io.IOException import java.io.IOException
import java.util.Calendar
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.abs
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
// Intent extra keys so we know where we came from
private const val NEW_ACCOUNT = "HomeActivity_NEW_ACCOUNT" private const val NEW_ACCOUNT = "HomeActivity_NEW_ACCOUNT"
private const val FROM_ONBOARDING = "HomeActivity_FROM_ONBOARDING" private const val FROM_ONBOARDING = "HomeActivity_FROM_ONBOARDING"
@ -88,6 +104,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
ConversationClickListener, ConversationClickListener,
GlobalSearchInputLayout.GlobalSearchInputLayoutListener { GlobalSearchInputLayout.GlobalSearchInputLayoutListener {
private val TAG = "HomeActivity"
private lateinit var binding: ActivityHomeBinding private lateinit var binding: ActivityHomeBinding
private lateinit var glide: RequestManager private lateinit var glide: RequestManager
@ -244,17 +262,17 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
globalSearchViewModel.result.map { result -> globalSearchViewModel.result.map { result ->
result.query to when { result.query to when {
result.query.isEmpty() -> buildList { result.query.isEmpty() -> buildList {
add(GlobalSearchAdapter.Model.Header(R.string.contacts)) add(GlobalSearchAdapter.Model.Header(R.string.contactContacts))
add(GlobalSearchAdapter.Model.SavedMessages(publicKey)) add(GlobalSearchAdapter.Model.SavedMessages(publicKey))
addAll(result.groupedContacts) addAll(result.groupedContacts)
} }
else -> buildList { else -> buildList {
result.contactAndGroupList.takeUnless { it.isEmpty() }?.let { result.contactAndGroupList.takeUnless { it.isEmpty() }?.let {
add(GlobalSearchAdapter.Model.Header(R.string.conversations)) add(GlobalSearchAdapter.Model.Header(R.string.sessionConversations))
addAll(it) addAll(it)
} }
result.messageResults.takeUnless { it.isEmpty() }?.let { result.messageResults.takeUnless { it.isEmpty() }?.let {
add(GlobalSearchAdapter.Model.Header(R.string.global_search_messages)) add(GlobalSearchAdapter.Model.Header(R.string.messages))
addAll(it) addAll(it)
} }
} }
@ -427,7 +445,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
val clip = ClipData.newPlainText("Account ID", thread.recipient.address.toString()) val clip = ClipData.newPlainText("Account ID", thread.recipient.address.toString())
val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
manager.setPrimaryClip(clip) manager.setPrimaryClip(clip)
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.copied, Toast.LENGTH_SHORT).show()
} }
else if (thread.recipient.isCommunityRecipient) { else if (thread.recipient.isCommunityRecipient) {
val threadId = threadDb.getThreadIdIfExistsFor(thread.recipient) val threadId = threadDb.getThreadIdIfExistsFor(thread.recipient)
@ -436,7 +454,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
val clip = ClipData.newPlainText("Community URL", openGroup.joinURL) val clip = ClipData.newPlainText("Community URL", openGroup.joinURL)
val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
manager.setPrimaryClip(clip) manager.setPrimaryClip(clip)
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.copied, Toast.LENGTH_SHORT).show()
} }
} }
bottomSheet.onBlockTapped = { bottomSheet.onBlockTapped = {
@ -482,9 +500,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
private fun blockConversation(thread: ThreadRecord) { private fun blockConversation(thread: ThreadRecord) {
showSessionDialog { showSessionDialog {
title(R.string.RecipientPreferenceActivity_block_this_contact_question) title(R.string.block)
text(R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact) text(Phrase.from(context, R.string.blockDescription)
button(R.string.RecipientPreferenceActivity_block) { .put(NAME_KEY, thread.recipient.name)
.format())
dangerButton(R.string.block, R.string.AccessibilityId_blockConfirm) {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
storage.setBlocked(listOf(thread.recipient), true) storage.setBlocked(listOf(thread.recipient), true)
@ -492,6 +512,9 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
binding.recyclerView.adapter!!.notifyDataSetChanged() binding.recyclerView.adapter!!.notifyDataSetChanged()
} }
} }
// Block confirmation toast added as per SS-64
val txt = Phrase.from(context, R.string.blockBlockedUser).put(NAME_KEY, thread.recipient.name).format().toString()
Toast.makeText(context, txt, Toast.LENGTH_LONG).show()
} }
cancelButton() cancelButton()
} }
@ -499,12 +522,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
private fun unblockConversation(thread: ThreadRecord) { private fun unblockConversation(thread: ThreadRecord) {
showSessionDialog { showSessionDialog {
title(R.string.RecipientPreferenceActivity_unblock_this_contact_question) title(R.string.blockUnblock)
text(R.string.RecipientPreferenceActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact) text(Phrase.from(context, R.string.blockUnblockName).put(NAME_KEY, thread.recipient.name).format())
button(R.string.RecipientPreferenceActivity_unblock) { dangerButton(R.string.blockUnblock, R.string.AccessibilityId_unblockConfirm) {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
storage.setBlocked(listOf(thread.recipient), false) storage.setBlocked(listOf(thread.recipient), false)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
binding.recyclerView.adapter!!.notifyDataSetChanged() binding.recyclerView.adapter!!.notifyDataSetChanged()
} }
@ -559,18 +581,42 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
private fun deleteConversation(thread: ThreadRecord) { private fun deleteConversation(thread: ThreadRecord) {
val threadID = thread.threadId val threadID = thread.threadId
val recipient = thread.recipient val recipient = thread.recipient
val message = if (recipient.isGroupRecipient) { val title: String
val message: CharSequence
if (recipient.isGroupRecipient) {
val group = groupDatabase.getGroup(recipient.address.toString()).orNull() val group = groupDatabase.getGroup(recipient.address.toString()).orNull()
// If you are an admin of this group you can delete it
if (group != null && group.admins.map { it.toString() }.contains(textSecurePreferences.getLocalNumber())) { if (group != null && group.admins.map { it.toString() }.contains(textSecurePreferences.getLocalNumber())) {
getString(R.string.admin_group_leave_warning) title = getString(R.string.groupDelete)
message = Phrase.from(this.applicationContext, R.string.groupDeleteDescription)
.put(GROUP_NAME_KEY, group.title)
.format()
} else { } else {
resources.getString(R.string.activity_home_leave_group_dialog_message) // Otherwise this is either a community, or it's a group you're not an admin of
title = if (recipient.isCommunityRecipient) getString(R.string.communityLeave) else getString(R.string.groupLeave)
message = Phrase.from(this.applicationContext, R.string.groupLeaveDescription)
.put(GROUP_NAME_KEY, group.title)
.format()
} }
} else { } else {
resources.getString(R.string.activity_home_delete_conversation_dialog_message) // If this is a 1-on-1 conversation
if (recipient.name != null) {
title = getString(R.string.conversationsDelete)
message = Phrase.from(this.applicationContext, R.string.conversationsDeleteDescription)
.put(NAME_KEY, recipient.name)
.format()
}
else {
// If not group-related and we don't have a recipient name then this must be our Note to Self conversation
title = getString(R.string.noteToSelf)
message = getString(R.string.clearMessagesNoteToSelfDescription)
}
} }
showSessionDialog { showSessionDialog {
title(title)
text(message) text(message)
button(R.string.yes) { button(R.string.yes) {
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
@ -583,7 +629,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
GroupUtil.doubleDecodeGroupID(recipient.address.toString()).toHexString() GroupUtil.doubleDecodeGroupID(recipient.address.toString()).toHexString()
.takeIf(DatabaseComponent.get(context).lokiAPIDatabase()::isClosedGroup) .takeIf(DatabaseComponent.get(context).lokiAPIDatabase()::isClosedGroup)
?.let { MessageSender.explicitLeave(it, false) } ?.let { MessageSender.explicitLeave(it, false) }
} catch (_: IOException) { } catch (ioe: IOException) {
Log.w(TAG, "Got an IOException while sending leave group message")
} }
} }
// Delete the conversation // Delete the conversation
@ -597,8 +644,9 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
} }
// Update the badge count // Update the badge count
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context) ApplicationContext.getInstance(context).messageNotifier.updateNotification(context)
// Notify the user // Notify the user
val toastMessage = if (recipient.isGroupRecipient) R.string.MessageRecord_left_group else R.string.activity_home_conversation_deleted_message val toastMessage = if (recipient.isGroupRecipient) R.string.groupMemberYouLeft else R.string.conversationsDeleted
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show() Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show()
} }
} }
@ -618,7 +666,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
private fun hideMessageRequests() { private fun hideMessageRequests() {
showSessionDialog { showSessionDialog {
text(getString(R.string.hide_message_requests)) text(getString(R.string.hide))
button(R.string.yes) { button(R.string.yes) {
textSecurePreferences.setHasHiddenMessageRequests() textSecurePreferences.setHasHiddenMessageRequests()
homeViewModel.tryReload() homeViewModel.tryReload()

View File

@ -27,9 +27,12 @@ import kotlinx.coroutines.withContext
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityPathBinding import network.loki.messenger.databinding.ActivityPathBinding
import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.utilities.NonTranslatableStringConstants.APP_NAME
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
import org.session.libsession.utilities.getColorFromAttr import org.session.libsession.utilities.getColorFromAttr
import org.session.libsignal.utilities.Snode import org.session.libsignal.utilities.Snode
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.ui.getSubbedString
import org.thoughtcrime.securesms.util.GlowViewUtilities import org.thoughtcrime.securesms.util.GlowViewUtilities
import org.thoughtcrime.securesms.util.IP2Country import org.thoughtcrime.securesms.util.IP2Country
import org.thoughtcrime.securesms.util.PathDotView import org.thoughtcrime.securesms.util.PathDotView
@ -49,7 +52,12 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
super.onCreate(savedInstanceState, isReady) super.onCreate(savedInstanceState, isReady)
binding = ActivityPathBinding.inflate(layoutInflater) binding = ActivityPathBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
supportActionBar!!.title = resources.getString(R.string.activity_path_title) supportActionBar!!.title = resources.getString(R.string.onionRoutingPath)
// Substitute "Session" into the path description. Note: This is a non-translatable string.
val txt = applicationContext.getSubbedString(R.string.onionRoutingPathDescription,APP_NAME_KEY to APP_NAME)
binding.pathDescription.text = txt
binding.pathRowsContainer.disableClipping() binding.pathRowsContainer.disableClipping()
binding.learnMoreButton.setOnClickListener { learnMore() } binding.learnMoreButton.setOnClickListener { learnMore() }
update(false) update(false)
@ -98,6 +106,7 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
private fun update(isAnimated: Boolean) { private fun update(isAnimated: Boolean) {
binding.pathRowsContainer.removeAllViews() binding.pathRowsContainer.removeAllViews()
if (OnionRequestAPI.paths.isNotEmpty()) { if (OnionRequestAPI.paths.isNotEmpty()) {
val path = OnionRequestAPI.paths.firstOrNull() ?: return finish() val path = OnionRequestAPI.paths.firstOrNull() ?: return finish()
val dotAnimationRepeatInterval = path.count().toLong() * 1000 + 1000 val dotAnimationRepeatInterval = path.count().toLong() * 1000 + 1000
@ -105,8 +114,8 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
val isGuardSnode = (OnionRequestAPI.guardSnodes.contains(snode)) val isGuardSnode = (OnionRequestAPI.guardSnodes.contains(snode))
getPathRow(snode, LineView.Location.Middle, index.toLong() * 1000 + 2000, dotAnimationRepeatInterval, isGuardSnode) getPathRow(snode, LineView.Location.Middle, index.toLong() * 1000 + 2000, dotAnimationRepeatInterval, isGuardSnode)
} }
val youRow = getPathRow(resources.getString(R.string.activity_path_device_row_title), null, LineView.Location.Top, 1000, dotAnimationRepeatInterval) val youRow = getPathRow(resources.getString(R.string.onionRoutingPath), null, LineView.Location.Top, 1000, dotAnimationRepeatInterval)
val destinationRow = getPathRow(resources.getString(R.string.activity_path_destination_row_title), null, LineView.Location.Bottom, path.count().toLong() * 1000 + 2000, dotAnimationRepeatInterval) val destinationRow = getPathRow(resources.getString(R.string.onionRoutingPathDestination), null, LineView.Location.Bottom, path.count().toLong() * 1000 + 2000, dotAnimationRepeatInterval)
val rows = listOf( youRow ) + pathRows + listOf( destinationRow ) val rows = listOf( youRow ) + pathRows + listOf( destinationRow )
for (row in rows) { for (row in rows) {
binding.pathRowsContainer.addView(row) binding.pathRowsContainer.addView(row)
@ -162,11 +171,11 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
} }
private fun getPathRow(snode: Snode, location: LineView.Location, dotAnimationStartDelay: Long, dotAnimationRepeatInterval: Long, isGuardSnode: Boolean): LinearLayout { private fun getPathRow(snode: Snode, location: LineView.Location, dotAnimationStartDelay: Long, dotAnimationRepeatInterval: Long, isGuardSnode: Boolean): LinearLayout {
val title = if (isGuardSnode) resources.getString(R.string.activity_path_guard_node_row_title) else resources.getString(R.string.activity_path_service_node_row_title) val title = if (isGuardSnode) resources.getString(R.string.onionRoutingPathEntryNode) else resources.getString(R.string.onionRoutingPathServiceNode)
val subtitle = if (IP2Country.isInitialized) { val subtitle = if (IP2Country.isInitialized) {
IP2Country.shared.countryNamesCache[snode.ip] ?: resources.getString(R.string.activity_path_resolving_progress) IP2Country.shared.countryNamesCache[snode.ip] ?: resources.getString(R.string.resolving)
} else { } else {
resources.getString(R.string.activity_path_resolving_progress) resources.getString(R.string.resolving)
} }
return getPathRow(title, subtitle, location, dotAnimationStartDelay, dotAnimationRepeatInterval) return getPathRow(title, subtitle, location, dotAnimationStartDelay, dotAnimationRepeatInterval)
} }
@ -179,7 +188,7 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(intent) startActivity(intent)
} catch (e: Exception) { } catch (e: Exception) {
Toast.makeText(this, R.string.invalid_url, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.communityEnterUrlErrorInvalid, Toast.LENGTH_SHORT).show()
} }
} }
// endregion // endregion
@ -250,13 +259,11 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
override fun onAttachedToWindow() { override fun onAttachedToWindow() {
super.onAttachedToWindow() super.onAttachedToWindow()
startAnimation() startAnimation()
} }
override fun onDetachedFromWindow() { override fun onDetachedFromWindow() {
super.onDetachedFromWindow() super.onDetachedFromWindow()
stopAnimation() stopAnimation()
} }

View File

@ -18,15 +18,15 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import network.loki.messenger.R import network.loki.messenger.R
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider
import org.thoughtcrime.securesms.ui.SessionShieldIcon import org.thoughtcrime.securesms.ui.SessionShieldIcon
import org.thoughtcrime.securesms.ui.theme.ThemeColors
import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.components.SlimPrimaryOutlineButton import org.thoughtcrime.securesms.ui.components.SlimPrimaryOutlineButton
import org.thoughtcrime.securesms.ui.contentDescription import org.thoughtcrime.securesms.ui.contentDescription
import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
import org.thoughtcrime.securesms.ui.theme.LocalType import org.thoughtcrime.securesms.ui.theme.LocalType
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider
import org.thoughtcrime.securesms.ui.theme.ThemeColors
@Composable @Composable
internal fun SeedReminder(startRecoveryPasswordActivity: () -> Unit) { internal fun SeedReminder(startRecoveryPasswordActivity: () -> Unit) {
@ -49,23 +49,23 @@ internal fun SeedReminder(startRecoveryPasswordActivity: () -> Unit) {
Column(Modifier.weight(1f)) { Column(Modifier.weight(1f)) {
Row { Row {
Text( Text(
stringResource(R.string.save_your_recovery_password), stringResource(R.string.recoveryPasswordBannerTitle),
style = LocalType.current.h8 style = LocalType.current.h8
) )
Spacer(Modifier.requiredWidth(LocalDimensions.current.xxsSpacing)) Spacer(Modifier.requiredWidth(LocalDimensions.current.xxsSpacing))
SessionShieldIcon() SessionShieldIcon()
} }
Text( Text(
stringResource(R.string.save_your_recovery_password_to_make_sure_you_don_t_lose_access_to_your_account), stringResource(R.string.recoveryPasswordBannerDescription),
style = LocalType.current.small style = LocalType.current.small
) )
} }
Spacer(Modifier.width(LocalDimensions.current.xsSpacing)) Spacer(Modifier.width(LocalDimensions.current.xsSpacing))
SlimPrimaryOutlineButton( SlimPrimaryOutlineButton(
text = stringResource(R.string.continue_2), text = stringResource(R.string.theContinue),
modifier = Modifier modifier = Modifier
.align(Alignment.CenterVertically) .align(Alignment.CenterVertically)
.contentDescription(R.string.AccessibilityId_reveal_recovery_phrase_button), .contentDescription(R.string.AccessibilityId_recoveryPasswordBanner),
onClick = startRecoveryPasswordActivity onClick = startRecoveryPasswordActivity
) )
} }
@ -78,6 +78,6 @@ private fun PreviewSeedReminder(
@PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors @PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors
) { ) {
PreviewTheme(colors) { PreviewTheme(colors) {
SeedReminder {} SeedReminder { }
} }
} }

View File

@ -100,7 +100,7 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Account ID", publicKey) val clip = ClipData.newPlainText("Account ID", publicKey)
clipboard.setPrimaryClip(clip) clipboard.setPrimaryClip(clip)
Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT) Toast.makeText(requireContext(), R.string.copied, Toast.LENGTH_SHORT)
.show() .show()
true true
} }

View File

@ -6,12 +6,14 @@ import android.text.SpannableStringBuilder
import android.text.style.StyleSpan import android.text.style.StyleSpan
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import java.util.Locale
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.truncateIdForDisplay import org.session.libsession.utilities.truncateIdForDisplay
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.ContentView import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.ContentView
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Contact as ContactModel
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.GroupConversation import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.GroupConversation
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Header import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Header
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Message import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Message
@ -19,9 +21,6 @@ import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.SavedMes
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.SubHeader import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.SubHeader
import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.SearchUtil import org.thoughtcrime.securesms.util.SearchUtil
import java.util.Locale
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Contact as ContactModel
class GlobalSearchDiff( class GlobalSearchDiff(
private val oldQuery: String?, private val oldQuery: String?,
@ -78,8 +77,8 @@ fun ContentView.bindQuery(query: String, model: GlobalSearchAdapter.Model) {
} }
binding.searchResultSubtitle.text = getHighlight(query, membersString) binding.searchResultSubtitle.text = getHighlight(query, membersString)
} }
is Header, // do nothing for header is Header, // do nothing for header
is SubHeader, // do nothing for subheader is SubHeader, // do nothing for subheader
is SavedMessages -> Unit // do nothing for saved messages (displays note to self) is SavedMessages -> Unit // do nothing for saved messages (displays note to self)
} }
} }
@ -112,7 +111,7 @@ fun ContentView.bindModel(query: String?, model: ContactModel) = binding.run {
searchResultSubtitle.text = null searchResultSubtitle.text = null
val recipient = Recipient.from(root.context, Address.fromSerialized(model.contact.accountID), false) val recipient = Recipient.from(root.context, Address.fromSerialized(model.contact.accountID), false)
searchResultProfilePicture.update(recipient) searchResultProfilePicture.update(recipient)
val nameString = if (model.isSelf) root.context.getString(R.string.note_to_self) val nameString = if (model.isSelf) root.context.getString(R.string.noteToSelf)
else model.contact.getSearchName() else model.contact.getSearchName()
searchResultTitle.text = getHighlight(query, nameString) searchResultTitle.text = getHighlight(query, nameString)
} }
@ -120,7 +119,7 @@ fun ContentView.bindModel(query: String?, model: ContactModel) = binding.run {
fun ContentView.bindModel(model: SavedMessages) { fun ContentView.bindModel(model: SavedMessages) {
binding.searchResultSubtitle.isVisible = false binding.searchResultSubtitle.isVisible = false
binding.searchResultTimestamp.isVisible = false binding.searchResultTimestamp.isVisible = false
binding.searchResultTitle.setText(R.string.note_to_self) binding.searchResultTitle.setText(R.string.noteToSelf)
binding.searchResultProfilePicture.update(Address.fromSerialized(model.currentUserPublicKey)) binding.searchResultProfilePicture.update(Address.fromSerialized(model.currentUserPublicKey))
binding.searchResultProfilePicture.isVisible = true binding.searchResultProfilePicture.isVisible = true
} }
@ -128,11 +127,13 @@ fun ContentView.bindModel(model: SavedMessages) {
fun ContentView.bindModel(query: String?, model: Message) = binding.apply { fun ContentView.bindModel(query: String?, model: Message) = binding.apply {
searchResultProfilePicture.isVisible = true searchResultProfilePicture.isVisible = true
searchResultTimestamp.isVisible = true searchResultTimestamp.isVisible = true
// val hasUnreads = model.unread > 0 // val hasUnreads = model.unread > 0
// unreadCountIndicator.isVisible = hasUnreads // unreadCountIndicator.isVisible = hasUnreads
// if (hasUnreads) { // if (hasUnreads) {
// unreadCountTextView.text = model.unread.toString() // unreadCountTextView.text = model.unread.toString()
// } // }
searchResultTimestamp.text = DateUtils.getDisplayFormattedTimeSpanString(root.context, Locale.getDefault(), model.messageResult.sentTimestampMs) searchResultTimestamp.text = DateUtils.getDisplayFormattedTimeSpanString(root.context, Locale.getDefault(), model.messageResult.sentTimestampMs)
searchResultProfilePicture.update(model.messageResult.conversationRecipient) searchResultProfilePicture.update(model.messageResult.conversationRecipient)
val textSpannable = SpannableStringBuilder() val textSpannable = SpannableStringBuilder()
@ -146,7 +147,7 @@ fun ContentView.bindModel(query: String?, model: Message) = binding.apply {
model.messageResult.bodySnippet model.messageResult.bodySnippet
)) ))
searchResultSubtitle.text = textSpannable searchResultSubtitle.text = textSpannable
searchResultTitle.text = if (model.isSelf) root.context.getString(R.string.note_to_self) searchResultTitle.text = if (model.isSelf) root.context.getString(R.string.noteToSelf)
else model.messageResult.conversationRecipient.getSearchName() else model.messageResult.conversationRecipient.getSearchName()
searchResultSubtitle.isVisible = true searchResultSubtitle.isVisible = true
} }

View File

@ -1,216 +1,233 @@
package org.thoughtcrime.securesms.linkpreview; package org.thoughtcrime.securesms.linkpreview;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.os.Build;
import android.text.Html; import android.text.Html;
import android.text.SpannableString; import android.text.SpannableString;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.style.URLSpan; import android.text.style.URLSpan;
import android.text.util.Linkify; import android.text.util.Linkify;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import java.text.ParseException;
import org.thoughtcrime.securesms.util.DateUtils; import java.text.SimpleDateFormat;
import org.session.libsignal.utilities.guava.Optional;
import org.session.libsession.utilities.Util;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import okhttp3.HttpUrl; import okhttp3.HttpUrl;
import org.session.libsession.utilities.Util;
import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.guava.Optional;
public final class LinkPreviewUtil { public final class LinkPreviewUtil {
private static final Pattern DOMAIN_PATTERN = Pattern.compile("^(https?://)?([^/]+).*$", Pattern.CASE_INSENSITIVE); private static final Pattern DOMAIN_PATTERN = Pattern.compile("^(https?://)?([^/]+).*$", Pattern.CASE_INSENSITIVE);
private static final Pattern ALL_ASCII_PATTERN = Pattern.compile("^[\\x00-\\x7F]*$", Pattern.CASE_INSENSITIVE); private static final Pattern ALL_ASCII_PATTERN = Pattern.compile("^[\\x00-\\x7F]*$", Pattern.CASE_INSENSITIVE);
private static final Pattern ALL_NON_ASCII_PATTERN = Pattern.compile("^[^\\x00-\\x7F]*$", Pattern.CASE_INSENSITIVE); private static final Pattern ALL_NON_ASCII_PATTERN = Pattern.compile("^[^\\x00-\\x7F]*$", Pattern.CASE_INSENSITIVE);
private static final Pattern OPEN_GRAPH_TAG_PATTERN = Pattern.compile("<\\s*meta[^>]*property\\s*=\\s*\"\\s*og:([^\"]+)\"[^>]*/?\\s*>", Pattern.CASE_INSENSITIVE); private static final Pattern OPEN_GRAPH_TAG_PATTERN = Pattern.compile("<\\s*meta[^>]*property\\s*=\\s*\"\\s*og:([^\"]+)\"[^>]*/?\\s*>", Pattern.CASE_INSENSITIVE);
private static final Pattern ARTICLE_TAG_PATTERN = Pattern.compile("<\\s*meta[^>]*property\\s*=\\s*\"\\s*article:([^\"]+)\"[^>]*/?\\s*>", Pattern.CASE_INSENSITIVE); private static final Pattern ARTICLE_TAG_PATTERN = Pattern.compile("<\\s*meta[^>]*property\\s*=\\s*\"\\s*article:([^\"]+)\"[^>]*/?\\s*>", Pattern.CASE_INSENSITIVE);
private static final Pattern OPEN_GRAPH_CONTENT_PATTERN = Pattern.compile("content\\s*=\\s*\"([^\"]*)\"", Pattern.CASE_INSENSITIVE); private static final Pattern OPEN_GRAPH_CONTENT_PATTERN = Pattern.compile("content\\s*=\\s*\"([^\"]*)\"", Pattern.CASE_INSENSITIVE);
private static final Pattern TITLE_PATTERN = Pattern.compile("<\\s*title[^>]*>(.*)<\\s*/title[^>]*>", Pattern.CASE_INSENSITIVE); private static final Pattern TITLE_PATTERN = Pattern.compile("<\\s*title[^>]*>(.*)<\\s*/title[^>]*>", Pattern.CASE_INSENSITIVE);
private static final Pattern FAVICON_PATTERN = Pattern.compile("<\\s*link[^>]*rel\\s*=\\s*\".*icon.*\"[^>]*>", Pattern.CASE_INSENSITIVE); private static final Pattern FAVICON_PATTERN = Pattern.compile("<\\s*link[^>]*rel\\s*=\\s*\".*icon.*\"[^>]*>", Pattern.CASE_INSENSITIVE);
private static final Pattern FAVICON_HREF_PATTERN = Pattern.compile("href\\s*=\\s*\"([^\"]*)\"", Pattern.CASE_INSENSITIVE); private static final Pattern FAVICON_HREF_PATTERN = Pattern.compile("href\\s*=\\s*\"([^\"]*)\"", Pattern.CASE_INSENSITIVE);
/** /**
* @return All whitelisted URLs in the source text. * @return All whitelisted URLs in the source text.
*/ */
public static @NonNull List<Link> findWhitelistedUrls(@NonNull String text) { public static @NonNull List<Link> findWhitelistedUrls(@NonNull String text) {
SpannableString spannable = new SpannableString(text); SpannableString spannable = new SpannableString(text);
boolean found = Linkify.addLinks(spannable, Linkify.WEB_URLS); boolean found = Linkify.addLinks(spannable, Linkify.WEB_URLS);
if (!found) { if (!found) {
return Collections.emptyList(); return Collections.emptyList();
}
return Stream.of(spannable.getSpans(0, spannable.length(), URLSpan.class))
.map(span -> new Link(span.getURL(), spannable.getSpanStart(span)))
.filter(link -> isValidLinkUrl(link.getUrl()))
.toList();
}
/**
* @return True if the host is valid.
*/
public static boolean isValidLinkUrl(@Nullable String linkUrl) {
if (linkUrl == null) return false;
HttpUrl url = HttpUrl.parse(linkUrl);
return url != null &&
!TextUtils.isEmpty(url.scheme()) &&
"https".equals(url.scheme()) &&
isLegalUrl(linkUrl);
}
/**
* @return True if the top-level domain is valid.
*/
public static boolean isValidMediaUrl(@Nullable String mediaUrl) {
if (mediaUrl == null) return false;
HttpUrl url = HttpUrl.parse(mediaUrl);
return url != null &&
!TextUtils.isEmpty(url.scheme()) &&
"https".equals(url.scheme()) &&
isLegalUrl(mediaUrl);
}
public static boolean isLegalUrl(@NonNull String url) {
Matcher matcher = DOMAIN_PATTERN.matcher(url);
if (matcher.matches()) {
String domain = matcher.group(2);
String cleanedDomain = domain.replaceAll("\\.", "");
return ALL_ASCII_PATTERN.matcher(cleanedDomain).matches() ||
ALL_NON_ASCII_PATTERN.matcher(cleanedDomain).matches();
} else {
return false;
}
}
public static boolean isValidMimeType(@NonNull String url) {
String[] validMimeType = {".jpg", ".png", ".gif", ".jpeg"};
if (url.contains(".")) {
for (String mimeType : validMimeType) {
if (url.contains(mimeType)) {
return true;
} }
}
return false;
}
return true;
}
public static @NonNull OpenGraph parseOpenGraphFields(@Nullable String html) { return Stream.of(spannable.getSpans(0, spannable.length(), URLSpan.class))
return parseOpenGraphFields(html, text -> Html.fromHtml(text).toString()); .map(span -> new Link(span.getURL(), spannable.getSpanStart(span)))
} .filter(link -> isValidLinkUrl(link.getUrl()))
.toList();
static @NonNull OpenGraph parseOpenGraphFields(@Nullable String html, @NonNull HtmlDecoder htmlDecoder) {
if (html == null) {
return new OpenGraph(Collections.emptyMap(), null, null);
} }
Map<String, String> openGraphTags = new HashMap<>(); /**
Matcher openGraphMatcher = OPEN_GRAPH_TAG_PATTERN.matcher(html); * @return True if the host is valid.
*/
public static boolean isValidLinkUrl(@Nullable String linkUrl) {
if (linkUrl == null) return false;
while (openGraphMatcher.find()) { HttpUrl url = HttpUrl.parse(linkUrl);
String tag = openGraphMatcher.group(); return url != null &&
String property = openGraphMatcher.groupCount() > 0 ? openGraphMatcher.group(1) : null; !TextUtils.isEmpty(url.scheme()) &&
"https".equals(url.scheme()) &&
isLegalUrl(linkUrl);
}
if (property != null) { /**
Matcher contentMatcher = OPEN_GRAPH_CONTENT_PATTERN.matcher(tag); * @return True if the top-level domain is valid.
if (contentMatcher.find() && contentMatcher.groupCount() > 0) { */
String content = htmlDecoder.fromEncoded(contentMatcher.group(1)); public static boolean isValidMediaUrl(@Nullable String mediaUrl) {
openGraphTags.put(property.toLowerCase(), content); if (mediaUrl == null) return false;
HttpUrl url = HttpUrl.parse(mediaUrl);
return url != null &&
!TextUtils.isEmpty(url.scheme()) &&
"https".equals(url.scheme()) &&
isLegalUrl(mediaUrl);
}
public static boolean isLegalUrl(@NonNull String url) {
Matcher matcher = DOMAIN_PATTERN.matcher(url);
if (matcher.matches()) {
String domain = matcher.group(2);
String cleanedDomain = domain.replaceAll("\\.", "");
return ALL_ASCII_PATTERN.matcher(cleanedDomain).matches() ||
ALL_NON_ASCII_PATTERN.matcher(cleanedDomain).matches();
} else {
return false;
} }
}
} }
Matcher articleMatcher = ARTICLE_TAG_PATTERN.matcher(html); public static boolean isValidMimeType(@NonNull String url) {
String[] validMimeType = {".jpg", ".png", ".gif", ".jpeg"};
while (articleMatcher.find()) { if (url.contains(".")) {
String tag = articleMatcher.group(); for (String mimeType : validMimeType) {
String property = articleMatcher.groupCount() > 0 ? articleMatcher.group(1) : null; if (url.contains(mimeType)) {
return true;
if (property != null) { }
Matcher contentMatcher = OPEN_GRAPH_CONTENT_PATTERN.matcher(tag); }
if (contentMatcher.find() && contentMatcher.groupCount() > 0) { return false;
String content = htmlDecoder.fromEncoded(contentMatcher.group(1));
openGraphTags.put(property.toLowerCase(), content);
} }
} return true;
} }
String htmlTitle = ""; public static @NonNull OpenGraph parseOpenGraphFields(@Nullable String html) {
String faviconUrl = ""; return parseOpenGraphFields(html, text -> Html.fromHtml(text).toString());
Matcher titleMatcher = TITLE_PATTERN.matcher(html);
if (titleMatcher.find() && titleMatcher.groupCount() > 0) {
htmlTitle = htmlDecoder.fromEncoded(titleMatcher.group(1));
} }
Matcher faviconMatcher = FAVICON_PATTERN.matcher(html); static @NonNull OpenGraph parseOpenGraphFields(@Nullable String html, @NonNull HtmlDecoder htmlDecoder) {
if (faviconMatcher.find()) { if (html == null) {
Matcher faviconHrefMatcher = FAVICON_HREF_PATTERN.matcher(faviconMatcher.group()); return new OpenGraph(Collections.emptyMap(), null, null);
if (faviconHrefMatcher.find() && faviconHrefMatcher.groupCount() > 0) { }
faviconUrl = faviconHrefMatcher.group(1);
} Map<String, String> openGraphTags = new HashMap<>();
Matcher openGraphMatcher = OPEN_GRAPH_TAG_PATTERN.matcher(html);
while (openGraphMatcher.find()) {
String tag = openGraphMatcher.group();
String property = openGraphMatcher.groupCount() > 0 ? openGraphMatcher.group(1) : null;
if (property != null) {
Matcher contentMatcher = OPEN_GRAPH_CONTENT_PATTERN.matcher(tag);
if (contentMatcher.find() && contentMatcher.groupCount() > 0) {
String content = htmlDecoder.fromEncoded(contentMatcher.group(1));
openGraphTags.put(property.toLowerCase(), content);
}
}
}
Matcher articleMatcher = ARTICLE_TAG_PATTERN.matcher(html);
while (articleMatcher.find()) {
String tag = articleMatcher.group();
String property = articleMatcher.groupCount() > 0 ? articleMatcher.group(1) : null;
if (property != null) {
Matcher contentMatcher = OPEN_GRAPH_CONTENT_PATTERN.matcher(tag);
if (contentMatcher.find() && contentMatcher.groupCount() > 0) {
String content = htmlDecoder.fromEncoded(contentMatcher.group(1));
openGraphTags.put(property.toLowerCase(), content);
}
}
}
String htmlTitle = "";
String faviconUrl = "";
Matcher titleMatcher = TITLE_PATTERN.matcher(html);
if (titleMatcher.find() && titleMatcher.groupCount() > 0) {
htmlTitle = htmlDecoder.fromEncoded(titleMatcher.group(1));
}
Matcher faviconMatcher = FAVICON_PATTERN.matcher(html);
if (faviconMatcher.find()) {
Matcher faviconHrefMatcher = FAVICON_HREF_PATTERN.matcher(faviconMatcher.group());
if (faviconHrefMatcher.find() && faviconHrefMatcher.groupCount() > 0) {
faviconUrl = faviconHrefMatcher.group(1);
}
}
return new OpenGraph(openGraphTags, htmlTitle, faviconUrl);
} }
return new OpenGraph(openGraphTags, htmlTitle, faviconUrl); public static final class OpenGraph {
}
public static final class OpenGraph { private final Map<String, String> values;
private final Map<String, String> values; private final @Nullable String htmlTitle;
private final @Nullable String faviconUrl;
private final @Nullable String htmlTitle; private static final String KEY_TITLE = "title";
private final @Nullable String faviconUrl; private static final String KEY_DESCRIPTION_URL = "description";
private static final String KEY_IMAGE_URL = "image";
private static final String KEY_PUBLISHED_TIME_1 = "published_time";
private static final String KEY_PUBLISHED_TIME_2 = "article:published_time";
private static final String KEY_MODIFIED_TIME_1 = "modified_time";
private static final String KEY_MODIFIED_TIME_2 = "article:modified_time";
private static final String KEY_TITLE = "title"; public OpenGraph(@NonNull Map<String, String> values, @Nullable String htmlTitle, @Nullable String faviconUrl) {
private static final String KEY_DESCRIPTION_URL = "description"; this.values = values;
private static final String KEY_IMAGE_URL = "image"; this.htmlTitle = htmlTitle;
private static final String KEY_PUBLISHED_TIME_1 = "published_time"; this.faviconUrl = faviconUrl;
private static final String KEY_PUBLISHED_TIME_2 = "article:published_time"; }
private static final String KEY_MODIFIED_TIME_1 = "modified_time";
private static final String KEY_MODIFIED_TIME_2 = "article:modified_time";
public OpenGraph(@NonNull Map<String, String> values, @Nullable String htmlTitle, @Nullable String faviconUrl) { public @NonNull Optional<String> getTitle() {
this.values = values; return Optional.of(Util.getFirstNonEmpty(values.get(KEY_TITLE), htmlTitle));
this.htmlTitle = htmlTitle; }
this.faviconUrl = faviconUrl;
public @NonNull Optional<String> getImageUrl() {
return Optional.of(Util.getFirstNonEmpty(values.get(KEY_IMAGE_URL), faviconUrl));
}
private static long parseISO8601(String date) {
if (date == null || date.isEmpty()) { return -1L; }
SimpleDateFormat format;
if (Build.VERSION.SDK_INT >= 24) {
format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", Locale.getDefault());
} else {
format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault());
}
try {
return format.parse(date).getTime();
} catch (ParseException pe) {
Log.w("OpenGraph", "Failed to parse date.", pe);
return -1L;
}
}
@SuppressLint("ObsoleteSdkInt")
public long getDate() {
return Stream.of(values.get(KEY_PUBLISHED_TIME_1),
values.get(KEY_PUBLISHED_TIME_2),
values.get(KEY_MODIFIED_TIME_1),
values.get(KEY_MODIFIED_TIME_2))
.map(OpenGraph::parseISO8601)
.filter(time -> time > 0)
.findFirst()
.orElse(0L);
}
} }
public @NonNull Optional<String> getTitle() { public interface HtmlDecoder {
return Optional.of(Util.getFirstNonEmpty(values.get(KEY_TITLE), htmlTitle)); @NonNull String fromEncoded(@NonNull String html);
} }
public @NonNull Optional<String> getImageUrl() { }
return Optional.of(Util.getFirstNonEmpty(values.get(KEY_IMAGE_URL), faviconUrl));
}
@SuppressLint("ObsoleteSdkInt")
public long getDate() {
return Stream.of(values.get(KEY_PUBLISHED_TIME_1),
values.get(KEY_PUBLISHED_TIME_2),
values.get(KEY_MODIFIED_TIME_1),
values.get(KEY_MODIFIED_TIME_2))
.map(DateUtils::parseIso8601)
.filter(time -> time > 0)
.findFirst()
.orElse(0L);
}
}
public interface HtmlDecoder {
@NonNull String fromEncoded(@NonNull String html);
}
}

View File

@ -44,7 +44,7 @@ fun DocumentsPage(
content.isEmpty() -> { content.isEmpty() -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text( Text(
text = stringResource(R.string.media_overview_documents_fragment__no_documents_found), text = stringResource(R.string.attachmentsFilesEmpty),
style = LocalType.current.base, style = LocalType.current.base,
color = LocalColors.current.text color = LocalColors.current.text
) )

View File

@ -35,10 +35,10 @@ class FixedTimeBuckets(
@StringRes @StringRes
fun getBucketText(time: ZonedDateTime): Int? { fun getBucketText(time: ZonedDateTime): Int? {
return when { return when {
time >= startOfToday -> R.string.BucketedThreadMedia_Today time >= startOfToday -> R.string.BucketedThreadMedia_Today
time >= startOfYesterday -> R.string.BucketedThreadMedia_Yesterday time >= startOfYesterday -> R.string.BucketedThreadMedia_Yesterday
time >= startOfThisWeek -> R.string.BucketedThreadMedia_This_week time >= startOfThisWeek -> R.string.attachmentsThisWeek
time >= startOfThisMonth -> R.string.BucketedThreadMedia_This_month time >= startOfThisMonth -> R.string.attachmentsThisMonth
else -> null else -> null
} }
} }

View File

@ -34,6 +34,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -68,7 +69,7 @@ fun MediaOverviewScreen(
} else { } else {
Toast.makeText( Toast.makeText(
context, context,
R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, R.string.cameraGrantAccessDenied,
Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
} }
@ -101,31 +102,18 @@ fun MediaOverviewScreen(
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
Toast.makeText( Toast.makeText(
context, context,
R.string.ConversationItem_unable_to_open_media, R.string.attachmentsErrorOpen,
Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
} }
} }
is MediaOverviewEvent.ShowSaveAttachmentError -> { is MediaOverviewEvent.ShowSaveAttachmentError -> {
val message = context.resources.getQuantityText( Toast.makeText(context, R.string.attachmentsSaveError, Toast.LENGTH_LONG).show()
R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card,
event.errorCount
)
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
} }
is MediaOverviewEvent.ShowSaveAttachmentSuccess -> { is MediaOverviewEvent.ShowSaveAttachmentSuccess -> {
val message = if (event.directory.isNotBlank()) { Toast.makeText(context, R.string.saved, Toast.LENGTH_LONG).show()
context.resources.getString(
R.string.SaveAttachmentTask_saved_to,
event.directory
)
} else {
context.resources.getString(R.string.SaveAttachmentTask_saved)
}
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
} }
} }
} }
@ -241,15 +229,11 @@ private fun SaveAttachmentWarningDialog(
val context = LocalContext.current val context = LocalContext.current
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = context.getString(R.string.ConversationFragment_save_to_sd_card), title = context.getString(R.string.warning),
text = context.resources.getQuantityString( text = context.resources.getString(R.string.attachmentsWarning),
R.plurals.ConversationFragment_saving_n_media_to_storage_warning,
numSelected,
numSelected
),
buttons = listOf( buttons = listOf(
DialogButtonModel(GetString(R.string.save), onClick = onAccepted), DialogButtonModel(GetString(R.string.save), GetString(R.string.AccessibilityId_save), color = LocalColors.current.danger, onClick = onAccepted),
DialogButtonModel(GetString(android.R.string.cancel), dismissOnClick = true) DialogButtonModel(GetString(android.R.string.cancel), GetString(R.string.AccessibilityId_cancel), dismissOnClick = true)
) )
) )
} }
@ -305,7 +289,6 @@ private fun ActionProgressDialog(
private val MediaOverviewTab.titleResId: Int private val MediaOverviewTab.titleResId: Int
get() = when (this) { get() = when (this) {
MediaOverviewTab.Media -> R.string.MediaOverviewActivity_Media MediaOverviewTab.Media -> R.string.media
MediaOverviewTab.Documents -> R.string.MediaOverviewActivity_Documents MediaOverviewTab.Documents -> R.string.document
} }

View File

@ -50,7 +50,7 @@ fun MediaOverviewTopAppBar(
IconButton(onClick = onSelectAllClicked) { IconButton(onClick = onSelectAllClicked) {
Icon( Icon(
painterResource(R.drawable.ic_baseline_select_all_24), painterResource(R.drawable.ic_baseline_select_all_24),
contentDescription = stringResource(R.string.MediaOverviewActivity_Select_all), contentDescription = stringResource(R.string.selectAll),
tint = LocalColors.current.text, tint = LocalColors.current.text,
) )
} }

View File

@ -234,11 +234,7 @@ class MediaOverviewViewModel(
viewModelScope.launch { viewModelScope.launch {
val selectedMedia = selectedMedia.toList() val selectedMedia = selectedMedia.toList()
mutableShowingActionProgress.value = application.resources.getQuantityString( mutableShowingActionProgress.value = application.resources.getString(R.string.saving)
R.plurals.ConversationFragment_saving_n_attachments,
selectedMedia.size,
selectedMedia.size,
)
val attachments = selectedMedia val attachments = selectedMedia
.asSequence() .asSequence()
@ -308,7 +304,7 @@ class MediaOverviewViewModel(
} }
viewModelScope.launch { viewModelScope.launch {
mutableShowingActionProgress.value = application.getString(R.string.MediaOverviewActivity_Media_delete_progress_message) mutableShowingActionProgress.value = application.getString(R.string.deleting)
// Delete the selected media items, and retrieve the thread ID for the address if any // Delete the selected media items, and retrieve the thread ID for the address if any
val threadId = withContext(Dispatchers.Default) { val threadId = withContext(Dispatchers.Default) {

View File

@ -64,7 +64,7 @@ fun MediaPage(
state.isEmpty() -> { state.isEmpty() -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text( Text(
text = stringResource(R.string.media_overview_activity__no_media), text = stringResource(R.string.attachmentsMediaEmpty),
style = LocalType.current.base, style = LocalType.current.base,
color = LocalColors.current.text color = LocalColors.current.text
) )

Some files were not shown because too many files have changed in this diff Show More