1012 Commits

Author SHA1 Message Date
a5d2cfdd58 chore(all): Bump moxxmpp to fix SASL2 login with SCRAM 2024-11-16 18:14:18 +01:00
2665837f80 fix(ui,service): Fix creating a new chat after dismissing it 2024-10-20 15:27:25 +02:00
66ea2e05d6 fix(ui): Fix too empty navigation stack when adding a new conversation 2024-10-20 11:39:42 +02:00
36d65679d7 fix(ui): Fix opening the self-profile if no profile picture is set 2024-10-20 11:30:34 +02:00
28614e33b6 fix(service): Fix crash when the account has no profile picture 2024-10-19 22:21:53 +02:00
a6750d3369 chore(service): Bump moxxmpp 2024-10-19 21:47:34 +02:00
4038eaa466 fix(ui): Fix exception when leaving the chat view 2024-10-19 21:30:02 +02:00
7f40f2511e fix(service): Fix loading quoted messages 2024-10-19 20:58:30 +02:00
aef6580074 fix(service): Fix adding a message quote 2024-10-19 20:45:31 +02:00
6fd5567217 fix(all): Fix build via the Nix Flake 2024-10-03 18:42:12 +02:00
e54116fa4c fix(android): Fix flake.nix and build with newer Flutter 2024-10-03 17:01:29 +02:00
809933d9b6 fix(android): Bump Android plugin 2024-10-01 20:42:12 +02:00
594f772eae fix(all): Fix issues with flutter run 2024-09-30 19:53:56 +02:00
145bdff33b fix(all): Update the Flutter version 2024-09-29 18:56:57 +02:00
bf653cf556 fix(service): Fix reply compatibility with Gajim 2024-06-16 15:00:16 +02:00
46b3603b6f Merge pull request 'New homescreen UI' (#351) from new-ui/homescreen into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/351
2024-04-13 14:50:11 +00:00
0a3260adcd feat(ui): Special thanks to Synoh and Ailyaut 2024-04-10 21:30:48 +02:00
63d8fc1eb2 fix(ui): Remove the 'Chat' text from the FAB 2024-04-08 21:58:04 +02:00
ba45728b57 fix(ui): Push chat title and body closer together 2024-04-08 21:55:28 +02:00
e1dd958959 fix(ui): Remove obsolete TODO 2024-04-07 23:29:14 +02:00
391aab1b7d fix(ui): Fix broken navigation in the sticker picker 2024-04-07 23:21:13 +02:00
1567a60fa1 fix(ui): Fix global shared media view 2024-04-07 23:20:18 +02:00
8ed018a652 fix(ui): Remove obsolete TODO 2024-04-07 23:13:18 +02:00
3304802ce0 fix(ui): Remove TODO :) 2024-04-07 23:10:09 +02:00
2305976f3c feat(ui): Remove ConversationsListItem completely 2024-04-07 19:35:07 +02:00
3c937194b4 feat(ui): Replace ConversationsListRow in the new chat page 2024-04-07 18:46:02 +02:00
eac5c692f5 feat(ui): Fix some message preview formatting things 2024-04-07 12:33:23 +02:00
89a1318a58 feat(ui): Remove the non-squircle avatar widget 2024-04-07 12:27:37 +02:00
a086facec3 feat(ui): Turn profile picture size into constant 2024-04-06 22:02:13 +02:00
7b7abee1f8 feat(ui): Improve the prepended text to message previews 2024-04-06 20:51:50 +02:00
09b08e0cd3 fix(ui): Only set the message state if there is some state 2024-04-06 20:43:15 +02:00
2ebabaf8a0 feat(ui): Consider the account JID in getConversationByJid 2024-04-06 20:30:47 +02:00
c0fa7f5fab fix(ui): Replace string with i18n string 2024-04-06 20:25:22 +02:00
517005a065 fix(ui): Handle AppBar underscroll 2024-04-06 17:23:57 +02:00
b9d4e9d93e feat(ui,service): Implement favourites 2024-04-06 16:45:37 +02:00
c02e9dc0a9 feat(service): Log setting the active conversation JID 2024-04-01 22:17:16 +02:00
0b55928187 fix(ui): Fix back-navigation behaviour 2024-04-01 21:39:07 +02:00
37f5d4a36d fix(ui): Fix import ordering 2024-04-01 20:40:35 +02:00
9bde93b01e refactor(ui): Rename bloc to state 2024-04-01 20:34:33 +02:00
fbec7c7c70 refactor(ui): StartGroupchatBloc -> StartGroupchatCubit 2024-04-01 20:31:09 +02:00
de5fa8024b fix(ui): Fix being stuck after navigation 2024-04-01 20:24:12 +02:00
0cf503dfde fix(ui): Fix using context.read for navigation 2024-04-01 20:19:38 +02:00
da43406b72 refactor(ui): Remove unused navigation state 2024-04-01 18:35:12 +02:00
a80bef225a refactor(ui): NavigationBloc -> NavigationCubit 2024-04-01 18:32:43 +02:00
d953f09e6f refactor(ui): ConversationBloc -> ConversationCubit 2024-04-01 18:16:22 +02:00
763573612a refactor(ui): NewConversationBloc -> NewConversationCubit 2024-04-01 18:05:21 +02:00
a60aea3d7d refactor(ui): OwnDevicesBloc -> OwnDevicesCubit 2024-04-01 17:57:35 +02:00
e36d670ba5 refactor(ui): StickersBloc -> StickersCubit 2024-04-01 17:50:36 +02:00
9c2f5827c8 refactor(ui): StickerPackBloc -> StickerPackCubit 2024-04-01 17:46:55 +02:00
f1c39bdb90 refactor(ui): StartChatBloc -> StartChatCubit 2024-04-01 17:39:34 +02:00
a98937aa61 refactor(ui): ShareSelectionBloc -> ShareSelectionCubit 2024-04-01 16:34:11 +02:00
20f60c6156 refactor(ui): ServerInfoBloc -> ServerInfoCubit 2024-04-01 16:25:21 +02:00
68c96aa75d refactor(ui): SendFilesBloc -> SendFilesCubit 2024-04-01 16:22:33 +02:00
1f4240623a refactor(ui): RequestBloc -> RequestsCubit 2024-04-01 16:07:43 +02:00
3af3d5c35c refactor(ui): PreferencesBloc -> PreferencesCubit 2024-04-01 16:03:06 +02:00
98bea3efed refactor(ui): LoginBloc -> LoginCubit 2024-04-01 15:54:18 +02:00
c2f7634197 refactor(ui): DevicesBloc -> DevicesCubit 2024-04-01 15:48:16 +02:00
30b4110358 refactor(ui): CropBackgroundBloc -> CropBackgroundCubit 2024-04-01 15:42:35 +02:00
32444fa9f5 refactor(ui): CropBloc -> CropCubit 2024-04-01 15:34:22 +02:00
a8398eb07b refactor(ui): BlocklistBloc -> BlocklistCubit 2024-04-01 15:27:33 +02:00
3af188c8a2 refactor(ui): ProfileBloc -> ProfileCubit 2024-04-01 15:19:41 +02:00
5cfc5f72b7 refactor(ui): Remove the UIDataService 2024-03-30 22:36:51 +01:00
bb273ed520 fix(ui): Add back more conversations functionality 2024-03-30 22:13:11 +01:00
af83d50f67 feat(ui): More translations 2024-03-30 22:02:36 +01:00
d7927d3ca9 fix(ui): Hide the delete button if we have only one account 2024-03-30 20:18:07 +01:00
8928d3a34c fix(ui): Allow opening non-redesigned pages again 2024-03-30 20:11:36 +01:00
de84d5924b fix(ui): Fix WillPopScope usage 2024-03-30 17:03:06 +01:00
3fa3de7a5e fix(service): Bump moxdns to fix issue with DNSSEC
Why is my router inserting itself into a DNSSEC-protected query
to my own domain?
2024-02-24 13:55:58 +01:00
86db8f00a7 feat(ui): Use the TextScaler 2024-02-14 20:19:56 +01:00
f808316ff9 fix(ui): Finally center the text 2023-12-23 17:28:24 +01:00
e956c76664 feat(ui): Re-add marking as read 2023-12-22 21:03:43 +01:00
50b2a1a077 feat(ui): Adjust the look of the context menu 2023-12-22 21:02:55 +01:00
316832982f fix(ui): Fix alignment of context menu 2023-12-20 21:54:25 +01:00
a9f8b7a09a feat(ui): Replace the stack with an overlay 2023-12-20 21:41:47 +01:00
155c9a6c60 feat(ui): Fix issue with moving the TextEditingController into the Cubit 2023-12-20 21:25:17 +01:00
f89d3827a4 feat(ui): Move the TextEditingController into the Cubit 2023-12-20 20:24:07 +01:00
c459862fde feat(ui): Highlight title and body of chats on search 2023-12-20 17:28:29 +01:00
008771cbfc feat(ui): Show a new text if there are no search results 2023-12-20 16:42:57 +01:00
01bef089c7 fix(ui): Reset the input field on navigating back 2023-12-20 16:34:50 +01:00
269d9e8771 fix(ui): Prevent UI updates if the user cancelled the search 2023-12-20 16:32:18 +01:00
f21efceb71 fix(ui): Use const 2023-12-20 16:28:49 +01:00
b18d3b6f26 fix(service): Minor fixes 2023-12-20 16:28:22 +01:00
37c46b62d2 feat(ui): Show work indicator for searches 2023-12-20 16:28:11 +01:00
4401a3a09a feat(ui): Get account list from AccountCubit 2023-12-20 16:17:25 +01:00
682bf06d4f feat(ui): Pull out the bottom sheet into its own file 2023-12-19 18:08:06 +01:00
aa902fc49a feat(ui): Move the conversations page to /home 2023-12-19 17:56:14 +01:00
02f0a324ad feat(ui): Remove ValueNotifiers 2023-12-19 17:50:07 +01:00
18b3670269 feat(service): Let the search include the last message 2023-12-18 21:04:08 +01:00
176a1c7df6 feat(ui,service): Implement a simple conversation search 2023-12-18 19:07:56 +01:00
6d482623e1 feat(ui): Add a TODO 2023-11-26 20:26:56 +01:00
a001f0b3fe feat(ui): Move the TextEditController somewhere else 2023-11-26 20:22:34 +01:00
467d966652 fix(ui): Set the search fields text color 2023-11-26 12:01:56 +01:00
55aa3737fc feat(ui): Replace ConversationsBloc with two Cubits 2023-11-25 15:53:23 +01:00
12f93359f6 feat(ui): Introduce an AccountCubit 2023-11-25 15:34:32 +01:00
05ed9de710 feat(ui): Move conversations handling into a Cubit 2023-11-25 15:12:46 +01:00
6b6e795d7d fix(ui): Clear the search field when closing 2023-11-25 12:27:07 +01:00
2345e60f1a feat(ui): Back navigation dismisses the search 2023-11-25 12:13:33 +01:00
24464da728 fix(ui): For the moment, make every chat not a favourite 2023-11-25 12:01:26 +01:00
b62a831905 feat(ui): Show favourited chats 2023-11-25 11:55:51 +01:00
33970a42c8 feat(ui): Implement the UI of the chat search 2023-11-24 21:34:38 +01:00
4604fea553 feat(ui): Show an error icon if the message has an error 2023-11-24 21:34:25 +01:00
706c1f2488 feat(ui): Make the AppBar's color correct 2023-11-24 17:42:12 +01:00
b587aa99ae feat(ui): Dynamically size the bottom modal 2023-11-24 17:24:22 +01:00
205286b9cf feat(ui): Larger padding 2023-11-18 22:02:03 +01:00
87a6cbe76a feat(ui): Prelimiary work on the header 2023-11-18 21:56:26 +01:00
c1d9185a2f fix(ui): Do not explicitly color the card background 2023-11-18 21:35:04 +01:00
466d502db1 feat(ui): Implement avatars 2023-11-18 21:34:06 +01:00
4b6455b12d fix(service): Fix avatar reference counting 2023-11-18 21:33:54 +01:00
6b22e6d739 feat(ui): Port more of the conversation design 2023-11-18 21:14:48 +01:00
dbb563aa42 feat(ui): Use the system-provided color scheme 2023-11-18 17:58:22 +01:00
5dfed5dd93 refactor(ui): Move the logo assets into the design repo
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-10-27 18:05:36 +02:00
5a23f743aa feat(ui): Handle keyboard content insertions
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Fixes #347.
2023-10-10 16:50:59 +02:00
245b6384be fix(tests): Adapt avatar tests to new moxxmpp stream requirement
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-10-07 23:58:11 +02:00
119ac4c350 feat(ci): Run tests and linter 2023-10-07 23:53:30 +02:00
2a7308147a feat(ci): Fail if any command exits with != 0
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-10-07 19:01:40 +02:00
ed36e93fbb fix(ci): Fix differences between wc and BusyBox wc
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-10-07 19:00:21 +02:00
f400ba46ae fix(ci): Fix name of bash container
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-10-07 18:58:08 +02:00
738b447103 Merge pull request 'Implement a basic participants list (and other things)' (#348) from feat/participants-list into master
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/348
2023-10-07 16:54:31 +00:00
d25e79d46e feat(all): Remove path override for moxxmpp 2023-10-07 18:54:19 +02:00
d347c6f2a9 feat(all): Restrict the CI to the master branch 2023-10-05 21:10:58 +02:00
ef9b29db4f feat(all): Run the fastlane checks as a pipeline 2023-10-05 21:10:32 +02:00
0003eed64d feat(all): Add a check for fastlane metadata length 2023-10-05 21:10:17 +02:00
da20e1e9ab feat(i18n): Admin -> Administrator 2023-10-01 21:13:51 +02:00
3ea85b4094 feat(ui): Localise the admin and owner texts 2023-10-01 21:09:52 +02:00
10c0a18b41 feat(shared): "Factor" out the boolean converter 2023-10-01 21:00:09 +02:00
6d6007cd32 feat(ui): Improve display of the self member 2023-10-01 20:57:10 +02:00
6ec1365aec feat(service): Deal with a user changing their nickname 2023-10-01 20:54:17 +02:00
f918d56aa7 feat(service): Handle users joining and leaving 2023-10-01 13:40:22 +02:00
2ed9da8c1a feat(ui): Clean the sorting code a bit 2023-10-01 13:04:30 +02:00
0e78d851cf feat(ui,service): Handle the "self-participant" 2023-09-29 22:45:23 +02:00
a62a4e0da1 feat(ui,service): Get a basic participant list going 2023-09-29 21:35:56 +02:00
3f9ae41f2c feat(ui): Use a better fallback icon for groupchats 2023-09-26 15:37:38 +02:00
673a6717c4 feat(ui,service): Handle groupchat avatars 2023-09-26 15:33:43 +02:00
85b987db90 feat(ui): Wrap the slider in a UnconstrainedBox 2023-09-25 21:22:58 +02:00
ddf419da92 feat(ui,service): Handle leaving and rejoining MUCs 2023-09-25 21:10:34 +02:00
bd929b24e5 fix(service): Use a groupchat's title in notifications 2023-09-24 19:27:13 +02:00
9c5a8fd499 fix(ui): Fix weird navigation stack after joining a groupchat 2023-09-24 19:21:52 +02:00
d6a9dfb086 fix(ui): Fix color of the conversations speed dial 2023-09-24 19:05:25 +02:00
c3b66b3b3a feat(ui): Show the last message's sender for groupchats 2023-09-24 19:02:13 +02:00
a006fd7fc6 fix(ui): Remove large "padding" of the check mark 2023-09-24 18:55:52 +02:00
d1c44ae6e3 feat(ui): Use consistent color generation 2023-09-24 18:51:16 +02:00
fd9842b1af Merge pull request 'Translations update from Weblate' (#338) from translate/moxxy:weblate-moxxy-moxxy into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/338
2023-09-24 14:07:19 +00:00
Codeberg Translate
b2d0ef530e Update translation files
Updated by "Squash Git commits" hook in Weblate.

Co-authored-by: Codeberg Translate <translate@codeberg.org>
Translate-URL: https://translate.codeberg.org/projects/moxxy/fastlane-metadata/
Translation: Moxxy/Fastlane Metadata
2023-09-24 14:06:32 +00:00
Codeberg Translate
abe5107636 Update translation files
Updated by "Squash Git commits" hook in Weblate.

Co-authored-by: Codeberg Translate <translate@codeberg.org>
Translate-URL: https://translate.codeberg.org/projects/moxxy/fastlane-metadata/
Translation: Moxxy/Fastlane Metadata
2023-09-24 14:06:32 +00:00
Codeberg Translate
7970f1ed73 Translated using Weblate (Dutch)
Currently translated at 100.0% (320 of 320 strings)

Update translation files

Updated by "Squash Git commits" hook in Weblate.

Translated using Weblate (Dutch)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (319 of 319 strings)

Co-authored-by: Codeberg Translate <translate@codeberg.org>
Co-authored-by: Vistaus <vistausss@fastmail.com>
Translate-URL: https://translate.codeberg.org/projects/moxxy/fastlane-metadata/
Translate-URL: https://translate.codeberg.org/projects/moxxy/fastlane-metadata/nl/
Translate-URL: https://translate.codeberg.org/projects/moxxy/moxxy/nl/
Translation: Moxxy/Fastlane Metadata
Translation: Moxxy/Moxxy
2023-09-24 14:06:32 +00:00
Codeberg Translate
dc4f6541f5 Translated using Weblate (Galician)
Currently translated at 100.0% (320 of 320 strings)

Update translation files

Updated by "Squash Git commits" hook in Weblate.

Co-authored-by: Codeberg Translate <translate@codeberg.org>
Co-authored-by: ghose <correo@xmgz.eu>
Translate-URL: https://translate.codeberg.org/projects/moxxy/fastlane-metadata/
Translate-URL: https://translate.codeberg.org/projects/moxxy/moxxy/gl/
Translation: Moxxy/Fastlane Metadata
Translation: Moxxy/Moxxy
2023-09-24 14:06:32 +00:00
3d619b323e feat(ui): Add the sender name to stickers 2023-09-24 16:06:25 +02:00
30a930ebbf fix(ui): Fix the sender name of quoted messages 2023-09-24 15:59:10 +02:00
79bebe9270 fix(ui): Fix hardcoded ConversationTypes 2023-09-24 15:36:53 +02:00
d3d5a62187 fix(ui): Make the file widget a bit less ugly 2023-09-24 14:01:42 +02:00
2d818f2b4e feat(ui): Add the sender name to all message types 2023-09-24 13:34:27 +02:00
763070a60d fix(ui,service): Fix sending chat states in groupchats 2023-09-24 12:56:04 +02:00
48ca46385d fix(ui): Do not show the online indicator in groupchats 2023-09-23 22:26:27 +02:00
848d6524d5 chore(all): Bump moxxmpp 2023-09-23 22:19:59 +02:00
1a415b9c97 feat(ui): Add the sender name to the message 2023-09-23 13:50:04 +02:00
1678466ec4 fix(ui): Don't try to request avatars for groupchats 2023-09-22 22:31:15 +02:00
bc35a36510 feat(service): "Pre-join" open groupchats 2023-09-22 22:17:57 +02:00
f0807c9841 fix(ui): Fix images being pixelated 2023-09-21 13:54:26 +02:00
9ea5d41096 Merge pull request 'Implement media viewers' (#344) from feat/media-viewers into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/344
2023-09-20 20:23:20 +00:00
628a03dce7 fix(ui): Fix the playbutton overflowing in shared videos 2023-09-20 22:22:50 +02:00
c58e9ab47c chore(all): Bump moxxy_native 2023-09-20 22:14:30 +02:00
d90657d3a8 feat(ui): Use the ImageViewer in the sendfiles page 2023-09-20 21:59:29 +02:00
81e1a10079 fix(ui,service): Fix massive storage leak
It turns out that picked files are not cleaned up and just accumulate.
This commit removes then if the file sending process is cancelled or if
the file is uploaded. And as a safety measure, this currenly only
happens on Android, where we know we copy files into the cache
directory.
2023-09-20 19:59:35 +02:00
a5b7c920e3 feat(ui): Use the VideoViewer in the sendfiles page 2023-09-20 19:32:23 +02:00
4dc2cc4c9c feat(ui): Offload the isolate stuff to image 2023-09-20 14:21:49 +02:00
edc765f088 fix(ui): Fix usage of wrong event channel 2023-09-20 14:21:27 +02:00
7e3dde6c40 feat(ui): Allow sharing messages from the long-press menu 2023-09-19 20:50:20 +02:00
a5ab14302b fix(ui): The source code is hosted on Codeberg 2023-09-19 17:22:23 +02:00
92c171e967 feat(ui): Hide the top bar with the other UI elements 2023-09-19 17:20:45 +02:00
c50b436a29 fix(service): Fix backgroundPath migration being too greedy 2023-09-19 16:57:49 +02:00
acac9eac60 fix(ui): Fix issue with having no background image set 2023-09-19 16:54:55 +02:00
3467ef9227 fix(service): Be less strict about what avatars we accept 2023-09-19 16:08:01 +02:00
b601c88519 fix(service): Fix bug where the latest video thumbnail is not shown 2023-09-19 14:40:15 +02:00
407c84afec fix(ui): Fix video messages looking wrong 2023-09-18 23:18:15 +02:00
a8acfa6870 fix(ui): Shared videos have no "play" button 2023-09-18 23:08:39 +02:00
0a4af03e20 feat(ui): Make the shared media list use the viewers 2023-09-18 23:08:26 +02:00
5dfe080328 feat(ui): Allow viewing videos 2023-09-18 22:53:58 +02:00
4c2e2f0da9 feat(ui): Allow viewing and sharing images 2023-09-18 21:28:11 +02:00
e55a8c327c feat(all): Remove flutter_keyboard_height 2023-09-18 20:59:07 +02:00
c49228b37f fix(service): Fetch a matching hash if the avatar file does not exist 2023-09-18 14:01:02 +02:00
2f109f04c8 feat(ui): Round the ink splash around profile options 2023-09-18 13:44:11 +02:00
d63cbdf0c0 Merge pull request 'Rework avatars' (#342) from rework/avatars into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/342
2023-09-18 11:29:06 +00:00
6451bb9b4b fix(ui): Fix spilling of the Ink 2023-09-18 00:03:39 +02:00
2f677128d0 feat(ui): Replace PickedAvatar with a record 2023-09-17 23:47:45 +02:00
7f3418a401 fix(service): Fix avatar migration 2023-09-17 23:44:22 +02:00
3d708dc8e6 feat(service): Migrate the old avatars to the new cache location 2023-09-17 23:23:01 +02:00
c38b6e366c chore(ui,service): Fix linter warnings 2023-09-17 20:30:28 +02:00
f080ee9872 chore(all): Bump moxxmpp 2023-09-17 20:21:18 +02:00
b9e953ddad feat(service): Add a test for the new AvatarService behaviour 2023-09-17 20:14:16 +02:00
8c6efcdb67 fix(service): Fix potential security issue
After fetching an avatar, check if the provided hash
matches the data's hash.
2023-09-16 13:25:37 +02:00
0911c2cb72 fix(ui): Fix send button being always vertically centered 2023-09-16 13:17:34 +02:00
7889a1aba8 feat(ui): Add rounded corners to the profile ink splash 2023-09-15 23:20:30 +02:00
ebf049a301 feat(service,ui): Rewrite the avatar service 2023-09-15 23:20:14 +02:00
e7ba8c0b64 feat(all): avatarPath, avatarHash, backgroundImage are now nullable 2023-09-15 16:25:29 +02:00
aa0b598a2f Merge pull request 'Use Material3' (#337) from feat/material3 into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/337
2023-09-14 20:22:38 +00:00
91718591ea chore(all): Bump version and update changelog 2023-09-14 22:22:04 +02:00
210863b169 chore(all): Refactor and change the sticker icon 2023-09-14 22:18:41 +02:00
3112ff0f9e fix(ui): Fix style issues with the emoji picker 2023-09-14 22:10:07 +02:00
d179a9c8e8 fix(ui): Fix the "overflow" of the emoji picker 2023-09-14 22:03:51 +02:00
82ec87756c fix(ui): Fix leaving the audio recorder running after cancelling 2023-09-14 19:00:46 +02:00
d4f4a27a08 chore(ui): Document the MobileMessagingTextFieldController 2023-09-14 18:35:25 +02:00
1f03bc729a chore(ui): Fix linter warnings 2023-09-14 18:24:02 +02:00
4a0e5b9031 fix(ui): Make the lock threshold depend on the lock button height 2023-09-14 17:16:28 +02:00
28ed8b1c0a fix(ui): Stop layouting the record icon when we have text 2023-09-14 17:09:44 +02:00
0e53ed23c0 fix(ui): Fix some record button overlay issues
- Dragging now immediately moves the overlay up
- The overlay should not start perfectly centered
2023-09-14 16:59:32 +02:00
5bf75e1be5 feat(ui): Ask if the voice recording should be discarded 2023-09-13 20:46:27 +02:00
1faa20b23d feat(ui): Actually start recording 2023-09-13 20:14:49 +02:00
3decd0f58b fix(ui): Fix linter warnings 2023-09-13 18:59:28 +02:00
3770165f6f fix(ui): Bring back message quoting 2023-09-13 18:44:18 +02:00
e091e3982b feat(ui): Initial rework of the messaging input field 2023-09-13 18:29:38 +02:00
bdba9cebdd fix(ui): Fix shape of the multi-button 2023-09-12 13:17:13 +02:00
e0d6776f11 fix(ui): Remove dialog/bottom sheet shapes 2023-09-12 12:28:12 +02:00
d7a64e3048 fix(ui): Add missing translation 2023-09-11 22:02:39 +02:00
914b4b0a0c fix(ui,service,shared): Fix linter issues 2023-09-11 21:59:46 +02:00
a7f844fab4 chore(ui): Add a TODO 2023-09-11 21:50:54 +02:00
4337b07658 fix(ui): Fix blurring background images 2023-09-11 21:50:13 +02:00
1033cbc247 fix(ui): Translate the login button 2023-09-11 21:14:53 +02:00
5a41099238 chore(all): Update all dependencies 2023-09-11 21:14:22 +02:00
b3eb12afbb fix(ui): Adjust the speed dial to Material3 2023-09-11 20:44:20 +02:00
62b79b6c7e fix(ui): Fix exception when no data is stored 2023-09-11 20:34:52 +02:00
5def026ca7 feat(ui): Replace RoundedButton with FilledButton 2023-09-11 20:31:05 +02:00
54d672b17e feat(ui): Replace BorderlessTopbar with AppBar 2023-09-11 20:24:15 +02:00
6a0d5ff3fc chore(all): Update Flutter to 3.13 2023-09-11 13:06:04 +02:00
3eedf7527d Merge pull request 'Replace moxplatform with moxxy_native' (#336) from feat/replace-moxplatform into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/336
2023-09-11 10:33:31 +00:00
a370d0d6ce chore(all): Remove the moxxy_native and moxplatform overrides 2023-09-10 22:14:35 +02:00
b4e3a5c542 chore(ui,service,shared): Fix linter issues 2023-09-10 19:22:56 +02:00
927f354f18 chore(service,ui,shared): Migrate away from moxplatform 2023-09-10 19:14:45 +02:00
45a460a11f chore(all): Use moxxy_native from hosted Pub repo 2023-09-08 15:43:34 +02:00
7655befcc3 chore(docs): Update build instructions 2023-09-08 15:38:03 +02:00
c96880d437 fix(android): (Hopefully) fix build issues
Okay, so it turns out we cannot just have a plugin right
next to the actual app because then Flutter won't generate
both the GeneratedPluginRegistrant and the correct links to
the plugins. As such, we have to move the plugin code out of
Moxxy into the new moxxy_native library.

However, we need access to the activity to receive the notification
tap events (at least on Android). To fix this, we have a quirk that
checks if we have a notification event before finishing the handling
of the PreStartDoneEvent.
2023-09-08 15:33:39 +02:00
21c8079f49 release: Release v0.5.0 2023-09-07 17:05:16 +02:00
175b2d5f2a Merge pull request 'Translations update from Weblate' (#333) from translate/moxxy:weblate-moxxy-moxxy into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/333
2023-09-07 12:21:17 +00:00
Codeberg Translate
38ef7efac9 Translated using Weblate (Dutch)
Currently translated at 100.0% (309 of 309 strings)

Update translation files

Updated by "Squash Git commits" hook in Weblate.

Co-authored-by: Codeberg Translate <translate@codeberg.org>
Co-authored-by: Vistaus <vistausss@fastmail.com>
Translate-URL: https://translate.codeberg.org/projects/moxxy/fastlane-metadata/
Translate-URL: https://translate.codeberg.org/projects/moxxy/moxxy/nl/
Translation: Moxxy/Fastlane Metadata
Translation: Moxxy/Moxxy
2023-09-07 12:19:17 +00:00
Codeberg Translate
aca226b764 Translated using Weblate (Galician)
Currently translated at 100.0% (309 of 309 strings)

Co-authored-by: ghose <correo@xmgz.eu>
Translate-URL: https://translate.codeberg.org/projects/moxxy/moxxy/gl/
Translation: Moxxy/Moxxy
2023-09-07 12:19:17 +00:00
2c5fbd8d5d Merge pull request 'Use Android 13's photo picker' (#331) from feat/photo-picker into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/331
2023-09-07 12:19:13 +00:00
17295320c3 fix(android): Make notification tapping work again 2023-09-06 21:17:18 +02:00
37390eea97 chore(all): Remove file_picker 2023-09-06 18:11:45 +02:00
ab71754faf chore(docs): Mention the new photo picker 2023-09-06 18:10:51 +02:00
17396c3db2 chore(android): Lint using ktlint 2023-09-06 18:01:28 +02:00
742f8b6d8d chore(ui): Fix linter issues 2023-09-06 18:00:34 +02:00
161dd5cca8 fix(ui,service): Fix exception when exiting conversation 2023-09-06 17:58:57 +02:00
4dfacc759b chore(android): Refactor the plugin code a bit 2023-09-06 17:55:57 +02:00
da57b113f8 feat(android,ui): Use Android 13's new photo picker 2023-09-06 17:51:53 +02:00
4a1d30e8c4 fix(android): Add missing drawables 2023-09-06 13:14:57 +02:00
4e4a5c1b52 Merge pull request 'Translations update from Weblate' (#327) from translate/moxxy:weblate-moxxy-moxxy into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/327
2023-09-05 14:07:06 +00:00
4e097d8286 chore(docs): Mention pigeon 2023-09-05 11:58:09 +02:00
bfe2a0f58a feat(service): Notify on blocking failure
Fixes #189.
2023-09-04 15:41:25 +02:00
Codeberg Translate
13ece41a8f Translated using Weblate (Russian)
Currently translated at 90.8% (277 of 305 strings)

Co-authored-by: Codeberg Translate <translate@codeberg.org>
Co-authored-by: evmexa-prog <vladvle@yandex.ru>
Translate-URL: https://translate.codeberg.org/projects/moxxy/moxxy/ru/
Translation: Moxxy/Moxxy
2023-09-04 12:59:16 +00:00
Codeberg Translate
f35cb04359 Translated using Weblate (Galician)
Currently translated at 100.0% (305 of 305 strings)

Co-authored-by: Codeberg Translate <translate@codeberg.org>
Co-authored-by: ghose <correo@xmgz.eu>
Translate-URL: https://translate.codeberg.org/projects/moxxy/moxxy/gl/
Translation: Moxxy/Moxxy
2023-09-04 12:59:16 +00:00
Codeberg Translate
08d11f3e9f Translated using Weblate (Polish)
Currently translated at 6.8% (21 of 305 strings)

Added translation using Weblate (Polish)

Co-authored-by: Codeberg Translate <translate@codeberg.org>
Co-authored-by: Eryk Michalak <gnu.ewm@protonmail.com>
Translate-URL: https://translate.codeberg.org/projects/moxxy/moxxy/pl/
Translation: Moxxy/Moxxy
2023-09-04 12:59:16 +00:00
63253e9cae Merge pull request 'Thumbnail generation on download and Android 13 fixes' (#329) from feat/thumbnails into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/329
2023-09-04 12:59:11 +00:00
26dafb4e9e chore(docs): Update the changelogs more 2023-09-03 22:12:17 +02:00
111c66aa6a chore(docs): Update changelog 2023-09-03 22:07:54 +02:00
5b03fc9b47 fix(service): Various fixes
- Fix a fourth notificatio channel appearing
- Pass a locale to the service on startup to prevent misnamed
  notification channels
2023-09-03 22:03:13 +02:00
672ae736d3 chore(docs): Mention ktlint for Android code 2023-09-03 16:40:03 +02:00
e37db3d00c fix(service): Fix typo in JID migration 2023-09-03 16:34:35 +02:00
919ed6f0a1 chore(android): Format with ktlint 2023-09-03 14:52:21 +02:00
79867e4eaa chore(all): Update moxplatform 2023-09-03 13:51:59 +02:00
99600bafb0 fix(ui,service): Merge ConversationExited with sending a gone 2023-09-03 00:03:23 +02:00
59aad79aa0 feat(service): Show video thumbnails in the notification 2023-09-02 20:53:49 +02:00
6fc4672a6e fix(service,ui): Fix notifications not updating 2023-09-02 20:17:04 +02:00
478c639ae7 feat(android): Refactor a little bit
Also, add VIBRATE permission.
2023-09-02 13:06:46 +02:00
7f2c978736 fix(service): Bring back notification tap events 2023-09-02 13:03:24 +02:00
f472239102 fix(ui): Replace context.read with GetIt 2023-08-30 21:00:27 +02:00
f2844122c0 chore(ui,service): Rename getVideoThumbnailPath 2023-08-30 20:59:14 +02:00
7781b12dac fix(service): Ensure that the correct locale is used 2023-08-30 20:21:16 +02:00
1e795b8b10 fix(service): Translate the foreground service info 2023-08-30 20:19:32 +02:00
7d0896d84f fix(service): Fix message channel name 2023-08-30 16:05:40 +02:00
fc0aade0ae fix(all): Remove Awesome Notifications 2023-08-30 16:04:39 +02:00
d969622675 feat(service): Replace the notification channels to apply new options 2023-08-30 15:51:51 +02:00
23839b6ec6 fix(service,ui): Fix notification issues 2023-08-26 23:28:49 +02:00
0b120c1e9c fix(service): Fix a fresh start on Android 13 2023-08-26 22:57:21 +02:00
e84d8f9455 feat(service): Store the occupant id with the messages
Linked issue: #315
2023-08-22 21:52:38 +02:00
c4973910f4 feat(all): Remove omemo_dart override 2023-08-22 20:25:30 +02:00
4ed57bc1d4 chore(ui): Move sendfiles.dart 2023-08-17 18:07:06 +02:00
e8125b661b Merge pull request 'Display the avatar and title when sharing files' (#325) from feat/sendfiles-avatar-title into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/325
2023-08-17 16:00:33 +00:00
c6b7bba5ee Merge branch 'master' into feat/sendfiles-avatar-title 2023-08-17 18:00:16 +02:00
cf5ee6c703 Merge pull request 'Translations update from Weblate' (#326) from translate/moxxy:weblate-moxxy-moxxy into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/326
2023-08-17 15:59:59 +00:00
63038b77bc fix(ui): Correctly handle the self-chat's "avatar" 2023-08-17 17:58:53 +02:00
7be67a9551 fix(ui): Also use the contact title in the share selection 2023-08-17 17:47:14 +02:00
0dc44572b1 fix(service): Optionally send the roster item's contact title 2023-08-17 17:42:51 +02:00
1de6bab86e feat(ui): Improve contrast with conversation indicator 2023-08-17 16:32:22 +02:00
0852e0efbf feat(ui,service): Allow usage of the conversation indicator in all cases 2023-08-17 15:05:55 +02:00
Codeberg Translate
8fe11f30f2 Translated using Weblate (French)
Currently translated at 100.0% (305 of 305 strings)

Co-authored-by: lejun <lejun@gmx.fr>
Translate-URL: https://translate.codeberg.org/projects/moxxy/moxxy/fr/
Translation: Moxxy/Moxxy
2023-08-16 10:53:05 +00:00
8b0a863949 feat(ui): Display recipient(s) and avatar 2023-08-15 21:49:43 +02:00
bde689b1b3 fix(ui): Fix overflows of images in the sendfiles page
Fixes #210.
2023-08-15 12:57:01 +02:00
2ff2402078 feat(ui): Make the server info page prettier 2023-08-14 21:27:38 +02:00
cb47fa210a Merge pull request 'Translations update from Weblate' (#320) from translate/moxxy:weblate-moxxy-moxxy into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/320
2023-08-14 17:08:25 +00:00
Codeberg Translate
599e56875d Update translation files
Updated by "Squash Git commits" hook in Weblate.

Translation: Moxxy/Fastlane Metadata
Translate-URL: https://translate.codeberg.org/projects/moxxy/fastlane-metadata/
2023-08-14 17:07:56 +00:00
Codeberg Translate
74569ccfd6 Translated using Weblate (French)
Currently translated at 28.5% (2 of 7 strings)

Translated using Weblate (French)

Currently translated at 24.2% (74 of 305 strings)

Added translation using Weblate (French)

Translated using Weblate (Dutch)

Currently translated at 100.0% (305 of 305 strings)

Co-authored-by: Codeberg Translate <translate@codeberg.org>
Co-authored-by: Vistaus <vistausss@fastmail.com>
Co-authored-by: doverdufflebag <alunyamoder@proton.me>
Translate-URL: https://translate.codeberg.org/projects/moxxy/fastlane-metadata/fr/
Translate-URL: https://translate.codeberg.org/projects/moxxy/moxxy/fr/
Translate-URL: https://translate.codeberg.org/projects/moxxy/moxxy/nl/
Translation: Moxxy/Fastlane Metadata
Translation: Moxxy/Moxxy
2023-08-14 17:07:56 +00:00
a009a68941 fix(ui): Fix issue with the self-avatar in the reaction list
Fixes #319.
2023-08-14 19:07:33 +02:00
37455c54e6 chore(service): Fix formatting 2023-08-14 18:54:50 +02:00
16dded8dee Merge pull request 'Make everything account JID dependent' (#318) from fix/jid-attribute into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/318
2023-08-14 16:51:53 +00:00
fa92ba1974 fix(service): Fix id of reaction messages 2023-08-14 18:51:36 +02:00
3011af4f0e fix(service): Fix linter issue 2023-08-14 14:53:29 +02:00
8a98815fd7 feat(service): Adjust the database creation 2023-08-14 14:48:06 +02:00
018f40d6db fix(service): Fix SQL syntax error 2023-08-13 12:07:27 +02:00
1d7e55c086 fix(service): Match by sid instead of origin id 2023-08-12 22:45:25 +02:00
d57e133d0d feat(service): Migrate GroupchatDetails 2023-08-12 21:00:01 +02:00
191996c5d4 fix(service): Fix minor bugs 2023-08-12 20:42:06 +02:00
bcef2dd818 fix(service): Fix foreign key issues 2023-08-12 19:08:38 +02:00
0358bf15a0 chore(service): Fix formatting 2023-08-12 18:59:00 +02:00
301ff664a8 fix(ui,shared,service): Use a UUID as a unique message key 2023-08-12 18:58:24 +02:00
865846af9a fix(service): Fix reactions 2023-08-12 16:48:34 +02:00
96b2c5f3c4 fix(service,shared): Fix more migration issues 2023-08-12 16:12:16 +02:00
db5e4e3c1f feat(service): Do not migrate data if we have no active JID 2023-08-12 15:56:55 +02:00
e3cd4aa3dd fix(service): Fix database migration 2023-08-12 15:51:45 +02:00
fbca84ae2f feat(service): Dismiss notifications on logout 2023-08-12 15:30:40 +02:00
6034ce5ba4 feat(ui,shared,service): Fix formatting and linter issues 2023-08-12 15:25:33 +02:00
2e71ffebd4 feat(service): Migrate the new notifications system 2023-08-12 13:43:38 +02:00
3ca7d63e8c fix(service): Fix some merge issues 2023-08-12 13:34:50 +02:00
ff2ca7397f Merge branch 'master' into fix/jid-attribute 2023-08-12 13:27:55 +02:00
843a1171b7 Merge pull request 'Translations update from Weblate' (#316) from translate/moxxy:weblate-moxxy-moxxy into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/316
2023-08-12 11:05:50 +00:00
Codeberg Translate
f0057e5487 Translated using Weblate (Galician)
Currently translated at 100.0% (301 of 301 strings)

Translated using Weblate (Japanese)

Currently translated at 32.5% (98 of 301 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (301 of 301 strings)

Translated using Weblate (Japanese)

Currently translated at 28.3% (84 of 296 strings)

Translated using Weblate (Japanese)

Currently translated at 18.5% (55 of 296 strings)

Translated using Weblate (Russian)

Currently translated at 94.5% (280 of 296 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (296 of 296 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (296 of 296 strings)

Added translation using Weblate (Galician)

Co-authored-by: 0eoc <0eoc@users.noreply.translate.codeberg.org>
Co-authored-by: Codeberg Translate <translate@codeberg.org>
Co-authored-by: Vistaus <vistausss@fastmail.com>
Co-authored-by: ghose <correo@xmgz.eu>
Co-authored-by: psw1747 <informatiekantoor@gmail.com>
Translate-URL: https://translate.codeberg.org/projects/moxxy/moxxy/gl/
Translate-URL: https://translate.codeberg.org/projects/moxxy/moxxy/ja/
Translate-URL: https://translate.codeberg.org/projects/moxxy/moxxy/nl/
Translate-URL: https://translate.codeberg.org/projects/moxxy/moxxy/ru/
Translation: Moxxy/Moxxy
2023-08-12 11:05:15 +00:00
d3914e3269 Merge pull request 'Deprecate Extensible File Thumbnails' (#317) from feat/remove-eft into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/317
2023-08-12 11:05:12 +00:00
431641e248 Merge branch 'master' into feat/remove-eft 2023-08-12 13:01:57 +02:00
e259b3ee63 Merge pull request 'Unencrypted files are now a warning, not an error' (#314) from fix/unencrypted-error into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/314
2023-08-12 11:00:06 +00:00
e34bbde27e fix(shared,service): Fix linter issues 2023-08-12 12:55:36 +02:00
f84593ec19 Merge branch 'master' into fix/unencrypted-error 2023-08-12 12:51:35 +02:00
49425440a5 Merge pull request 'Rework notifications' (#312) from rework/notifications into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/312
2023-08-12 10:49:37 +00:00
ee9332f6f2 fix(service): Adjust to groupchat changes 2023-08-11 23:21:44 +02:00
d06de37924 Merge branch 'master' into rework/notifications 2023-08-11 22:54:01 +02:00
0872b2a134 chore(service): Fix service port 2023-08-11 20:55:23 +02:00
95a6a458db chore(ui): Port the UI 2023-08-11 20:47:20 +02:00
112048df36 chore(service): Port everything (besides notifications) in the service 2023-08-11 18:15:18 +02:00
24a26fe454 chore(docs): Update and fix DOAP 2023-08-10 14:34:14 +02:00
fe846468ee Merge pull request 'Add groupchat support for Moxxy' (#300) from ikjot-2605/moxxy:feat/groupchat into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/300
2023-08-09 23:30:39 +00:00
be299ac90f feat(service): More refactoring 2023-08-10 01:28:15 +02:00
Ikjot Singh Dhody
549e61a168 feat(all): Fix linter issues.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-08-09 22:34:00 +05:30
f7e7c17598 chore(shared): Clean-up the message model 2023-08-09 14:19:03 +02:00
ikjot-2605
26bcaccd81 Merge branch 'master' into feat/groupchat 2023-08-08 15:39:43 +00:00
Ikjot Singh Dhody
df5810a347 feat(all): Formatting change, lock file update
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-08-08 21:09:00 +05:30
071ea6db5d feat(service): Write (untested) migration to the new format 2023-08-08 15:43:51 +02:00
01f46d4cdc feat(service): Replace EFT with JingleContentThumbnail
Just how Cheogram used to make them :)

Fixes #243.
2023-08-06 13:28:01 +02:00
2baf852a9a fix(service): Request batter-saving excemption only when optimised 2023-08-05 15:25:28 +02:00
7f5d8c353a feat(ui,service): Plumb the service into the requests dialog 2023-08-05 01:17:23 +02:00
f3a5683e13 feat(ui): Allow excluding Moxxy from battery optimisation 2023-08-05 00:14:12 +02:00
4323561774 chore(ui): Split the request BLoC into three files 2023-08-04 23:32:38 +02:00
6225ac65d9 feat(ui): Implement a better solution to handling permission requests 2023-08-04 23:26:29 +02:00
178648a5ee chore(all): Bump moxplatform 2023-08-04 16:38:03 +02:00
Ikjot Singh Dhody
ef931c566f feat(all): Send messages/chat state from Moxxy to MUC.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-08-03 23:04:13 +05:30
78dbf5473e feat(service): Migrate messages with the incorrect error 2023-08-03 16:18:58 +02:00
edcb95ac6f fix(service): Unencrypted files are now a warning, not an error
Fixes #214.
2023-08-03 16:11:08 +02:00
2fc23876e6 Merge branch 'master' into rework/notifications 2023-08-02 19:34:16 +00:00
293908c40c fix(service): Fix crash when the message contains no file
Fix extracted from #312.
2023-08-02 21:28:15 +02:00
Ikjot Singh Dhody
a3d4883406 feat(all): Remove unnecessary buttons/options for MUC.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-08-03 00:22:59 +05:30
Ikjot Singh Dhody
532f0b1bb2 feat(all): Formatting fix, and navigation fix.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-08-03 00:04:33 +05:30
Ikjot Singh Dhody
a98fe0d9f3 feat(all): Minor fixes - strings, formatting.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-08-02 23:42:36 +05:30
7919d11b00 feat(service): Implement a service for permission handling 2023-08-01 23:39:41 +02:00
ac5a2c9dd2 feat(service,ui): Request notification permission
This is a preparation for changes in Android 13.
2023-08-01 19:11:58 +02:00
Ikjot Singh Dhody
a7c3bd507f Merge branch 'master' of https://codeberg.org/moxxy/moxxy into feat/groupchat
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-08-01 22:30:01 +05:30
06d132b0b0 chore(service): Minor clean-up 2023-08-01 14:28:18 +02:00
837787fdaf fix(service): Reuse logic from the conversation model 2023-08-01 12:56:46 +02:00
9545b3d9cc chore(service,shared): Inline getStickerPackPath 2023-07-31 22:43:58 +02:00
Ikjot Singh Dhody
fba2cf86ae feat(all): Minor formatting changes.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-08-01 00:16:38 +05:30
d17a656d64 fix(service): Use the actual data directory instead of path_providers
This fixes an issue with creating a notifiction because we cannot expose
path_provider's data directory from the native FileProvider.
2023-07-31 20:46:26 +02:00
a5082762b5 fix(service): Fix crash when updating a notification 2023-07-31 18:17:30 +02:00
4160143f6e fix(ui): Fix mark as read not dismissing overlay
Fixes #313.
2023-07-31 17:50:02 +02:00
9fd6f847fe feat(service): React to locale changes 2023-07-31 17:26:08 +02:00
f65f8ec541 feat(service): Tell the notifications plugin about our own avatar 2023-07-30 22:33:37 +02:00
Ikjot Singh Dhody
af481bf465 feat(all): remove unnecessary comparator override.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-07-31 01:48:48 +05:30
Ikjot Singh Dhody
e6ae8182c2 feat(all): Organize import in main.dart.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-07-31 01:47:16 +05:30
31d0c9c9ec feat(service): Implement the reply button 2023-07-30 22:05:16 +02:00
de3bf12c18 feat(service): Re-implement (and fix) mark as read 2023-07-30 20:40:40 +02:00
9d2f6b29f7 fix(service): Null-pointer dereference 2023-07-29 22:34:37 +02:00
ee764d2213 feat(service): Migrate to Moxplatform's notifications 2023-07-29 22:23:31 +02:00
Ikjot Singh Dhody
de7b9adfa6 feat(all): Fix user flow for joining MUC.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-07-27 01:07:13 +05:30
8c53055341 fix(service): Remove database caching as it's probably broken
Fixes #299.
2023-07-26 12:33:46 +02:00
f0225eed4c feat(service): Show a notification when a message is bounced
Fixes #212. At the moment, this only happens with

- remote-server-not-found
- remote-server-timeout
- service-unavailable
2023-07-26 12:26:48 +02:00
b57bae878f fix(ui): Pop the system stack if we cannot pop the Flutter navigator
Fixes #305.
2023-07-25 22:48:42 +02:00
1a83ff37ff feat(all): Prepare for a correctly signed APK
Fixes #304.
2023-07-25 22:09:13 +02:00
e205785246 Merge pull request 'Translations update from Weblate' (#306) from translate/moxxy:weblate-moxxy-moxxy into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/306
2023-07-25 12:36:18 +00:00
3edda978fb Merge pull request 'Implement share shortcuts' (#302) from feat/direct-share into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/302
2023-07-25 12:35:00 +00:00
Codeberg Translate
ebabb0e445 Translated using Weblate (Russian)
Currently translated at 86.7% (255 of 294 strings)

Co-authored-by: 0eoc <0que@proton.me>
Translate-URL: https://translate.codeberg.org/projects/moxxy/moxxy/ru/
Translation: Moxxy/Moxxy
2023-07-24 18:38:06 +00:00
Ikjot Singh Dhody
e4f98bb82f feat(all): Add label to nick text field.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-07-24 23:46:11 +05:30
Ikjot Singh Dhody
56d6f97168 feat(all): Minor changes, fixes.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-07-24 23:37:45 +05:30
Ikjot Singh Dhody
b0067f055f feat(all): Move away from exception type design.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-07-22 14:26:26 +05:30
117d263e25 fix(service): Fix shared-to JIDs being the conversation title 2023-07-21 22:26:36 +02:00
63e66e5dce fix(service): Use a fallback icon when no avatar is available
This commit also fixes the Android SDK situation via the flake.
2023-07-21 19:24:10 +02:00
140a16ec0c fix(ui,service): Allow sharing to the self-chat
This also fixes some issues with chat that don't
have a profile picture.
2023-07-18 13:32:35 +02:00
44b95bbb5b feat(ui): Open the chat when sharing text via a direct share 2023-07-18 13:04:21 +02:00
4eeaa8c37b feat(ui): Handle direct shares 2023-07-17 22:27:11 +02:00
c95f2efd65 Merge branch 'master' into feat/direct-share 2023-07-17 17:17:58 +00:00
9dbf4b5467 feat(service): Implement share shortcuts 2023-07-17 19:16:12 +02:00
Ikjot Singh Dhody
bd094dfc9a feat(all): Remove unnecessary function.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-07-17 21:42:50 +05:30
Ikjot Singh Dhody
7e9d7d6281 feat(all): Check if Groupchat exists before method.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-07-17 21:41:29 +05:30
Ikjot Singh Dhody
2cf781459d feat(all): Debug mode and translatable string
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-07-17 20:28:05 +05:30
Ikjot Singh Dhody
4ff9e3c81e feat(all): Remove title from GroupchatDetails.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-07-17 20:15:03 +05:30
Ikjot Singh Dhody
e337f1c579 feat(all): Minor refactors - naming.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-07-17 20:04:36 +05:30
Ikjot Singh Dhody
7c840334e1 feat(all): Add docs to groupchat service methods.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-07-17 19:47:28 +05:30
0a120f1073 Merge pull request 'Translations update from Weblate' (#301) from translate/moxxy:weblate-moxxy-moxxy into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/301
2023-07-16 16:11:02 +00:00
Codeberg Translate
269738e618 Translated using Weblate (Dutch)
Currently translated at 100.0% (294 of 294 strings)

Translated using Weblate (German)

Currently translated at 28.5% (2 of 7 strings)

Translated using Weblate (German)

Currently translated at 100.0% (293 of 293 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (293 of 293 strings)

Co-authored-by: Codeberg Translate <translate@codeberg.org>
Co-authored-by: Vistaus <vistausss@fastmail.com>
Co-authored-by: nautilusx <translate@disroot.org>
Translate-URL: https://translate.codeberg.org/projects/moxxy/fastlane-metadata/de/
Translate-URL: https://translate.codeberg.org/projects/moxxy/moxxy/de/
Translate-URL: https://translate.codeberg.org/projects/moxxy/moxxy/nl/
Translation: Moxxy/Fastlane Metadata
Translation: Moxxy/Moxxy
2023-07-15 20:38:05 +00:00
Ikjot Singh Dhody
06eab1d6f5 feat(all): Make ConversationType an enhanced enum.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-07-14 14:55:55 +05:30
Ikjot Singh Dhody
008e816d70 feat(all): Rename JoinGroupchatResultEvent.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-07-14 14:30:01 +05:30
Ikjot Singh Dhody
2bbbc164b5 feat(all): Remove incorrect translations.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-07-14 14:26:51 +05:30
Ikjot Singh Dhody
11f4fd9932 feat(all): Add title to GroupchatDetails.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-07-12 12:09:12 +05:30
Ikjot Singh Dhody
a1451c6fbf feat(all): Refactor groupchat to new object in db.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-07-12 11:48:16 +05:30
1e94910ebd Merge pull request 'Paginate sticker pack access' (#298) from feat/sticker-pagination into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/298
2023-07-11 11:20:21 +00:00
Ikjot Singh Dhody
993da40297 feat(all): Complete join groupchat flow.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-07-11 02:15:31 +05:30
Ikjot Singh Dhody
09684b1268 feat(all): Fix fromDatabaseJson for MUC details.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-07-11 02:00:12 +05:30
Ikjot Singh Dhody
0abb89cf38 feat(all): Fix database issues with nick.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-07-11 01:48:35 +05:30
63d251a7f1 feat(service): Remove page caches for sticker packs 2023-07-10 20:20:30 +02:00
799af75bcc feat(ui,service): Only request stickers when required 2023-07-10 18:47:17 +02:00
8966c490fe fix(ui): Fix TODOs 2023-07-10 18:35:59 +02:00
4c09dbab60 fix(ui): Fix overflow in the sticker pack view 2023-07-10 16:37:31 +02:00
6fd5c33b0a fix(ui,service): Fix various issues 2023-07-10 16:35:13 +02:00
Ikjot Singh Dhody
7880f51b76 feat(all): Add db support for GroupchatDetails.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-07-10 01:32:17 +05:30
30e8a885bb fix(ui): Fix not disposing the sticker pack controller 2023-07-09 21:27:39 +02:00
42c695a2a1 fix(ui,service): Re-implement fetching local/remote sticker packs 2023-07-09 21:12:58 +02:00
Ikjot Singh Dhody
f0a79ca0e0 feat(all): Add GroupchatDetails model.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-07-09 21:51:36 +05:30
3ef2f3b8d6 feat(ui,service): Remove sticker packs list from StickersBloc 2023-07-09 17:15:38 +02:00
ae995b8670 feat(ui,service): Paginate the sticker picker 2023-07-09 16:49:30 +02:00
75c2f103bd feat(service): Try to not JUST ignore duplicate hash pointers 2023-07-09 14:22:41 +02:00
bc7958559a Merge pull request 'Translations update from Weblate' (#296) from translate/moxxy:weblate-moxxy-moxxy into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/296
2023-07-09 10:40:59 +00:00
Codeberg Translate
11228a0de0 Translated using Weblate (German)
Currently translated at 100.0% (293 of 293 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (7 of 7 strings)

Co-authored-by: Codeberg Translate <translate@codeberg.org>
Co-authored-by: PapaTutuWawa <papatutuwawa@polynom.me>
Co-authored-by: Vistaus <vistausss@fastmail.com>
Translate-URL: https://translate.codeberg.org/projects/moxxy/fastlane-metadata/nl/
Translate-URL: https://translate.codeberg.org/projects/moxxy/moxxy/de/
Translation: Moxxy/Fastlane Metadata
Translation: Moxxy/Moxxy
2023-07-09 10:40:24 +00:00
b6fe5cc29b fix(ui): Fix altIcon scaling 2023-07-09 12:32:55 +02:00
cb34b51149 fix(ui): Fix self-chat's avatar causing issues
Fixes #297.
2023-07-09 12:26:33 +02:00
6c9421a21a fix(docs): Fix typo 2023-07-09 12:15:28 +02:00
684a3a658d chore(docs): Add a note on translations 2023-07-09 12:14:08 +02:00
0ab7cccef6 Merge pull request 'Implement storage management' (#295) from feat/storage-management into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/295
2023-07-09 10:07:01 +00:00
c405717bcc feat(ui): Handle removing a sticker pack 2023-07-08 23:19:21 +02:00
7dcd14ef3a fix(service): Download a file if the file metadata has no path 2023-07-08 21:20:21 +02:00
4f033438ed feat(ui): Make the bar chart look prettier 2023-07-08 21:12:07 +02:00
21e40c38ca fix(ui): Only show the image if we're done loading 2023-07-08 21:05:08 +02:00
53e3b3c561 feat(ui): Prepare for selectively removing media files 2023-07-08 20:57:02 +02:00
d6416c77b8 fix(service): Fix crash when re-downloading a file 2023-07-08 20:36:30 +02:00
9ade143352 fix(ui): Make the "media bar" not red 2023-07-08 19:24:44 +02:00
dc05876ac7 feat(ui): Show a placeholder, when no shared media are available 2023-07-08 19:22:23 +02:00
d691d63353 fix(i18n): Make the sticker pack size string translatable 2023-07-08 19:09:26 +02:00
b666a06252 feat(ui): Reduce rounding of shared media items 2023-07-08 19:07:16 +02:00
27b209b4d8 fix(ui): Make storage strings translatable 2023-07-08 19:01:14 +02:00
d0cdb2cebe fix(service): Ensure that stickers always have a size != null 2023-07-08 17:28:46 +02:00
2123f5b51f feat(ui): Add a link to the stickers menu 2023-07-08 17:18:41 +02:00
93511da3f1 feat(ui): Refactor the bar chart into its own widget 2023-07-08 17:12:08 +02:00
267eef2a55 feat(ui,service): Show sticker packs' storage overhead 2023-07-08 15:01:45 +02:00
8f69b9ff3d fix(service): Exclude stickers from shared media 2023-07-08 14:35:15 +02:00
0d182cc4e5 feat(ui,service): Implement a bar chart showing storage usage 2023-07-08 14:31:26 +02:00
Ikjot Singh Dhody
06dcd5491b feat(all): Basic groupchat error handling set up.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-07-08 14:33:20 +05:30
Ikjot Singh Dhody
3641be4f56 feat(all): Join room functionality complete.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-07-08 00:43:01 +05:30
Ikjot Singh Dhody
18e28c3bbf feat(all): Groupchat service/bloc updated.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-07-08 00:11:21 +05:30
Ikjot Singh Dhody
62e39bf066 feat(all): Set base for groupchat implementation.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-07-07 12:52:48 +05:30
f6bfbff62c feat(ui): Add a parameter to showConfirmationDialog 2023-07-06 22:08:19 +02:00
dd0cb88841 feat(ui): Add a confirmation dialog 2023-07-06 22:04:27 +02:00
23ed1f6b1a feat(ui): Apply i18n to the deletion dialog 2023-07-06 21:58:59 +02:00
d12154b4ba fix(service): Fix a potential SQL injection 2023-07-06 21:52:22 +02:00
b625ee5c4e feat(service): Actually remove the files 2023-07-06 20:57:45 +02:00
4e48962fae feat(service): Write a query to find media files to delete 2023-07-06 12:48:31 +02:00
21832042df feat(ui): Make the storage page a bit prettier 2023-07-05 17:53:26 +02:00
d48c8371e4 feat(ui): Make the shared media list a bit prettier 2023-07-05 17:35:32 +02:00
67f6fb8236 feat(ui,service): Implement viewing all shared media files 2023-07-05 16:54:34 +02:00
369cc3e823 feat(ui,service): Implement showing how much storage we use 2023-07-05 14:19:56 +02:00
4857245a96 Merge remote-tracking branch 'weblate/master' 2023-07-04 18:03:30 +02:00
Vistaus
2f39d4b60b Translated using Weblate (Dutch)
Currently translated at 100.0% (7 of 7 strings)

Translation: Moxxy/Fastlane Metadata
Translate-URL: https://translate.codeberg.org/projects/moxxy/fastlane-metadata/nl/
2023-07-04 16:01:55 +00:00
Vistaus
0528aca3ad Translated using Weblate (Dutch)
Currently translated at 100.0% (273 of 273 strings)

Translation: Moxxy/Moxxy
Translate-URL: https://translate.codeberg.org/projects/moxxy/moxxy/nl/
2023-07-04 16:01:54 +00:00
d99e5d8801 Translated using Weblate (Dutch)
Currently translated at 100.0% (273 of 273 strings)

Translation: Moxxy/Moxxy
Translate-URL: https://translate.codeberg.org/projects/moxxy/moxxy/nl/
2023-07-04 16:01:54 +00:00
5cda06db24 fix(ui): Add missing languages to the locale menu 2023-07-04 18:01:51 +02:00
c93c3140cf chore(docs): Update CHANGELOG 2023-07-04 17:41:22 +02:00
f17bba7282 feat(ui): Parse legacy quotes
Fixes #226.
2023-07-04 17:32:03 +02:00
fe3dbd265e Merge pull request 'Implement sending of read receipts' (#293) from feat/read-markers into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/293
2023-07-04 12:53:07 +00:00
e6eee134bd fix(ui): Fix avatar for self-chat 2023-07-04 14:51:53 +02:00
1f5c75a647 chore(docs): Update CHANGELOG 2023-07-04 14:09:38 +02:00
d70527d65f feat(service): Move marking as read into MessageService 2023-07-04 14:06:55 +02:00
efb3f4b371 fix(ui): The message should be *just* visible 2023-07-04 13:47:41 +02:00
71f5e8f0b4 feat(ui,service): Implement dynamically sending read markers 2023-07-04 13:44:50 +02:00
a13bdd2e2b fix(i18n): Fix build issues with incomplete translations
Fallback to en as a locale, if a key does not exist. Also
fixes some whackiness from doing changes and not reflecting them back
to Weblate.
2023-07-03 22:34:54 +02:00
7fbc1ba812 fix(ui): Fix long-press requiring precise pointer placement 2023-07-03 16:59:08 +02:00
7f864f1d25 chore(docs): Mention new translations 2023-07-03 15:50:12 +02:00
ejix
ef3c11e870 Translated using Weblate (Russian)
Currently translated at 83.7% (227 of 271 strings)

Translation: Moxxy/Moxxy
Translate-URL: https://translate.codeberg.org/projects/moxxy/moxxy/ru/
2023-07-03 14:51:12 +02:00
Vistaus
c20bc964c3 Translated using Weblate (Dutch)
Currently translated at 100.0% (271 of 271 strings)

Translation: Moxxy/Moxxy
Translate-URL: https://translate.codeberg.org/projects/moxxy/moxxy/nl/
2023-07-03 14:51:12 +02:00
dd2629d073 Merge pull request 'Translations update from Weblate' (#290) from translate/moxxy:weblate-moxxy-moxxy into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/290
2023-07-03 12:45:35 +00:00
e5fa199925 fix(i18n): Make hard-coded string translatable 2023-07-03 14:43:15 +02:00
675a647a8d feat(ui): Show a toast when copying a message 2023-07-03 14:40:57 +02:00
f845c4134c fix(ui): Focus the textfield when editing a message 2023-07-03 14:36:18 +02:00
6442e9cab5 fix(ui): Make message corrections work again
Fixes #291.
2023-07-02 12:43:18 +02:00
0629a3d5bd fix(ui): Dismiss the keyboard on message selection 2023-07-01 22:03:25 +02:00
62d18588d7 chore(docs): Add the OMEMO fixes in the changelog 2023-07-01 22:00:28 +02:00
Codeberg Translate
4ee191e238 Translated using Weblate (Russian)
Currently translated at 57.3% (153 of 267 strings)

Added translation using Weblate (Russian)

Translated using Weblate (Dutch)

Currently translated at 100.0% (6 of 6 strings)

Translated using Weblate (Dutch)

Currently translated at 50.0% (3 of 6 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (267 of 267 strings)

Added translation using Weblate (Dutch)

Translated using Weblate (Japanese)

Currently translated at 19.8% (53 of 267 strings)

Added translation using Weblate (Japanese)

Co-authored-by: Codeberg Translate <translate@codeberg.org>
Co-authored-by: Vistaus <vistausss@fastmail.com>
Co-authored-by: ejix <ejix@zp1.net>
Co-authored-by: mmbd <stefan@kikeriki.at>
Translate-URL: https://translate.codeberg.org/projects/moxxy/fastlane-metadata/nl/
Translate-URL: https://translate.codeberg.org/projects/moxxy/moxxy/ja/
Translate-URL: https://translate.codeberg.org/projects/moxxy/moxxy/nl/
Translate-URL: https://translate.codeberg.org/projects/moxxy/moxxy/ru/
Translation: Moxxy/Fastlane Metadata
Translation: Moxxy/Moxxy
2023-07-01 19:51:23 +00:00
351de5ee93 feat(ui): Allow sending corrections for older messages 2023-07-01 21:47:36 +02:00
dae8ddb3b5 feat(shared): Migrate ConversationType to a JsonConverter 2023-07-01 21:38:19 +02:00
631a62e8ce feat(shared): Make more use of JsonConverters 2023-07-01 21:22:39 +02:00
59877a3e60 chore(all): Bump moxxmpp
Should fix errors when "fetching" avatars for JIDs where the
DiscoManager throws a more general StanzaError instead of a
DiscoError.
2023-07-01 13:21:15 +02:00
08da843d50 feat(ui,service): Merge adding a contact and "joining" groupchats
Even though groupchats are not implemented yet. This also handles
starting chats with JIDs, where the server cannot be reached/times out.
2023-07-01 13:11:56 +02:00
949781003a feat(docs): Add a link to the Weblate instance 2023-06-25 23:43:58 +02:00
4338c7a777 feat(ui,service): Refresh avatars using events 2023-06-24 21:09:48 +02:00
86de5cd22d feat(i18n): Rename English translation to support Weblate 2023-06-21 22:41:50 +02:00
bf754a4e51 feat(docs): Update DOAP 2023-06-21 20:37:44 +02:00
8913977c0a Merge pull request 'OMEMO Rework' (#286) from omemo-rework into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/286
2023-06-21 17:48:01 +00:00
f4be336e39 fix(service): Actual messages should come after the pseudo messages 2023-06-20 23:08:46 +02:00
836fe1bf87 fix(service): Fix message loading and receiving 2023-06-20 22:52:28 +02:00
7bca95203e feat(service,ui): Create pseudo messages for new/replaced ratchets 2023-06-20 17:19:46 +02:00
059a22cbe8 feat(service): Be smarter about creating pseudo messages 2023-06-20 16:42:15 +02:00
2740692772 feat(service): "Create" pseudo messages 2023-06-20 00:00:45 +02:00
c0008fece9 feat(ui,service): Minor changes
- Adjust UI functions to reflect that the ratchets are only with
  hasSessioWith = true
- Update moxxmpp to fix stream management reconnection loops
2023-06-19 23:21:21 +02:00
d7bb54a088 fix(service): Fix loading IK public keys as the wrong type 2023-06-19 14:35:42 +02:00
eb41b3f0f7 chore(all): Update moxxmpp and omemo_dart 2023-06-18 22:00:17 +02:00
e3cbc47cc8 feat(service): Create the new OMEMO tables at creation time 2023-06-18 20:19:17 +02:00
75767d26b4 chore(service,ui): Format 2023-06-18 20:14:03 +02:00
a01667a8f7 feat(service): Cleanup 2023-06-18 20:13:02 +02:00
e4dec4168c feat(service): Implement all old functionality 2023-06-18 20:07:50 +02:00
59f1a3a289 feat(service): Reimplement more old functionality 2023-06-18 19:56:19 +02:00
9c8aec6543 feat(service): MAKE OMEMO WORK FIRST TRY 2023-06-18 15:58:10 +02:00
7c8a368d73 chore(ui,service): Migrate to moxlib 0.2.0 2023-06-18 12:28:22 +02:00
0bda382e40 feat(service): Stub out the persistence layer 2023-06-18 00:22:52 +02:00
330b4dd69f feat(service): Adjust to (some) moxxmpp changes
This commit is broken.
2023-06-17 23:59:50 +02:00
7a7e43eb3c fix(service): Fix database creation 2023-06-13 17:40:53 +02:00
5e797d6b54 fix(service): Do not use contact avatar path when contact has no avatar 2023-06-11 22:58:58 +02:00
1b3dd0634b fix(service): Disabling contact integration removes pseudo roster items 2023-06-11 22:53:32 +02:00
b1bdacb834 feat(service): Implement FAST
Fixes #175 (with the previous commit).
2023-06-11 22:26:17 +02:00
a4b9485019 feat(service): Implement SASL2 with Bind2 2023-06-11 22:09:30 +02:00
20489fbb25 Merge pull request 'Improve avatar and presence handling' (#285) from feat/better-avatar-handling into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/285
2023-06-10 22:20:23 +00:00
a2fa000a31 feat(all): Improve presence subscription handling 2023-06-10 21:10:21 +02:00
343f0e7dae fix(service): Add missing OOB fallback 2023-06-09 00:11:27 +02:00
f0f13097c0 feat(all): Use CachedXMPPAvatar also for our own avatar 2023-06-08 23:53:37 +02:00
0025e83be5 fix(docs): Remove XEP-0054 from DOAP 2023-06-08 21:26:57 +02:00
ffb8e9f3fc feat(service): Hopefully improve roster handling 2023-06-08 21:21:24 +02:00
8081931c55 feat(service): Remove VCardManager 2023-06-08 18:06:06 +02:00
792276d06a fix(service): Get missing metadata 2023-06-07 23:47:21 +02:00
58edc256fd chore(all): Update moxxmpp to 0.4.0 2023-06-07 20:59:25 +02:00
f30d04a593 feat(service): Current state 2023-06-07 19:02:13 +02:00
cc42f32b21 feat(all): Move avatars to CachingXMPPAvatar 2023-05-29 18:49:15 +02:00
353623c5ae chore(all): Add a script to check formatting 2023-05-29 15:48:53 +02:00
a09c30a076 release: Hotfix 0.4.3 2023-05-29 15:41:59 +02:00
3bfd72fde1 fix(ui): Fix crash with plaintext messages
Fixes #284.
2023-05-29 15:23:27 +02:00
39e6b4a48b feat(all): Remove Flutter fork 2023-05-29 15:21:13 +02:00
32b2e35d42 feat(docs): Add fastlane changelog for 0.4.2 2023-05-26 21:34:48 +02:00
8e1bcbfd1d fix(all): Actually, build with debug info 2023-05-26 21:31:09 +02:00
336a6d56cd release: Release 0.4.2 2023-05-26 21:29:05 +02:00
a283454cae feat(ui): Move interactions with the FilePicker to a safe wrapper
Handle getting the storage permission ourselves. That way we can show
a toast, if we did not get the permission. Also fixes an observed
situation where FilePicker would crash in its native code due to not
having the storage permission.

Fixes #283.
2023-05-26 21:15:35 +02:00
8b16a8a37b chore(ui): Fix formatting error 2023-05-26 20:47:22 +02:00
727a1a3423 fix(ui,service): Fix self-chat files showing a spinner
Fixes #282.
Fixes #281.
2023-05-26 20:47:11 +02:00
0c42c117a0 chore(all): Bump flutter_secure_storage 2023-05-25 22:41:15 +02:00
d795cb717e feat(i18n): Translate missing profile strings 2023-05-25 21:41:34 +02:00
1d5d1fdf86 fix(ui): Fix keyboard dodging too much in certain situations 2023-05-25 21:34:07 +02:00
d795c34dab feat(service): Do not request storage permission 2023-05-25 14:43:37 +02:00
b38f5c139f chore(all): Bump moxxmpp 2023-05-25 12:47:34 +02:00
b623f32fbf feat(all): Bump moxxmpp
- Bump moxxmpp to allow queueing stanzas that are sent offline
- Should fix #75.
2023-05-24 22:53:24 +02:00
19fd079436 chore(all): Bump moxxmpp 2023-05-23 16:01:40 +02:00
7d70a96533 fix(ui): Add bottom padding to a sticker pack's name
Also replace the weird list with a GroupedListView.
2023-05-22 14:19:05 +02:00
dce6e34289 fix(ui): Fix padding in the new conversations page
Fixes #276.
2023-05-22 13:51:20 +02:00
881f080916 Merge pull request 'Rework the conversation page' (#275) from chore/conversation-rewrite into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/275
2023-05-21 21:56:41 +00:00
051687535b fix(ui): Fix weird padding in the conversation topbar 2023-05-21 23:56:30 +02:00
0b420933e0 fix(ui): Improve contrast in dark mode 2023-05-21 23:48:42 +02:00
0b3876c3f0 feat(ui): Use the same selection effect for the share selection 2023-05-21 23:40:19 +02:00
9711d45a7a chore(ui): Rename overview_menu.dart -> context_menu.dart 2023-05-21 23:36:31 +02:00
8dcba94de7 feat(ui): Improve the selection of conversation items 2023-05-21 23:34:09 +02:00
226dca8c1a feat(ui): Implement the new typing animation 2023-05-21 13:01:18 +02:00
ad01a7e3e3 feat(ui): Re-add audio messages 2023-05-20 15:56:24 +02:00
adde5a4134 feat(ui): Re-implement the sticker picker 2023-05-19 19:52:22 +02:00
9ae1807225 chore(ui): Clean-up the selection effect 2023-05-19 13:56:44 +02:00
e7f8446c02 feat(ui): Implement a prettier overview animation 2023-05-17 21:27:58 +02:00
7b05bf200c feat(ui): Implement tailor-made keyboard dodging 2023-05-16 23:42:19 +02:00
e992cb309f chore(all): Bump moxxmpp 2023-05-16 13:46:52 +02:00
0f138678ec fix(ui): Fix a sticker as the first message not appearing
Fixes #274.
2023-05-16 12:35:35 +02:00
35658e611a fix(ui): Do not clear the conversation on exit
Fixes #262.

Since we no longer query the BLoC whether we should send a chat state
notification, but instead ask the controller, we can safely remove
the clearing of the `conversation` field.
2023-05-16 12:18:46 +02:00
2a25cd44cf fix(service): Fix invalid hashes being sent with stickers
Fixes #273.

Also fixes:
- Weird (wrong) serialization of the hash maps
- An issue with migrations when passing a const list

NOTE: If you ran Moxxy between the merge of #267 and this commit, you
have to remove Moxxy's data and start anew.
2023-05-16 01:07:40 +02:00
29053df245 chore(ui): Completely rework the BorderlessTopbar
Fixes #249.
2023-05-15 14:39:28 +02:00
78ad02ec80 feat(ui,service): Allow replying with a sticker
Fixes #270.
2023-05-15 12:57:58 +02:00
e3f2ef22a6 fix(ui): Update the conversation list when an upload failed
Fixes #271.
2023-05-15 11:09:03 +02:00
f884e181e3 fix(ui): Sticker quotes now say "Sticker"
Fixes #268.
2023-05-15 00:07:51 +02:00
e69d7ed0a2 feat(ui): Make quote roundings better
Fixes #269.
2023-05-14 23:54:46 +02:00
d65e11a3ea feat(ui): Prepend a "You:" when the last message was sent by us
Fixes #242. Reference #258. Thanks!
2023-05-14 23:06:17 +02:00
294d0ee02c Merge pull request 'Improve the database' (#267) from feat/message-rework into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/267
2023-05-14 20:51:45 +00:00
6f4abebb32 feat(service): Make adding migrations less of a hassle 2023-05-14 18:07:08 +02:00
5d83796b37 chore(service): Move methods into their respective services 2023-05-14 17:37:02 +02:00
a06c697fe3 fix(service): Remove the 'reactions' column 2023-05-14 16:45:18 +02:00
5de2a8b6af fix(service): Fix migration 2023-05-14 15:07:40 +02:00
7234f67c42 chore(ui): Fix formatting issue 2023-05-14 14:19:41 +02:00
972f5079f9 feat(service): Create indices for common queries 2023-05-14 14:18:56 +02:00
27d4ed1781 feat(ui): Test that padding the reactions list works 2023-05-14 12:26:25 +02:00
5f074ef695 feat(ui): Make the reactions preview slightly transparent 2023-05-14 12:10:44 +02:00
d0f60519fd feat(ui): Fill in the reaction list with actual data 2023-05-14 12:02:38 +02:00
cd7c495cb7 feat(ui): Show 'You' if a reaction came from us 2023-05-14 00:46:54 +02:00
59317d45f9 chore(ui): Move the reaction bubble colors to the constants 2023-05-14 00:18:25 +02:00
7c2c9f978d chore(shared): Fix naming issue in the reaction model 2023-05-14 00:13:51 +02:00
d540f0c2f2 chore(ui): Add documentation 2023-05-14 00:07:59 +02:00
340bbb7ca8 feat(ui): Make reactions look prettier 2023-05-14 00:00:11 +02:00
0aaffd1249 fix(ui,service): Fix linter issues 2023-05-13 22:14:14 +02:00
04be2e8c88 fix(service): Fix removing a reaction 2023-05-13 22:12:05 +02:00
57dbe83901 fix(service): Make adding a reaction work 2023-05-13 22:07:17 +02:00
60c5328eb0 feat(ui,service): Fix linter issues 2023-05-13 21:58:24 +02:00
189d9ca9cd feat(ui): Allow picking a new reaction 2023-05-13 21:48:11 +02:00
5d797b1e66 fix(ui): Make only our own reactions clickable 2023-05-13 20:55:01 +02:00
2f1a40b4d9 feat(ui,service): Allow requesting the reactions on a given message 2023-05-13 20:50:46 +02:00
02c0cd5af0 feat(ui,service): Make reactions work again 2023-05-12 23:35:27 +02:00
f2a70cd137 feat(service): Remove shared media table and attribute 2023-05-11 21:12:36 +02:00
8d88c25f05 feat(ui): Make the shared media list look nicer 2023-05-11 21:06:23 +02:00
c1c5625441 chore(ui,service): Format and lint 2023-05-11 20:36:35 +02:00
462e800907 fix(service): Fix TODOs 2023-05-11 20:35:15 +02:00
faa5ee2c4f feat(ui,service): Implement paged shared media requests 2023-05-11 16:33:55 +02:00
5dad5730ce fix(ui): If an image has no size, decode it full 2023-05-10 13:43:58 +02:00
5017187927 fix(service): Fix file uploading 2023-05-10 13:41:12 +02:00
14e7f72bd3 feat(ui): SHow an error icon if the last message has an error 2023-05-10 12:27:19 +02:00
9ef67f5788 feat(service): Untested work on file sending 2023-05-10 12:26:37 +02:00
79226f6ca8 fix(service): Fix downloading a file again 2023-05-10 00:42:09 +02:00
c8c0239e36 feat(tests): Add tests for getPrefixedSubMap 2023-05-10 00:34:07 +02:00
f1be10bf8c fix(service): Fix database loading 2023-05-10 00:24:39 +02:00
18c3c9d324 feat(all): Use stickers as an extension of SFS 2023-05-09 21:24:20 +02:00
4825fe881d fix(ui): Fix not downloaded files having no background color 2023-05-08 23:57:57 +02:00
081d20fe50 fix(all): Format and lint 2023-05-08 23:57:37 +02:00
c1a66711db feat(service): Verify hash after download 2023-05-08 21:15:54 +02:00
b113e78423 feat(service): Create hash pointers only after integrity checks 2023-05-08 13:27:06 +02:00
470e8aac9c feat(service): Guard against empty SFS source lists 2023-05-08 13:11:03 +02:00
39babfbadd fix(service): Fix wrong type when querying file metadata 2023-05-08 13:08:25 +02:00
86f7e63f65 fix(service): Append the extension to the saved filename 2023-05-08 00:00:54 +02:00
ecd2a71981 feat(ui,service): Port UI and fix first bugs 2023-05-07 22:51:06 +02:00
2ece9e6209 feat(service): Try to bring over the service 2023-05-07 16:56:27 +02:00
9310b9c305 chore(all): Bump moxxmpp 2023-05-07 00:19:09 +02:00
abad9897b8 fix(service): Use database table constants 2023-04-09 13:27:16 +02:00
0cfffff94c chore(all): Bump moxxmpp 2023-04-09 13:21:47 +02:00
6c53103345 chore(service): Bump moxxmpp 2023-04-07 23:18:52 +02:00
346ef66bca chore(all): Update moxxmpp to 0.3.1 2023-04-06 15:30:50 +02:00
e092201030 feat(ui): If possible, decode shared images at a lower resolution 2023-03-21 22:45:04 +01:00
3c14521ca0 feat(service): Log the HTTP status when an upload fails 2023-03-21 20:17:08 +01:00
4b43427bf0 fix(ui): Fix color of block/add buttons 2023-03-21 20:03:40 +01:00
b7f39fe8ed chore(service): Format the database migrations again 2023-03-21 19:56:20 +01:00
1f64569bc2 fix(service): Fix locking the UI when importing broken sticker packs
Fixes #241.
2023-03-21 19:56:14 +01:00
7c56383601 chore(all): Lint and format tests and migrations 2023-03-21 16:32:34 +01:00
2de50b012b fix(service): Fix broken migration 2023-03-21 16:17:56 +01:00
1de90e3ce1 feat(docs): Explain the commit message format 2023-03-21 12:18:19 +01:00
64a175819f chore(meta): Bump moxxmpp 2023-03-21 12:02:36 +01:00
4cc507832c chore(service): Bump moxxmpp
Fixes #248.
2023-03-20 13:07:15 +01:00
fd1e14e4cd Merge pull request 'Note to self feature addition' (#256) from ikjot-2605/moxxy:feature_note_to_self into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/256
2023-03-20 11:52:25 +00:00
Ikjot Singh Dhody
a78db354ab feat(notes): Handle reactions correctly for notes.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-03-20 13:41:14 +05:30
Ikjot Singh Dhody
a86d83eeba feat(notes): Fix upload logger.
Consistent method for checking note vs chat conversation.

Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-03-19 00:56:45 +05:30
Ikjot Singh Dhody
02e73ade5e feat(notes): Utilize converation object for notes.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-03-18 15:33:17 +05:30
9d0a84b317 fix(ui): Fix text overflow when quoting a file
Fixes #261.
2023-03-17 21:28:32 +01:00
Ikjot Singh Dhody
0cf237914b feat(notes): Update read marker cleaner for notes.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-03-17 03:58:47 +05:30
Ikjot Singh Dhody
398c23fccb feat(notes): Remove unneccessary changes.
Remove unneccessary "sent" changes in file widgets.

Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-03-17 03:41:52 +05:30
8f68292dfd fix(ui): Fix UI crash when less than 8 shared media items are available 2023-03-16 17:09:58 +01:00
Ikjot Singh Dhody
8ef62e7ff1 feat(notes): Read/Encrypted Markers for Notes.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-03-16 00:58:41 +05:30
Ikjot Singh Dhody
99257f4b28 feat(notes): Disable Chat State/File Upload-Notes
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-03-15 23:51:48 +05:30
ikjot-2605
9f529a3a1c Merge branch 'master' into feature_note_to_self 2023-03-15 17:00:04 +00:00
8178a0dd8a fix(ui): Remove the quote *AFTER* sending the message 2023-03-15 12:36:36 +01:00
0f250b6eae fix(ui): Sending a message does not reset the quoted message 2023-03-15 12:34:04 +01:00
716579cc5e chore(ui): Remove unused import 2023-03-15 12:33:47 +01:00
25caf3f4a6 fix(ui): Use the filename attribute also in quotes 2023-03-15 12:29:02 +01:00
Ikjot Singh Dhody
1c1b598768 feat(notes): Fix quoted file widget.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-03-15 02:57:41 +05:30
ikjot-2605
7cbb56dc2c Merge branch 'master' into feature_note_to_self 2023-03-14 21:25:11 +00:00
7f41ec2aac fix(ui): Always directly use the filename attribute in media widgets 2023-03-14 21:13:20 +01:00
Ikjot Singh Dhody
ac5fc38de6 feat(notes): Formatting changes.
Update file.dart to work when unified filename is used.

Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-03-15 00:45:10 +05:30
1f3c568d0c chore(ui): Restructure the message widget directory
Fixes #257.
2023-03-14 00:28:20 +01:00
Ikjot Singh Dhody
2a186377df feat(notes): Add edit note capability.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-03-13 06:42:42 +05:30
Ikjot Singh Dhody
d529974cd9 feat(notes): Hide forward option for notes.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-03-13 06:32:28 +05:30
Ikjot Singh Dhody
f378c60bf5 feat(notes): Handle message/note retractions.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-03-13 06:28:40 +05:30
Ikjot Singh Dhody
e4523a2d33 feat(notes): Handle file upload for notes.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-03-13 06:24:55 +05:30
Ikjot Singh Dhody
4aacd36c59 feat(notes): Add switch case for helper.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-03-13 05:18:00 +05:30
Ikjot Singh Dhody
a291d9ab07 feat(notes): Database migration-Conversation type.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-03-13 04:55:52 +05:30
Ikjot Singh Dhody
9d73fc3a94 feat(notes): Miscellaneous fixes - Review 1.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-03-13 04:43:20 +05:30
Ikjot Singh Dhody
8a33d88e31 feat(notes): German translation - SpeedDialChild.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-03-13 00:03:48 +05:30
Ikjot Singh Dhody
6650686d48 feat(notes): Update ConversationTopBar for Notes.
For notes, don't show block user, add to contacts row.

For notes, don't give encryption options, and don't allow to block user.

Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-03-12 23:44:47 +05:30
Ikjot Singh Dhody
8570997cb0 feat(notes): Remove moxxmpp implementation - Note.
Message stanza not sent for Notes to Self.

Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-03-12 23:15:12 +05:30
Ikjot Singh Dhody
31ee7b919b feat(notes): Diasble Notification Service for Note
Format SpeedDialChild for Conversation.

Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-03-10 13:44:26 +05:30
Ikjot Singh Dhody
30f6ecd2f8 feat(notes): Fix enum decoding - ConversationType.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-03-10 13:32:19 +05:30
Ikjot Singh Dhody
9e3700001d feat(notes): Add 'type' argument to Conversation.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-03-08 17:50:55 +05:30
Ikjot Singh Dhody
2928602e8d feat(notes): Remove JAVA_HOME override from gradle
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-03-07 21:00:31 +05:30
Ikjot Singh Dhody
09fc55d2c7 Merge branch 'master' into feature_note_to_self
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-03-07 20:58:44 +05:30
b391425d48 fix(docs): Fix grammar 2023-03-07 12:32:45 +01:00
3b21486647 fix(style): Remove some comprehensions 2023-03-07 12:13:18 +01:00
641ac01b33 fix(docs): Fix formatting of checklist 2023-03-07 12:00:15 +01:00
233370b448 fix(style): Ensure commit message starts with uppercase 2023-03-07 11:55:44 +01:00
45bff04329 feat(docs): Add a contribution document (#252) 2023-03-07 11:54:46 +01:00
6d32387e6c feat(shared): Remove the Semaphore (#255) 2023-03-07 00:29:20 +01:00
4f51cf1f80 feat(ui): Remove empty files 2023-03-07 00:28:13 +01:00
46f7e5beaa fix(meta): Reformat code 2023-03-06 23:38:14 +01:00
fee39f56fa fix(meta): Fix linter warnings 2023-03-06 23:35:08 +01:00
a3e8758dbd Merge pull request 'feat: use dart format to format code' (#254) from coder-with-a-bushido/moxxy:dartformat-patch into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/254
2023-03-06 19:27:23 +00:00
Ikjot Singh Dhody
2b6ed19847 feat(notes): Make JID "" for notes conversation.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-03-06 18:47:31 +05:30
Ikjot Singh Dhody
34971950ad feat(notes): Add notes to self SpeedDialChild
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-03-06 16:43:48 +05:30
Karthikeyan S
29b22b7dd9 feat: use dart format to format code 2023-03-06 10:19:10 +05:30
8bc4771345 fix(ui): "non-colored" bottom icons should always be white 2023-03-04 13:10:48 +01:00
314c8f8d18 fix(service): Fix custom update and return routine
Use sqflite's SqlBuilder to build the queries. This
is much less error prone that what I did before.
2023-03-03 13:58:31 +01:00
dd3e47e492 feat(service): Utilise Sqlite's RETURNING statement
Fixes #247.
2023-03-03 13:13:36 +01:00
7f90f3315a docs(meta): Add common build issues 2023-03-02 11:39:35 +01:00
ceb43c0f0f feat(ui): Use GridViews in more places 2023-03-01 22:00:05 +01:00
e225cab90a fix(ui): Only show the summary item when we have >= 8 shared media items 2023-03-01 18:09:17 +01:00
87793a032c feat(service): Cache the first message page for 5 conversations (#166) 2023-03-01 17:46:32 +01:00
b3227129d5 fix(ui): Fix unread badges 2023-03-01 17:22:17 +01:00
5861c7f8cb feat(meta): Update to Flutter 3.7.3 2023-03-01 13:29:20 +01:00
1181d1c526 Merge pull request 'Paginate messages and shared media' (#239) from feat/message-pagination into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/239
2023-02-26 21:13:12 +00:00
b3c02324aa feat(service): Remove the message cache 2023-02-26 18:54:20 +01:00
3664b5f8c5 fix(service): Fix the display of shared media 2023-02-23 21:09:55 +01:00
d58bf448ef feat(ui): Replace Wrap with a GridView 2023-02-23 17:09:09 +01:00
95d1e1ed38 feat(meta): Paginate shared media loading 2023-02-23 16:59:15 +01:00
bbaa41f389 feat(meta): Implement the last missing message pagination code 2023-02-22 11:46:29 +01:00
20bff17c74 chore(meta): Fix linter issues 2023-02-21 22:59:41 +01:00
31a7d18905 feat(ui): Move a lot of code into a generic controller 2023-02-21 22:47:03 +01:00
c4f04b73be feat(shared): Rename constant 2023-02-21 15:09:51 +01:00
188c6199c9 fix(ui): Show sent stickers 2023-02-20 15:56:12 +01:00
62413eb8e4 feat(ui): Decode images at a lower resolution 2023-02-19 20:57:05 +01:00
1c4697caa7 fix(ui): Sticker sending 2023-02-19 20:44:53 +01:00
785272ba21 feat(ui): Remove MessageUpdatedEvent 2023-02-19 20:41:48 +01:00
d28e669b5f fix(ui): Bubble corner radius 2023-02-19 20:39:28 +01:00
fe3b07aa2f fix(ui): Message reactions 2023-02-19 20:32:48 +01:00
a21ecf9bbf fix(ui): Retracted messages have emoji-only font size 2023-02-19 20:22:20 +01:00
55113543dd fix(ui): Message retraction 2023-02-19 20:20:06 +01:00
76041671eb fix(ui): Handle chat states 2023-02-19 20:16:07 +01:00
be2d4ec29f feat(ui): Implement message updating 2023-02-19 20:01:08 +01:00
dfa221768c feat(ui): Handle new messages 2023-02-19 19:52:48 +01:00
9b2278a0ff feat(ui): Move pickerVisible into the controller 2023-02-19 18:22:31 +01:00
24b0a0c7bb feat(ui): Remove obsolete code from the BLoC 2023-02-19 18:02:22 +01:00
023ad574a8 fix(ui): Fetch our own JID 2023-02-19 17:55:37 +01:00
74772dc6b5 fix(ui): Fix the microphone button not disappearing 2023-02-19 17:50:42 +01:00
8fc7734827 fix(ui): Implement quoting a message 2023-02-19 17:40:52 +01:00
43659b01bd feat(meta): Fix scrollToBottom and message sending 2023-02-19 17:27:12 +01:00
de2e2f3987 feat(meta): Paginate message requests 2023-02-18 21:02:55 +01:00
28591a6787 feat(ui): Render date bubbles in a less stupid way 2023-02-18 16:43:24 +01:00
e78dae0950 fix(service): Fix crash on startup 2023-02-10 19:52:34 +01:00
5b86f69444 fix(ui): Clamp text quotes to 2 lines maximum
Fixes #237.
2023-02-10 19:37:00 +01:00
92a7d30e43 feat(service): Improve and fix the API for ConversationService 2023-02-03 20:33:04 +01:00
fa311bfb95 feat(service): Remove autoAcceptSubscriptionRequests 2023-02-03 17:07:03 +01:00
c1988a9bcd chore(meta): Rework the conversation service
- Conversations are now uniquely identified by their JID instead of some
  ID
- The ConversationService's cache is now guarded by a lock
2023-02-03 12:15:49 +01:00
27185b21b5 feat(i18n): Devices -> Security 2023-02-01 13:57:14 +01:00
bad4295aec feat(ui): Show something when we have no sessions with a JID 2023-02-01 13:55:02 +01:00
b891f29e11 feat(meta): Remove explicit subscription management 2023-01-31 16:56:14 +01:00
35a752e565 feat(ui): Auto-accept subscription requests if the JID is in the roster 2023-01-31 12:33:39 +01:00
6c5189744a fix(ui): Fix off-by-one error 2023-01-31 12:20:56 +01:00
81e9a7d420 feat(meta): Integrate subscription requests 2023-01-30 21:27:05 +01:00
3a01025471 feat(service): Log why OMEMO publishing fails 2023-01-30 17:31:13 +01:00
e652ecca44 fix(ui): Make the first message appear at the top of the screen 2023-01-30 17:30:55 +01:00
c244d54d22 fix(ui): Keep the intro page behind the login page 2023-01-30 17:03:36 +01:00
cff9000d6b fix(ui): Back button's splash is behind the bar 2023-01-30 17:03:14 +01:00
dc8804de3a fix(ui): Fix logging out not navigating correctly 2023-01-30 16:59:04 +01:00
92467630cd feat(ui): Use flutter_list_view for the message list
This allows us to eventually jump to list indices.
Closes #232.
Also adds a missing date bubble at the top.
2023-01-29 16:23:46 +01:00
452734a433 chore(service): Rename init to initialize 2023-01-29 15:38:54 +01:00
49c7b18d57 fix(service): Stop no connection timer when we lost network connectivity 2023-01-29 15:36:20 +01:00
f7665403b9 chore(service): Move XmppState into its own service 2023-01-28 20:45:51 +01:00
9ae047b2d0 chore(service): Use direct initialization 2023-01-28 20:19:30 +01:00
4523d87028 chore(service): Move createFallbackBodyForQuotedMessage into helpers 2023-01-28 20:14:47 +01:00
c34c0ffd0f feat(service): Show notification on unrecoverable error 2023-01-28 20:12:27 +01:00
a179d0f6cc chore(meta): Use moxxmpp 0.2.0 2023-01-27 22:04:47 +01:00
6c1b7c54d0 feat(meta): Bump moxxmpp 2023-01-27 19:23:08 +01:00
bbb59ac2cc fix(service): Remove workaround 2023-01-27 19:22:27 +01:00
f16d33decd feat(service): Adapt to moxxmpp changes 2023-01-27 00:15:12 +01:00
c4e5504c1d feat(service): Exclude roster and Carbons from OMEMO 2023-01-23 13:03:31 +01:00
0fb8230e50 feat(service): Ignore key exchange errors
Fixes #228.
2023-01-23 11:54:21 +01:00
86be724246 feat(meta): Bump moxxmpp and omemo_dart
Should fix some OMEMO issues that have crept up.
2023-01-22 19:27:41 +01:00
27b3ad0da5 fix(meta): Mark XEP-0384 as complete 2023-01-21 21:17:02 +01:00
25167ed078 fix(meta): Fix every message being an error 2023-01-21 20:48:58 +01:00
7fb0cf139b fix(ui): Fix crash when tapping notification while app is dead
Fixes #217.
2023-01-21 20:41:51 +01:00
6e8d54c91b Merge branch 'Poussinou-master' 2023-01-21 19:46:33 +01:00
a6191fd8af fix(docs): Revert badge formatting 2023-01-21 19:46:09 +01:00
bfeea6ffa5 fix(ui): Stop sending chat states for not focused chats
Fixes #221.
2023-01-21 19:45:27 +01:00
48451385e9 feat(service): Treat acknowledged as displayed and received 2023-01-21 19:45:27 +01:00
0e894f84cc fix(i18n): Translate forgotten string 2023-01-21 19:45:27 +01:00
0ca12232a8 fix(meta): Show an error (again) if contact does not support OMEMO:2
Fixes #195.
2023-01-21 19:45:27 +01:00
c2d28efe62 fix(ui): Stop sending chat states for not focused chats
Fixes #221.
2023-01-21 16:07:32 +01:00
0496c38496 feat(service): Treat acknowledged as displayed and received 2023-01-21 15:48:39 +01:00
dd4c481c4f fix(i18n): Translate forgotten string 2023-01-21 15:48:21 +01:00
7f1b5233e8 fix(meta): Show an error (again) if contact does not support OMEMO:2
Fixes #195.
2023-01-21 15:43:36 +01:00
Poussinou
41aae3cab9 Mise à jour de 'README.md' 2023-01-21 10:50:43 +00:00
9838fbc95f fix(ui): Bind the displayed version to the pubspec
Adds a new builder to moxxyv2_builders that just extracts
the version string from the pubspec.yaml.
2023-01-20 14:29:10 +01:00
f5c59823bf fix(ui): Closing chat does not navigate back 2023-01-18 22:17:03 +01:00
241a8b4d53 release(meta): Release 0.4.1 2023-01-18 21:39:35 +01:00
25d193e930 feat(meta): Add a build script 2023-01-18 21:19:20 +01:00
e6924cc02d fix(ui): Rearange the settings page a bit 2023-01-18 20:19:34 +01:00
60985c6b37 feat(ui): Hide testing commands outside of debug mode 2023-01-18 20:14:26 +01:00
a015399b57 fix(ui): Allow users to unlock the developer options
Fixes #211.
2023-01-18 20:10:01 +01:00
4b6c7998f3 fix(meta): Sharing now also works when the app is closed
Fixes #218.
2023-01-18 15:05:56 +01:00
26312e313f feat(meta): Bump moxxmpp 2023-01-15 00:55:47 +01:00
b63b5d7fd2 fix(service): Fix stanza correlation when from is missing
Fixed by bumping moxxmpp.
2023-01-14 16:30:22 +01:00
ca2943a94d feat(ui): Hide the speed dial when recording an audio message 2023-01-14 12:59:41 +01:00
32a4cd9361 feat(meta): Bump moxxmpp and moxxmpp_socket_tcp 2023-01-14 12:57:35 +01:00
2320e4ed17 fix(service): Remove weird newline 2023-01-14 12:44:46 +01:00
dee479a918 fix(ui): Move the DragTargets more to the left 2023-01-13 23:05:15 +01:00
6895ef1e32 feat(ui): Move the send button back to a speed dial
This makes the voice message UX more like what Signal and co. do.
Also makes the message TextField less crowded. Kind of fixes #207.
2023-01-13 23:03:02 +01:00
5c51eefa3e fix(i18n): Add missing string 2023-01-13 18:58:12 +01:00
0d7ae321a7 feat(ui): Improve the look of the message input field 2023-01-13 18:55:16 +01:00
b4063a64e0 fix(service): Await future 2023-01-13 18:20:04 +01:00
65154f2f5c feat(ui): Rework the file widget 2023-01-13 18:18:22 +01:00
19a22bd0d1 fix(ui): Fix text overflow for the file widget 2023-01-13 17:59:00 +01:00
a7da7baf5a feat(meta): Bump moxxmpp 2023-01-13 17:48:50 +01:00
a344a94112 fix(xmpp): Fix quotes being cut off
Fixes #203.
2023-01-13 13:41:37 +01:00
f44861fead feat(ui): The quote bubble base only depends on the surrounding bubble
Also adds an indicator as to who send a message. Fixes #213.
2023-01-13 00:49:56 +01:00
1c4a30ebb4 fix(ui): Show a text when no sticker packs are installed 2023-01-12 23:53:27 +01:00
70e2ca3d3e fix(ui): Fix some non-occurences of pickerHeight 2023-01-12 23:46:37 +01:00
0d4aee1625 feat(ui): Merge the emoji and the sticker picker
Fixes #209.
2023-01-12 23:44:31 +01:00
ad6aa33b7c fix(ui): Date bubbles' text is always black 2023-01-12 21:18:59 +01:00
284b5fa4df feat(ui): Make the bottom backdrop transparent 2023-01-12 21:16:14 +01:00
b9aac0c3d7 fix(service): Fix file uploads and downloads 2023-01-12 21:09:19 +01:00
6ce90e08ef fix(ui): NPE when a media message has not been downloaded 2023-01-11 17:49:04 +01:00
5ac80d8d60 fix(ui): Fix smaller code issues 2023-01-11 17:48:57 +01:00
56e1fa52d8 feat(ui): Make quotes look nicer 2023-01-11 17:44:51 +01:00
3ae1b7d168 fix(ui): Improve the contrast of the fallback avatar letters
Fixes #206.
2023-01-11 17:17:25 +01:00
d8f654c81c feat(ui): Remove the shadow of the TextField
Fixes #208.
2023-01-11 16:56:49 +01:00
cbcbd4d6dc fix(ui): Remove the use of a Stack inside the quote base
This makes the code feel nicer and also fixes #204 since Flutter
can now use the IconButton's dimensions for layouting and size
computations.
2023-01-10 18:15:58 +01:00
be899b5611 feat(ui): Small color improvements 2023-01-10 17:47:16 +01:00
361bbe8d85 fix(meta): Bump moxxmpp to fix SM 2023-01-09 13:49:43 +01:00
1e017af277 fix(service): Fix only the first roster item being added to the database 2023-01-07 22:23:50 +01:00
c4c22a36bb fix(service): Fix OMEMO device generation 2023-01-07 20:53:29 +01:00
84924b480b feat(service): Call omemo_dart's onNewConnection 2023-01-05 15:22:30 +01:00
358074f4ee fix(service): Generating OMEMO keys failed 2023-01-05 12:39:41 +01:00
084314fbcf fix(ui): Fix version number 2023-01-05 12:36:58 +01:00
c42f301ae0 fix(ui): Fix using the wrong color in text quotes
Fixes #196.
2023-01-05 12:36:03 +01:00
c8cd37e451 release: Tag version 0.4.0 2023-01-02 21:13:08 +01:00
9f8f3a5407 fix(meta): Fix fresh/migrated version hickups 2023-01-02 21:11:49 +01:00
6f1493808f feat(ui): Move the bubbles into their own directory 2023-01-02 19:01:55 +01:00
c9d32694db fix(i18n): Translate forgotten strings 2023-01-02 18:59:28 +01:00
8632a2fc81 fix(ui): Finally fix message bubbles? 2023-01-02 18:59:08 +01:00
46a09d5b62 feat(service): Manage sticker pack privacy
Fixes #192.
2023-01-02 18:04:27 +01:00
b7e5bbc7d2 fix(service): Fix avatars sometimes being not available 2023-01-02 17:38:22 +01:00
ed264f0c16 fix(service): Fix 'ghost' devices appearing 2023-01-02 17:19:23 +01:00
f1820575ad feat(ui): Show the ink splash on new device messages 2023-01-02 17:12:50 +01:00
d2e42d0a3c feat(meta): Show a message if a contact adds a new device 2023-01-02 15:19:08 +01:00
842cf5aaaa fix(service): Maybe fix avatar fetching crashes 2023-01-02 14:02:13 +01:00
c8f727e982 feat(meta): Update moxxmpp and omemo_dart 2023-01-02 14:00:21 +01:00
fd3c9190de Merge pull request 'Migrate to OmemoManager API' (#194) from feat/omemo-improvement into master
Reviewed-on: https://codeberg.org/moxxy/moxxyv2/pulls/194
2023-01-01 19:09:17 +00:00
69439d2b13 feat(meta): Remove commented-out omemo_dart override 2023-01-01 19:36:58 +01:00
6d41fee73f feat(meta): Lockfile update 2023-01-01 18:24:13 +01:00
0de99adeed fix(service): Adjust to new omemo_dart API 2023-01-01 18:22:41 +01:00
f71fd7c82c feat(meta): Update omemo_dart and moxxmpp 2023-01-01 18:22:25 +01:00
0a6b0b8fa5 feat(service): Migrate to new omemo_dart design 2023-01-01 15:02:35 +01:00
5e0ce8f098 fix(ui): Only show stickers that are images 2022-12-27 12:51:23 +01:00
9fc5989bd4 feat(ui): Add an assertion for adding a contact 2022-12-25 13:20:13 +01:00
cbe81861a5 fix(service): Fix avatars being empty when OMEMO is enabled 2022-12-25 13:09:24 +01:00
76a03cc2fa feat(service): Rework the blocklist service
Maybe fixes #14.
2022-12-25 01:25:12 +01:00
3774760548 fix(service): Fix missing type 2022-12-24 22:33:54 +01:00
4b1942b949 fix(ui): Move the date bubbles out of the chat bubble 2022-12-23 14:45:32 +01:00
Millesimus
2f03c02b58 fix(service): Remove unnecessary dio error handling. 2022-12-22 20:28:26 +01:00
Millesimus
639143934f Chore(service): A little tidying. 2022-12-22 20:28:26 +01:00
Millesimus
81bbbcd8e4 Fix(service): Re-enable progress indication using a completer. 2022-12-22 20:28:26 +01:00
Millesimus
bedd46756d Fix(service): flatten memory usage of downloads (sacrificing download progress indication). 2022-12-22 20:28:26 +01:00
Millesimus
bb6b342d82 Fix(service): flatten memory usage of uploads. 2022-12-22 20:28:17 +01:00
b6eb12cf30 feat(ui): Replace settings_ui with custom UI elements 2022-12-22 13:39:07 +01:00
80f8129011 feat(ui): Fix dialog corner radius and make barrier dismissible 2022-12-22 12:20:57 +01:00
86daad2455 feat(ui): Replace the modal with a dialog 2022-12-22 00:49:01 +01:00
e71cbd5ba9 fix(ui): Translate the "Shared media" in the profile
Also left-align the title.
2022-12-22 00:22:06 +01:00
c0fb9beef7 feat(ui): Replace the squircles with a simple list 2022-12-22 00:16:14 +01:00
db4b69a24a feat(ui): Also make the profile picture summon the profile page 2022-12-21 23:41:18 +01:00
7746784949 fix(ui): Fix InkWell overflowing for shared media 2022-12-21 23:13:28 +01:00
024bd48aba fix(ui): Fix InkWell padding for files 2022-12-21 22:45:18 +01:00
cb13c9faa4 feat(ui): Show a toast when a file cannot be opened 2022-12-21 22:39:50 +01:00
009ec759a3 feat(ui): Make quotes look better 2022-12-21 21:00:08 +01:00
6ba16ad020 feat(ui): Make the file widget look better 2022-12-21 20:54:15 +01:00
43b0b34cdd fix(service): Fix crash when conversation partner has no OMEMO:2 devices 2022-12-21 20:29:54 +01:00
94e6eb2d10 fix(ui): Scroll to bottom does not respect emoji/sticker picker 2022-12-21 20:27:48 +01:00
578eea5d9f fix(ui): Fix missing space 2022-12-21 20:15:18 +01:00
724450e049 fix(ui): Use auto_size_text to prevent fingerprints from overflowing 2022-12-21 20:07:34 +01:00
1759baebad feat(ui): Display something when no sticker packs are installed 2022-12-21 18:58:52 +01:00
896ef50b9a fix(ui): Show toasts on verification errors 2022-12-20 16:03:36 +01:00
c4d52b6687 docs(meta): Add Ko-Fi link 2022-12-20 00:05:44 +01:00
5c611a59aa docs(meta): Add funding 2022-12-19 23:04:22 +01:00
7068b989ef Merge pull request 'Implement Stickers' (#184) from feat/stickers into master
Reviewed-on: https://codeberg.org/moxxy/moxxyv2/pulls/184
2022-12-19 14:38:43 +00:00
820fda78e7 fix(ui): Fix sticker overviews having a black background 2022-12-19 15:35:22 +01:00
d758423ec6 fix(ui): Close the pickers when starting an audio message 2022-12-19 15:29:50 +01:00
5472f097a4 fix(meta): Depend on a git revision of moxxmpp 2022-12-19 14:15:13 +01:00
e373f5cffe feat(service): Implement automatic sticker pack downloading 2022-12-19 14:09:30 +01:00
f04729261b feat(ui): Add toasts 2022-12-19 13:33:05 +01:00
b6c8778aec fix(ui): Remove the restriction on clickable sticker messages 2022-12-19 12:48:15 +01:00
8dfe8d55a0 docs(meta): Add sticker pack format docs 2022-12-19 12:46:49 +01:00
36b7d5ce42 feat(ui): Show a spinner while a sticker pack import is running 2022-12-19 12:38:50 +01:00
8d780c3252 fix(ui): Fix alignment of description 2022-12-19 12:36:36 +01:00
a841d5de2d fix(ui): Some translations were off 2022-12-19 12:36:21 +01:00
fdd8d306f7 fix(ui): Close the emoji picker/sticker picker if the keyboard is visible 2022-12-19 12:14:25 +01:00
9510a0fced feat(ui): Make managing sticker packs nicer 2022-12-19 12:02:26 +01:00
c3ec9dfb11 fix(ui): Do not trigger the sticker pack page for sent messages 2022-12-18 20:28:29 +01:00
82c136b684 feat(ui): Update the conversation when stickers get added or removed 2022-12-18 20:19:49 +01:00
ea4bb752b9 fix(service): Guard against importing a sticker pack twice 2022-12-18 18:56:39 +01:00
bac673df99 fix(ui): Give the StickerPicker a background 2022-12-18 18:56:23 +01:00
df2c2f5e4b feat(ui): Honour restricted sticker packs 2022-12-18 18:25:44 +01:00
8c3863f970 feat(ui): Handle sticker XMPP URIs 2022-12-18 18:14:29 +01:00
bc49e31164 fix(service): Add missing attributes to stickers and sticker packs 2022-12-18 17:39:25 +01:00
ce4c54b0d5 fix(service): Freshly downloaded sticker packs are shown as still remote 2022-12-18 17:23:06 +01:00
7b09cdeefd feat(ui): Fix sticker messages not being rebuilt after downloading sticker pack 2022-12-18 17:20:50 +01:00
39dc96ab7a feat(ui): Add a shimmer for loading stickers 2022-12-18 17:10:10 +01:00
2d13ff328e feat(service): Publish a sticker pack after downloading it 2022-12-18 15:15:14 +01:00
53dd598547 feat(meta): Implement installing a remote sticker pack 2022-12-18 15:05:53 +01:00
40b4a540a8 feat(ui): Implement showing a remote sticker pack 2022-12-18 14:20:18 +01:00
33ae53c199 feat(meta): Mark XEP-0449 as complete 2022-12-18 00:57:34 +01:00
97e9b0636b fix(service): Fix OOB fallback messing the body up 2022-12-18 00:56:46 +01:00
b0b21e9d53 feat(ui): Prepare for remote sticker packs 2022-12-17 21:54:12 +01:00
53d5402502 feat(ui): Add a wrapper for local and remote images 2022-12-17 21:43:02 +01:00
a190a9564e fix(service): Silence some warning 2022-12-17 21:36:22 +01:00
7846520788 feat(service): Show 'Sticker' in the notification 2022-12-17 21:34:35 +01:00
3444683983 fix(service): Fix sent stickers not being visible in the preview 2022-12-17 21:29:06 +01:00
00118ddafe fix(meta): Switch to a hashKey 2022-12-17 21:16:38 +01:00
525ba293e3 feat(service): Remove sticker pack files on removal 2022-12-17 19:40:12 +01:00
071f6c08fd feat(ui): Add forgotten i18n string 2022-12-17 19:33:40 +01:00
da70236a45 feat(ui): Translate missing strings 2022-12-17 19:31:39 +01:00
cfdda2d293 feat(meta): Cleanup + simple sticker pack management 2022-12-17 19:24:20 +01:00
aba265d787 feat(service): Do not import sticker packs that are restricted 2022-12-17 17:42:54 +01:00
bbcb37bc4e feat(meta): Update DOAP 2022-12-17 17:34:58 +01:00
eff7d7493d feat(service): Calculate the sticker pack's id on import 2022-12-17 17:34:09 +01:00
730916758e feat(ui): Implement a simple sticker overview page 2022-12-17 16:21:14 +01:00
9acfe2751e feat(ui): Show the sticker in the conversation preview 2022-12-17 14:04:12 +01:00
386569d7cf feat(ui): Correctly handle quoting stickers 2022-12-17 14:00:41 +01:00
39a7e1eb19 feat(ui): Fix linter issues + i18n 2022-12-17 13:53:22 +01:00
f492845235 feat(ui): Somewhat handle not locally available stickers 2022-12-17 13:45:08 +01:00
ab42fc8b57 feat(ui): Add a sticker settings page 2022-12-17 13:36:44 +01:00
a5a9fce330 fix(ui): Fix crash 2022-12-17 13:21:21 +01:00
a70286dda4 feat(service): Allow receiving stickers 2022-12-17 12:40:50 +01:00
2b3e587be4 feat(meta): Display and sending of stickers 2022-12-17 12:22:10 +01:00
ebfac9730b fix(meta): Fix linter issues 2022-12-16 23:09:27 +01:00
fbd3c6ca92 refactor(ui): Refactor the StickerPicker 2022-12-16 23:02:40 +01:00
1cd3dabcea fix(ui): Make the send button's bottom padding adhere to the pickers 2022-12-16 22:54:31 +01:00
eba17880d0 feat(meta): Adapt the sticker picker 2022-12-16 22:49:59 +01:00
c168f910a9 feat(service): Allow importing sticker packs 2022-12-16 22:33:06 +01:00
98dd704fda feat(meta): Begin work in stickers 2022-12-16 20:58:02 +01:00
4ecebe8982 fix(service): Do not reopen conversations on subscription requests
Maybe fixes #165.
2022-12-12 12:56:30 +01:00
8f1d17636e Merge pull request 'Implement reading contact data from the phonebook' (#182) from feat/contact-integration into master
Reviewed-on: https://codeberg.org/moxxy/moxxyv2/pulls/182
2022-12-12 11:54:16 +00:00
fb1c202586 refactor(service): contact.dart -> contacts.dart 2022-12-12 12:50:47 +01:00
d7a4ce022e feat(service): Reset a roster item to pseudoRosterItem on removal 2022-12-12 12:40:09 +01:00
64c3796429 feat(ui): Prevent removing a pseudo contact 2022-12-12 12:37:21 +01:00
80a517beaa fix(ui): Display pseudo roster items in the share selection 2022-12-12 12:35:16 +01:00
cec31550f8 fix(service): Handle inRoster for pseudo roster items 2022-12-12 12:23:12 +01:00
bee760adf5 feat(ui): Handle removing pseudo contacts 2022-12-12 12:18:49 +01:00
155d5747f8 feat(service): Implement pseudo roster items 2022-12-12 12:02:13 +01:00
fd531a360e feat(service): Better handle contact removal 2022-12-12 00:20:22 +01:00
c3884a460d fix(service): Remove debug command 2022-12-11 23:06:10 +01:00
5f5c30673d fix(ui): Make the UI elements react to changes of the contact integration 2022-12-11 22:22:57 +01:00
f423cd5611 feat(service): The correct avatar now appears in the notification 2022-12-11 22:01:00 +01:00
7e059e13ef fix(ui): Prevent the avatar image from flickering 2022-12-11 21:53:38 +01:00
d965fbd57e feat(service): Make the service more togglable 2022-12-10 23:08:24 +01:00
55854ec586 feat(ui): Handle contact info in the profile page 2022-12-10 22:46:42 +01:00
8886c8e695 fix(ui): Fix contact info not being retrieved 2022-12-10 22:13:22 +01:00
d58f5f9a01 feat(service): Make the contact integration configurable 2022-12-10 21:30:47 +01:00
e060b0f549 feat(service): First attempt at handling phone contacts 2022-12-10 19:34:11 +01:00
73913c4ae6 fix(ui): Delete icon was visible on both ends after dismiss 2022-12-10 12:12:03 +01:00
21878ae135 fix(service): Fix defaultMuteState not being honoured 2022-12-10 12:02:09 +01:00
a08a110ef6 feat(ui): Allow verifying our own devices 2022-12-10 11:52:15 +01:00
f723c43603 feat(service): Cache fingerprints for all JIDs 2022-12-09 21:17:39 +01:00
d88876c928 feat(service): Cache our own device fingerprints 2022-12-09 20:51:56 +01:00
f15a3e6bf4 fix(service): Crash when accesing our own devices 2022-12-09 18:43:51 +01:00
4852237bf8 feat(service): Implement verifying OMEMO devices 2022-12-09 18:34:24 +01:00
9a0bc87636 Merge pull request 'Message reactions' (#178) from feat/reactions into master
Reviewed-on: https://codeberg.org/moxxy/moxxyv2/pulls/178
2022-12-09 14:50:55 +00:00
d73d27dccc fix(service): Fix senders being added multiple times to a reaction 2022-12-09 15:47:15 +01:00
6fa5e73226 feat(service): Handle messages with <no-store/> Message Processing Hints 2022-12-09 12:57:26 +01:00
1ff9ea256b fix(ui): Switch from Row to Wrap 2022-12-06 22:40:59 +01:00
7fca7e0246 fix(meta): Add a moxxmpp git override 2022-12-06 18:49:56 +01:00
846270b714 feat(ui): Translate the reaction button 2022-12-06 18:46:40 +01:00
50e7c5683f feat(service): Handle reactions from our own carbons 2022-12-06 15:45:31 +01:00
6883a9570f feat(ui): Swap around the emoji and the number of reactions 2022-12-06 14:10:44 +01:00
8f34bc001d docs(meta): Update DOAP 2022-12-06 14:09:54 +01:00
2f95e5452b fix(service): We don't need chat states for reactions 2022-12-06 13:57:02 +01:00
59a6307a21 feat(service): Send reactions 2022-12-06 13:52:42 +01:00
c8d52e6c41 feat(service): Implement receiving reactions 2022-12-06 13:23:17 +01:00
044766bf8a feat(ui): Build the UI for reactions 2022-12-06 12:19:21 +01:00
1f7c851228 feat(ui): Implement the basic UI for displaying reactions 2022-12-06 00:18:06 +01:00
ca90c658ff feat(ui): Make QR Codes more consistent 2022-12-05 20:19:03 +01:00
19de68e4f0 feat(ui): Make the QR scanner page more versatile 2022-12-04 18:45:38 +01:00
dc17d7d304 feat(ui): Implement scanning a contact's QR code 2022-12-04 18:31:24 +01:00
2372dbf6b3 fix(ui): Reset AddConversationPage's state when popping the route 2022-12-04 13:07:24 +01:00
3382e35447 fix(ui): Prevent clipping of the ink drop of buttons 2022-12-04 12:56:46 +01:00
a9cc4f55b8 fix(ui): Improve the look of the overview menu 2022-12-04 12:45:49 +01:00
63fbf7ebe4 fix(service): Auto-downloads are not marked as downloading (Fixes #167) 2022-12-04 12:04:44 +01:00
d1d6b67fd6 feat(ui): Replace 'custom' icon button with a FAB 2022-12-03 20:38:05 +01:00
87160e8648 fix(ui): Re-add JIDs to the newconversation page (Fixes #176) 2022-12-03 18:35:32 +01:00
e5553699c5 feat(ui): Show an indicator for the swipe gesture 2022-12-03 18:26:06 +01:00
adcfdc1a73 feat(ui,service): Implement sending audio messages 2022-12-03 17:55:23 +01:00
70464a2b71 feat(ui): Make cancelling a recording also possible via dragging 2022-12-03 13:24:38 +01:00
0852a75d9f fix(ui): The timer was opaque during hit testing when not recording 2022-12-03 13:11:48 +01:00
9affa0e89a feat(ui): Show a toast when the record button is tapped 2022-12-03 13:01:54 +01:00
cc13078ec5 feat(ui): Improve the touch behaviour of the overview menu 2022-12-03 12:48:34 +01:00
35285343b1 feat(ui): Add a timer while recording audio 2022-12-03 12:40:45 +01:00
cb6bce0c56 feat(ui): Implement the UI for audio recording 2022-12-02 23:05:06 +01:00
5c1eda72c3 fix(ui): Reimplement file sending 2022-12-02 20:30:03 +01:00
4542accc33 feat(ui): Remove the speeddial 2022-12-02 20:27:08 +01:00
8f5470076b feat(ui): Implement UI for audio messages during up/download 2022-12-02 16:55:02 +01:00
3427c3c761 feat(ui): Move the circle to the bottom bar 2022-11-30 20:50:51 +01:00
0cc8d0947b feat(ui): Don't generate video thumbnails for problematic formats 2022-11-30 19:19:54 +01:00
c3795450a9 feat(ui): Begin working on the recording UI 2022-11-30 19:08:42 +01:00
868d924836 fix(ui): Translate quoted message strings 2022-11-30 12:50:47 +01:00
d8f634d67c feat(ui): Hide the microphone and sticker buttons when quoting 2022-11-30 12:47:09 +01:00
09b97ab4c5 feat(ui): Make SharedFileWidget adhere to the other widget's style 2022-11-30 12:45:16 +01:00
8b4d7dd569 feat(ui): Make audio messages quotable 2022-11-30 12:38:16 +01:00
a63205d5e1 feat(ui): Implement shared media view for audio 2022-11-29 23:44:43 +01:00
e7a4e93366 fix(ui): Remove unused value 2022-11-29 23:38:45 +01:00
2c23f40415 feat(ui): Implement an audio widget 2022-11-29 23:36:09 +01:00
daf4ee79f2 fix(ui): Fix indentation 2022-11-27 13:01:00 +01:00
38a73d2890 feat(ui): Replace video spinners with a shimmer
Fixes #172.
2022-11-27 13:00:01 +01:00
b004d8364c docs(ui): Rename string 2022-11-26 22:42:13 +01:00
2498e23bd5 feat(ui): Generate video thumbnails 2022-11-26 22:40:42 +01:00
3a80d50cf5 docs(ui): Remove fixed TODO 2022-11-26 21:19:29 +01:00
8c12eb47ce feat(ui): Minor touch-ups for the 'about' page 2022-11-26 19:24:12 +01:00
cfec6afc7d feat(ui): Allow tapping the profile picture in a conversation 2022-11-26 19:04:13 +01:00
a3bdabca3c feat(ui): Allow tapping on the profile picture 2022-11-26 19:00:24 +01:00
f094a326ac feat(ui): Switch cropping libraries
This makes the avatar cropper much more consistent
with the background cropper. Fixes #168.
2022-11-26 18:40:46 +01:00
d24cab9c1a fix(i18n): Add missing translations 2022-11-26 18:09:42 +01:00
c7d1ecce35 feat(ui): Show whether the last received message was edited 2022-11-26 15:57:06 +01:00
306fd99b84 feat(service): Handle received corrections (Fixes #81) 2022-11-26 15:50:24 +01:00
ab63bc44a6 feat(service,ui): Allow correcting the last message 2022-11-26 15:17:33 +01:00
ef15f15458 feat(ui): Build the backbone for LMC support 2022-11-26 14:29:46 +01:00
db86136aa8 fix(service): Enable carbons (Fixes #171) 2022-11-26 13:56:24 +01:00
c344aed471 feat(ui): Encode backgrounds as JPEG (Fixes #94) 2022-11-26 13:47:09 +01:00
79b0a4ba7a feat(meta): Remove image_size_getter 2022-11-26 13:40:57 +01:00
4ce5e29a81 fix(ui): Use the CancelButton for the avatar cropper 2022-11-26 13:24:35 +01:00
524dec0991 fix(ui): Hide the media preview if the contact is typing 2022-11-26 12:12:36 +01:00
05074ed4f0 feat(ui): Replace the custom code with InteractiveViewer 2022-11-26 00:50:29 +01:00
4e4ed58605 feat(ui): Use the InteractiveViewer 2022-11-26 00:40:43 +01:00
6a109fe03d fix(service): Set a foreign key constraint on the conversations database 2022-11-25 21:36:41 +01:00
a783aab229 feat(ui): Show a media preview if it is thumbnailable 2022-11-25 20:35:43 +01:00
43dc2285b3 fix(service): Update conversation when a file is downloaded 2022-11-25 20:35:24 +01:00
eac8e3fb44 feat(ui): Show a media preview in the body line 2022-11-25 20:20:44 +01:00
035d29fabc fix(ui): Use the emoji + mime type combo for media messages 2022-11-25 20:01:54 +01:00
ad2b10972c docs(ui): Remove TODO 2022-11-25 18:11:05 +01:00
e65e1f3ec4 fix(service): Fix conversations not updating on delivery receipt 2022-11-25 18:08:28 +01:00
10b86812cd fix(service): Fix conversations not being properly loaded
Meanwhile, also remove the Completer system for preventing race
conditions with, hopefully, something better.
2022-11-25 17:58:09 +01:00
fe4c794f68 refactor(service): Conversations now point to the last message 2022-11-25 17:01:30 +01:00
6115d748e3 feat(ui): Show the last message state in the conversations list 2022-11-25 16:09:06 +01:00
b0f266bb0a fix(ui): Make the border shape const 2022-11-24 22:56:14 +01:00
646c99feb5 feat(ui): Allow scaling in the background cropper 2022-11-24 22:54:39 +01:00
e8461d7059 fix(ui): Round the dialog corners 2022-11-24 18:07:08 +01:00
b2efd9f22f fix(i18n): Fix typo 2022-11-24 18:04:17 +01:00
8e426b7fd6 feat(service): Tapping a message notification opens correct chat
Fixes #91.
2022-11-24 18:00:54 +01:00
c13a65b204 feat(service): Allow marking messages as read from the notification 2022-11-24 15:23:59 +01:00
503a24e003 feat(service): Send read marker when marking a chat as read 2022-11-24 15:02:52 +01:00
dfddd3d3d0 fix(shared): RetractMessageComment -> RetractMessageCommentCommand 2022-11-24 13:34:52 +01:00
26e01bb7f8 feat(ui): Implement marking a chat as read from the long-press menu 2022-11-24 13:32:32 +01:00
5f88626ddf feat(ui): Allow closing a chat from the long-press menu 2022-11-24 13:09:48 +01:00
e04bb29bb2 feat(ui): Allow copying a text message 2022-11-24 13:01:02 +01:00
c9690e028b fix(service): Fix null exception when closing a chat 2022-11-24 11:57:29 +01:00
8709f0bd8e refactor(ui): Move the overview code out of the chat bubble 2022-11-24 11:55:51 +01:00
13d7f33c37 refactor(ui): Move ChatBubble to OverviewMenuItem 2022-11-24 11:19:19 +01:00
426 changed files with 38706 additions and 14412 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
ko_fi: papatutuwawa

3
.gitignore vendored
View File

@@ -60,3 +60,6 @@ lib/i18n/*.dart
# Android artifacts
.android
# Build scripts
release-*/

View File

@@ -7,7 +7,7 @@ line-length=72
[title-trailing-punctuation]
[title-hard-tab]
[title-match-regex]
regex=^(feat|fix|chore|refactor|docs|release|test)\((xmpp|service|ui|shared|meta|tests|i18n)+(,(xmpp|service|ui|shared|meta|tests|i18n))*\): .*$
regex=^((feat|fix|chore|refactor)\((service|ui|shared|all|tests|i18n|docs|flake|android|ios|linux|windows|macos|ci)+(,(service|ui|shared|all|tests|i18n|docs|flake|android|ios|linux|windows|macos|ci))*\)|release): [A-Z0-9].*$
[body-trailing-whitespace]

36
.woodpecker.yml Normal file
View File

@@ -0,0 +1,36 @@
when:
branch: master
pipeline:
check-metadata:
image: bash:alpine3.18
commands:
- bash ./scripts/check-fastlane-metadata.sh
when:
# Only run this check when the Fastlane metadata changes
path:
includes: ['fastlane/metadata/**']
analysis:
image: git.polynom.me/papatutuwawa/docker-flutter:3.13.6
commands:
- PUB_HOSTED_URL=http://172.17.0.1:8000 dart pub get
- dart run build_runner build
- dart run pigeon --input pigeon/quirks.dart
- flutter analyze --no-pub
- flutter test --no-pub
when:
path:
includes: ['test/**', 'src/**']
notify:
image: git.polynom.me/papatutuwawa/woodpecker-xmpp
settings:
xmpp_tls: 1
xmpp_is_muc: 1
xmpp_recipient: moxxy-build@muc.moxxy.org
xmpp_alias: 2Bot
secrets: [ xmpp_jid, xmpp_password, xmpp_server ]
when:
status:
- failure

108
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,108 @@
# Contribution Guide
Thanks for your interest in the Moxxy XMPP client! This document contains guidelines and guides for working
on the Moxxy codebase.
## Non-Code Contributions
### Translations
You can contribute to Moxxy by translating parts of Moxxy into a language you can speak. To do that, head over to [Codeberg's Weblate instance](https://translate.codeberg.org/projects/moxxy/moxxy/), where you can start translating.
## Prerequisites
Before building or working on Moxxy, please make sure that your development environment is correctly set up.
Moxxy requires Flutter 3.7.3, since we use a fork of the Flutter library, and the JDK 17. Building Moxxy
is currently only supported for Android.
### Android Studio
If you use Android Studio, make sure that you use version "Flamingo Canary 3", as that one comes bundled with
JDK 17, instead of JDK 11 ([See here](https://codeberg.org/moxxy/moxxy/issues/252)). If that is
not an option, you can manually add a JDK 17 installation in Android Studio and tell the Flutter addon
to use that installation instead.
### NixOS
If you use NixOS or Nix, you can use the dev shell provided by the Flake in the repository's root. It contains
the correct JDK and Flutter version. However, make sure that other environment variables, like
`ANDROID_HOME` and `ANDROID_AVD_HOME`, are correctly set.
## Building
Currently, Moxxy contains a git submodule. While it is not utilised at the moment, it contains
the list of suggested XMPP providers to use for auto-registration. To properly clone the
repository, use `git clone --recursive https://codeberg.org/moxxy/moxxy.git`
In order to build Moxxy, you first have to run the code generator. To do that, first install all dependencies with
`flutter pub get`. Next, run the code generator using `flutter pub run build_runner build`. This builds required
data classes and the i18n support. Next, run `dart run pigeon --input pigeon/quirks.dart` to generate the communication
channels with the native code.
Finally, you can build Moxxy using `flutter run`, if you want to test a change, or `flutter build apk --release` to build
an optimized release build. The release builds found in the repository's releases are build using `flutter build apk --release --split-per-abi`.
## Contributing
If you want to fix a small issue, you can just fork, create a new branch, and start working right away. However, if you want to work
on a bigger feature, please first create an issue (if an issue does not already exist) or join the [development chat](xmpp:moxxy@muc.moxxy.org?join) (xmpp:moxxy@muc.moxxy.org?join)
to discuss the feature first.
Before creating a pull request, please make sure you checked every item on the following checklist:
- [ ] I formatted the code with the dart formatter (`dart format`) before running the linter
- [ ] I ran the linter (`flutter analyze`) and introduced no new linter warnings
- [ ] I ran the tests (`flutter test`) and introduced no new failing tests
- [ ] I used [gitlint](https://github.com/jorisroovers/gitlint) to ensure propper formatting of my commig messages
If you think that your code is ready for a pull request, but you are not sure if it is ready, prefix the PR's title with "WIP: ", so that discussion
can happen there. If you think your PR is ready for review, remove the "WIP: " prefix.
### Android
In case you modified the Android-native code, please also make sure that you checked every item on the following checklist:
- [ ] I checked that [ktlint](https://github.com/pinterest/ktlint) is not showing any linting issues (`ktlint android/app/src/main/kotlin/org/moxxy/moxxyv2/ '!android/app/src/main/kotlin/org/moxxy/moxxyv2/quirks'`)
### Tips
#### `data_classes.yaml`
When you add, remove, or modify data classes in `data_classes.yaml`, you need to rebuild the classes using `flutter pub run build_runner build`. However, there appears
to be a bug in my own build runner script, which prevents the data classes from being
rebuilt if they are changed. To fix this, remove the generated data classes by running
`rm lib/shared/*.moxxy.dart`, after which build_runner will rebuild the data classes.
### Code Guidelines
#### Translations
If your code adds new strings that should be translated, only add them to the base
language, which is English. Even if you know more than English, do not add the keys
to other language files. To prevent merge conflicts between Weblate and the repository,
all other languages are managed via [Codeberg's Weblate instance](https://translate.codeberg.org/projects/moxxy/moxxy/).
#### Commit messages
Commit messages should be uniformly formatted. `gitlint` is a linter for commit messages that enforces those guidelines. They are defined in the `.gitlint` file
at the root of the repository. `gitlint` can be installed as a pre-commit hook using
`gitlint install-hook`. That way, `gitlint` runs on every commit and warns you if the
commit message violates any of the defined rules.
Commit messages always follow the following format:
```
<type>(<areas>): <summary>
<full message>
```
`<type>` is the type of action that was performed in the commit and is one of the following: `feat` (Addition of a feature), `fix` (Fix a bug or other issue), `chore` (Bump dependency versions, fix formatter issues), `refactor` (A bigger "moving around" or rewriting of code), `docs` (Commits that just touch the documentation, be it code or, for example, the README).
`<areas>` are the areas inside the code that are touched by the change. They are a comma-separated list of one or more of the following: `service` (Everything inside `lib/service`), `ui` (Everything inside `lib/ui`), `shared` (Everything inside `lib/shared`), `all` (A bit of everything is involved), `tests` (Everyting inside `test` or `integration_test`), `i18n` (The translation files have been modified), `docs` (Documentation of any kind), `flake` (The NixOS flake has been modified).
`<summary>` is the summary of the entire commit in a few words. Make that that the entire
first line is not longer than 72 characters. `<summary>` also must start with an uppercase
letter or a number.
The `<full message>` is optional. In case your commit requires more explanation, write it
there. Make sure that there is an empty line between the full message and the summary line.
The exception to these rules is a commit message of the format `release: Release version x.y.z`, as it touches everything and is thus implicitly using `(all)` as an area code.

View File

@@ -2,38 +2,29 @@
An experimental XMPP client that tries to be as easy, modern and beautiful as possible.
The code is also available on [codeberg](https://codeberg.org/moxxy/moxxyv2).
The code is also available on [Codeberg](https://codeberg.org/moxxy/moxxy).
[<img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" alt="Get it on IzzyOnDroid" height="80" />](https://apt.izzysoft.de/fdroid/index/apk/org.moxxy.moxxyv2)
[<img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" alt="Get it on IzzyOnDroid" height="80">](https://apt.izzysoft.de/fdroid/index/apk/org.moxxy.moxxyv2)
Or [get the latest APK from Codeberg](https://codeberg.org/moxxy/moxxy/releases/latest).
## Screenshots
[<img src="https://codeberg.org/moxxy/moxxyv2/raw/branch/master/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png" width="20%"></img>](./fastlane/metadata/android/en-US/images/phoneScreenshots/1.png)
[<img src="https://codeberg.org/moxxy/moxxyv2/raw/branch/master/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png" width="20%"></img>](./fastlane/metadata/android/en-US/images/phoneScreenshots/2.png)
## Developing and Building
## Building and Contributing
Clone using `git clone --recursive https://github.com/Polynomdivision/moxxyv2.git`.
In order to build Moxxy, you need to have [Flutter](https://docs.flutter.dev/get-started/install) set
up. If you are running NixOS or using Nix, you can also use the Flake at the root of the repository
by running `nix develop` to get a development shell including everything that is needed. Note
that if you decide to use the Flake, `ANDROID_HOME` and `ANDROID_AVD_HOME` must be set to the respective directories.
Before building Moxxy, you need to generate all needed data classes. To do this, run
`flutter pub get` to install all dependencies. Then run `flutter pub run build_runner build` to generate
state classes, data classes and the database schemata. After that is done, you can either
build the app with `flutter build apk --debug` to create a debug build,
`flutter build apk --release` to create a relase build or just run the app in development
mode with `flutter run`.
After implementing a change or a feature, please ensure that nothing is broken by the change
by running `flutter test` afterwards. Also make sure that the code passes the linter by
running `flutter analyze`. This project also uses [gitlint](https://github.com/jorisroovers/gitlint)
to ensure uniform formatting of commit messages.
For build and contribution guidelines, please refer to [`CONTRIBUTING.md`](./CONTRIBUTING.md)
Also, feel free to join the development chat at `moxxy@muc.moxxy.org`.
### Translating
If you want to contribute by translating Moxxy, you can do that on [Codeberg's Weblate instance](https://translate.codeberg.org/projects/moxxy/moxxy/).
[![Translation status](https://translate.codeberg.org/widgets/moxxy/-/moxxy/multi-auto.svg)](https://translate.codeberg.org/engage/moxxy/)
## A Bit of History
This project is the successor of moxxyv1, which was written in *React Native* and abandoned
@@ -46,3 +37,10 @@ See `./LICENSE`.
## Special Thanks
- New logo designed by [Synoh](https://twitter.com/synoh_manda)
- New app UI designed by [Ailyaut](https://ailyaut.robotfumeur.fr/index.html)
## Support
If you like what I do and you want to support me, feel free to donate to me on Ko-Fi.
[<img src="https://codeberg.org/moxxy/moxxyv2/raw/branch/master/assets/repo/kofi.png" height="36" style="height: 36px; border: 0px;"></img>](https://ko-fi.com/papatutuwawa)

View File

@@ -6,13 +6,12 @@ linter:
use_setters_to_change_properties: false
avoid_positional_boolean_parameters: false
avoid_bool_literals_in_conditional_expressions: false
file_names: false
analyzer:
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
- "**/*.moxxy.dart"
- "test/"
- "integration_test/"
- "lib/service/database/migrations/*.dart"
- "lib/i18n/*.dart"
- "pigeon/quirks.dart"

View File

@@ -1,3 +1,9 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
@@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) {
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
@@ -21,17 +22,12 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
//compileSdkVersion flutter.compileSdkVersion
compileSdkVersion 33
compileSdkVersion 34
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
@@ -45,22 +41,22 @@ android {
defaultConfig {
applicationId "org.moxxy.moxxyv2"
// TODO: Remove once https://github.com/fluttercommunity/flutter_launcher_icons/pull/313 is merged
minSdkVersion 23
targetSdkVersion 31
//minSdkVersion flutter.minSdkVersion
//targetSdkVersion flutter.targetSdkVersion
minSdkVersion 26
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
// Externally signed using a security key
signingConfig null
}
}
dependencies {
compileOnly rootProject.findProject(":moxxy_native")
}
}
flutter {
@@ -68,5 +64,5 @@ flutter {
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "androidx.activity:activity-ktx:1.7.2"
}

View File

@@ -1,33 +1,36 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.moxxy.moxxyv2">
<application
android:label="Moxxy"
<application
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/ic_launcher"
android:label="Moxxy">
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTask"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:enableOnBackInvokedCallback="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<meta-data
android:name="flutterEmbedding"
android:value="2" />
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Allow receiving share intents for all kinds of things -->
<!-- Allow receiving share intents for all kinds of things -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
@@ -38,20 +41,34 @@
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
<!-- Enable usage of direct share -->
<meta-data
android:name="android.service.chooser.chooser_target_service"
android:value="androidx.sharetarget.ChooserTargetServiceCompat" />
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/share_targets" />
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
</queries>
</application>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
</queries>
</manifest>

View File

@@ -1,6 +1,57 @@
package org.moxxy.moxxyv2
import android.content.Intent
import android.os.Bundle
import android.util.Log
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import org.moxxy.moxxyv2.quirks.MoxxyQuirkApi
import org.moxxy.moxxyv2.quirks.QuirkNotificationEvent
import org.moxxy.moxxyv2.quirks.QuirkNotificationEventType
class MainActivity: FlutterActivity() {
class MainActivity : FlutterActivity(), MoxxyQuirkApi {
private var lastEvent: QuirkNotificationEvent? = null
private fun handleIntent(intent: Intent?): Boolean {
if (intent == null) return false
when (intent.action) {
org.moxxy.moxxy_native.TAP_ACTION -> {
Log.d("Moxxy", "Handling tap data")
lastEvent = QuirkNotificationEvent(
intent.getLongExtra(org.moxxy.moxxy_native.NOTIFICATION_EXTRA_ID_KEY, -1),
intent.getStringExtra(org.moxxy.moxxy_native.NOTIFICATION_EXTRA_JID_KEY)!!,
QuirkNotificationEventType.OPEN,
null,
org.moxxy.moxxy_native.notifications.extractPayloadMapFromIntent(intent),
)
return true
}
else -> {
Log.d("Moxxy", "Unknown intent action: ${intent.action}")
return false
}
}
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
MoxxyQuirkApi.setUp(flutterEngine.dartExecutor.binaryMessenger, this)
super.configureFlutterEngine(flutterEngine)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleIntent(intent)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleIntent(intent)
}
override fun earlyNotificationEventQuirk(): QuirkNotificationEvent? {
val event = lastEvent
lastEvent = null
return event
}
}

View File

@@ -0,0 +1,158 @@
// Autogenerated from Pigeon (v22.4.1), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
package org.moxxy.moxxyv2.quirks
import android.util.Log
import io.flutter.plugin.common.BasicMessageChannel
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MessageCodec
import io.flutter.plugin.common.StandardMessageCodec
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
private fun wrapResult(result: Any?): List<Any?> {
return listOf(result)
}
private fun wrapError(exception: Throwable): List<Any?> {
return if (exception is FlutterError) {
listOf(
exception.code,
exception.message,
exception.details
)
} else {
listOf(
exception.javaClass.simpleName,
exception.toString(),
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
)
}
}
/**
* Error class for passing custom error details to Flutter via a thrown PlatformException.
* @property code The error code.
* @property message The error message.
* @property details The error details. Must be a datatype supported by the api codec.
*/
class FlutterError (
val code: String,
override val message: String? = null,
val details: Any? = null
) : Throwable()
enum class QuirkNotificationEventType(val raw: Int) {
MARK_AS_READ(0),
REPLY(1),
OPEN(2);
companion object {
fun ofRaw(raw: Int): QuirkNotificationEventType? {
return values().firstOrNull { it.raw == raw }
}
}
}
/** Generated class from Pigeon that represents data sent in messages. */
data class QuirkNotificationEvent (
/** The notification id. */
val id: Long,
/** The JID the notification was for. */
val jid: String,
/** The type of event. */
val type: QuirkNotificationEventType,
/**
* An optional payload.
* - type == NotificationType.reply: The reply message text.
* Otherwise: undefined.
*/
val payload: String? = null,
/** Extra data. Only set when type == NotificationType.reply. */
val extra: Map<String?, String?>? = null
)
{
companion object {
fun fromList(pigeonVar_list: List<Any?>): QuirkNotificationEvent {
val id = pigeonVar_list[0] as Long
val jid = pigeonVar_list[1] as String
val type = pigeonVar_list[2] as QuirkNotificationEventType
val payload = pigeonVar_list[3] as String?
val extra = pigeonVar_list[4] as Map<String?, String?>?
return QuirkNotificationEvent(id, jid, type, payload, extra)
}
}
fun toList(): List<Any?> {
return listOf(
id,
jid,
type,
payload,
extra,
)
}
}
private open class NotificationsQuirksPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return when (type) {
129.toByte() -> {
return (readValue(buffer) as Long?)?.let {
QuirkNotificationEventType.ofRaw(it.toInt())
}
}
130.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
QuirkNotificationEvent.fromList(it)
}
}
else -> super.readValueOfType(type, buffer)
}
}
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
when (value) {
is QuirkNotificationEventType -> {
stream.write(129)
writeValue(stream, value.raw)
}
is QuirkNotificationEvent -> {
stream.write(130)
writeValue(stream, value.toList())
}
else -> super.writeValue(stream, value)
}
}
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface MoxxyQuirkApi {
fun earlyNotificationEventQuirk(): QuirkNotificationEvent?
companion object {
/** The codec used by MoxxyQuirkApi. */
val codec: MessageCodec<Any?> by lazy {
NotificationsQuirksPigeonCodec()
}
/** Sets up an instance of `MoxxyQuirkApi` to handle messages through the `binaryMessenger`. */
@JvmOverloads
fun setUp(binaryMessenger: BinaryMessenger, api: MoxxyQuirkApi?, messageChannelSuffix: String = "") {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxyv2.MoxxyQuirkApi.earlyNotificationEventQuirk$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
listOf(api.earlyNotificationEventQuirk())
} catch (exception: Throwable) {
wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<share-target android:targetClass="org.moxxy.moxxyv2.MainActivity">
<data android:mimeType="*/*" />
<category android:name="org.moxxy.moxxyv2.dynamic_share_target" />
</share-target>
</shortcuts>

View File

@@ -1,16 +1,3 @@
buildscript {
ext.kotlin_version = '1.6.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
@@ -26,6 +13,6 @@ subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

@@ -1,11 +1,25 @@
include ':app'
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
id "org.jetbrains.kotlin.android" version "1.8.21" apply false
}
include ":app"

View File

@@ -1,252 +0,0 @@
{
"@@name": "English",
"global": {
"title": "Moxxy",
"moxxySubtitle": "An experiment into building a modern, easy and beautiful XMPP client.",
"dialogAccept": "Okay",
"dialogCancel": "Cancel",
"yes": "Yes",
"no": "No"
},
"notifications": {
"permanent": {
"idle": "Idle",
"ready": "Ready to receive messages",
"connecting": "Connecting...",
"disconnect": "Disconnected",
"error": "Error"
},
"message": {
"reply": "Reply",
"markAsRead": "Mark as read"
},
"channels": {
"messagesChannelName": "Messages",
"messagesChannelDescription": "The notification channel for received messages",
"warningChannelName": "Warnings",
"warningChannelDescription": "Warnings related to Moxxy"
}
},
"messages": {
"image": "Image",
"video": "Video",
"audio": "Audio",
"file": "File",
"retracted": "The message has been retracted",
"retractedFallback": "A previous message has been retracted but your client does not support it"
},
"errors": {
"omemo": {
"couldNotPublish": "Could not publish the cryptographic identity to the server. This means that end-to-end encryption may not work.",
"notEncryptedForDevice": "This message was not encrypted for this device",
"invalidHmac": "Could not decrypt message",
"noDecryptionKey": "No decryption key available",
"messageInvalidAfixElement": "Invalid encrypted message"
},
"connection": {
"connectionTimeout": "Could not connect to server"
},
"login": {
"saslFailed": "Invalid login credentials",
"startTlsFailed": "Failed to establish a secure connection",
"noConnection": "Failed to establish a connection",
"unspecified": "Unspecified error"
},
"message": {
"unspecified": "Unknown error",
"fileUploadFailed": "The file upload failed",
"contactDoesntSupportOmemo": "The contact does not support encryption using OMEMO:2",
"fileDownloadFailed": "The file download failed",
"serviceUnavailable": "The message could not be delivered to the contact",
"remoteServerTimeout": "The message could not be delivered to the contact's server",
"remoteServerNotFound": "The message could not be delivered to the contact's server as it cannot be found",
"failedToEncrypt": "The message could not be encrypted",
"failedToEncryptFile": "The file could not be encrypted",
"failedToDecryptFile": "The file could not be decrypted",
"fileNotEncrypted": "The chat is encrypted but the file is not encrypted"
}
},
"warnings": {
"message": {
"integrityCheckFailed": "Could not verify file integrity"
}
},
"pages": {
"intro": {
"noAccount": "Have no XMPP account? No worries, creating one is really easy.",
"loginButton": "Login",
"registerButton": "Register"
},
"login": {
"title": "Login",
"xmppAddress": "XMPP-Address",
"password": "Password",
"advancedOptions": "Advanced options",
"createAccount": "Create account on server"
},
"conversations": {
"speeddialNewChat": "New chat",
"speeddialJoinGroupchat": "Join groupchat",
"overlaySettings": "Settings",
"noOpenChats": "You have no open chats",
"startChat": "Start a chat"
},
"conversation": {
"unencrypted": "Unencrypted",
"encrypted": "Encrypted",
"closeChat": "Close chat",
"closeChatConfirmTitle": "Close chat",
"closeChatConfirmSubtext": "Are you sure you want to close this chat?",
"blockUser": "Block user",
"online": "Online",
"retract": "Retract message",
"retractBody": "Are you sure you want to retract the message? Keep in mind that this is only a request that the client does not have to honour.",
"forward": "Forward",
"edit": "Edit",
"quote": "Quote"
},
"addcontact": {
"title": "Add new contact",
"xmppAddress": "XMPP-Address",
"subtitle": "You can add a contact either by typing in their XMPP address or by scanning their QR code",
"buttonAddToContact": "Add to contacts"
},
"newconversation": {
"title": "Start new chat",
"addContact": "Add contact",
"createGroupchat": "Create groupchat"
},
"crop": {
"setProfilePicture": "Set as profile picture"
},
"shareselection": {
"shareWith": "Share with...",
"confirmTitle": "Send file",
"confirmBody": "One or more chats are unencrypted. This means that the file will be leaked to the server. Do you still want to continue?"
},
"profile": {
"self": {
"devices": "Devices"
},
"conversation": {
"muteChatTooltip": "Mute chat",
"unmuteChatTooltip": "Unmute chat",
"muteChat": "Mute",
"unmuteChat": "Unmute",
"devices": "Devices"
},
"owndevices": {
"title": "Own Devices",
"thisDevice": "This device",
"otherDevices": "Other devices",
"deleteDeviceConfirmTitle": "Delete device",
"deleteDeviceConfirmBody": "This means that contacts will not be able to encrypt for that device. Continue?",
"recreateOwnSessions": "Rebuild sessions",
"recreateOwnSessionsConfirmTitle": "Recreate own sessions?",
"recreateOwnSessionsConfirmBody": "This will recreate the cryptographic sessions with your own devices. Use only if your own devices throw decryption errors.",
"recreateOwnDevice": "Recreate device",
"recreateOwnDeviceConfirmTitle": "Recreate own device?",
"recreateOwnDeviceConfirmBody": "This will recreate this device's cryptographic identity. It will take some time. If contacts verified your device, they will have to do it again. Continue?"
},
"devices": {
"title": "Devices",
"recreateSessions": "Rebuild sessions",
"recreateSessionsConfirmTitle": "Rebuild sessions?",
"recreateSessionsConfirmBody": "This will recreate the cryptographic sessions with your own devices. Use only if your own devices throw decryption errors."
}
},
"blocklist": {
"title": "Blocklist",
"noUsersBlocked": "You have no users blocked",
"unblockAll": "Unblock all",
"unblockAllConfirmTitle": "Are you sure?",
"unblockAllConfirmBody": "Are you sure you want to unblock all users?",
"unblockJidConfirmTitle": "Unblock ${jid}?",
"unblockJidConfirmBody": "Are you sure you want to unblock ${jid}? You will receive messages from this user again."
},
"settings": {
"settings": {
"title": "Settings",
"conversationsSection": "Conversations",
"accountSection": "Account",
"signOut": "Sign out",
"signOutConfirmTitle": "Sign Out",
"signOutConfirmBody": "You are about to sign out. Proceed?",
"miscellaneousSection": "Miscellaneous",
"debuggingSection": "Debugging"
},
"about": {
"title": "About",
"licensed": "Licensed under GPL3",
"viewSourceCode": "View source code"
},
"appearance": {
"title": "Appearance",
"languageSection": "Language",
"language": "App language",
"languageSubtext": "Currently selected: $selectedLanguage",
"systemLanguage": "Default language"
},
"licenses": {
"title": "Open-Source Licenses",
"licensedUnder": "Licensed under $license"
},
"conversation": {
"title": "Chat",
"appearance": "Appearance",
"selectBackgroundImage": "Select background image",
"selectBackgroundImageDescription": "This image will be the background of all your chats",
"removeBackgroundImage": "Remove background image",
"removeBackgroundImageConfirmTitle": "Remove background image",
"removeBackgroundImageConfirmBody": "Are you sure you want to remove your conversation background image?",
"newChatsSection": "New Conversations",
"newChatsMuteByDefault": "Mute new chats by default",
"newChatsE2EE": "Enable end-to-end encryption by default. WARNING: Experimental"
},
"debugging": {
"title": "Debugging options",
"generalSection": "General",
"generalEnableDebugging": "Enable debugging",
"generalEncryptionPassword": "Encryption password",
"generalEncryptionPasswordSubtext": "The logs may contain sensitive information so pick a strong passphrase",
"generalLoggingIp": "Logging IP",
"generalLoggingIpSubtext": "The IP the logs should be sent to",
"generalLoggingPort": "Logging Port",
"generalLoggingPortSubtext": "The IP the logs should be sent to"
},
"network": {
"title": "Network",
"automaticDownloadsSection": "Automatic Downloads",
"automaticDownloadsText": "Moxxy will automatically download files on...",
"automaticDownloadsMaximumSize": "Maximum Download Size",
"automaticDownloadsMaximumSizeSubtext": "The maximum file size for a file to be automatically downloaded",
"wifi": "Wifi",
"mobileData": "Mobile data"
},
"privacy": {
"title": "Pricacy",
"generalSection": "General",
"showContactRequests": "Show contact requests",
"showContactRequestsSubtext": "This will show people who added you to their contact list but sent no message yet",
"profilePictureVisibility": "Make profile picture public",
"profilePictureVisibilitSubtext": "If enabled, everyone can see your profile picture. If disabled, only users on your contact list can see your profile picture.",
"autoAcceptSubscriptionRequests": "Auto-accept subscription requests",
"autoAcceptSubscriptionRequestsSubtext": "If enabled, subscription requests will be automatically accepted if the user is in the contact list.",
"conversationsSection": "Conversation",
"sendChatMarkers": "Send chat markers",
"sendChatMarkersSubtext": "This will tell your conversation partner if you received or read a message",
"sendChatStates": "Send chat states",
"sendChatStatesSubtext": "This will show your conversation partner if you are typing or looking at the chat",
"redirectsSection": "Redirects",
"redirectText": "This will redirect $serviceName links that you tap to a proxy service, e.g. $exampleProxy",
"currentlySelected": "Currently selected: $proxy",
"redirectsTitle": "$serviceName Redirect",
"cannotEnableRedirect": "Cannot enable $serviceName redirects",
"cannotEnableRedirectSubtext": "You must first set a proxy service to redirect to. To do so, tap the field next to the switch.",
"urlEmpty": "URL cannot be empty",
"urlInvalid": "Invalid URL",
"redirectDialogTitle": "$serviceName Redirect"
}
}
}
}

View File

@@ -1,252 +1,409 @@
{
"@@name": "Deutsch",
"language": "Deutsch",
"global": {
"title": "Moxxy",
"moxxySubtitle": "Ein Experiment im Entwickeln eines modernen, einfachen und schönen XMPP-Clients.",
"dialogAccept": "Okay",
"dialogCancel": "Abbrechen",
"yes": "Ja",
"no": "Nein"
"title": "Moxxy",
"moxxySubtitle": "Ein Experiment im Entwickeln eines modernen, einfachen und schönen XMPP-Clients.",
"dialogAccept": "Okay",
"dialogCancel": "Abbrechen",
"yes": "Ja",
"no": "Nein"
},
"notifications": {
"permanent": {
"idle": "Bereit",
"ready": "Bereit zum Nachrichtenempfang",
"connecting": "Verbinde...",
"disconnect": "Keine Verbindung",
"error": "Fehler"
},
"message": {
"reply": "Antworten",
"markAsRead": "Als gelesen markieren"
},
"channels": {
"messagesChannelName": "Nachrichten",
"messagesChannelDescription": "Empfangene Nachrichten",
"warningChannelName": "Warnungen",
"warningChannelDescription": "Warnungen im Bezug auf Moxxy"
}
"permanent": {
"idle": "Bereit",
"ready": "Bereit zum Nachrichtenempfang",
"connecting": "Verbinde...",
"disconnect": "Keine Verbindung",
"error": "Fehler"
},
"message": {
"reply": "Antworten",
"markAsRead": "Als gelesen markieren"
},
"channels": {
"messagesChannelName": "Nachrichten",
"messagesChannelDescription": "Empfangene Nachrichten",
"warningChannelName": "Warnungen",
"warningChannelDescription": "Warnungen im Bezug auf Moxxy"
},
"titles": {
"error": "Fehler"
}
},
"dateTime": {
"justNow": "Gerade",
"nMinutesAgo": "vor ${min}min",
"mondayAbbrev": "Mon",
"tuesdayAbbrev": "Die",
"wednessdayAbbrev": "Mit",
"thursdayAbbrev": "Don",
"fridayAbbrev": "Fre",
"saturdayAbbrev": "Sam",
"sundayAbbrev": "Son",
"january": "Januar",
"february": "Februar",
"march": "März",
"april": "April",
"may": "Mai",
"june": "Juni",
"july": "Juli",
"august": "August",
"september": "September",
"october": "Oktober",
"november": "November",
"december": "Dezember",
"today": "Heute",
"yesterday": "Gestern"
},
"messages": {
"image": "Bild",
"video": "Video",
"audio": "Audio",
"file": "Datei",
"retracted": "Die Nachricht wurde zurückgezogen",
"retractedFallback": "Eine vorherige Nachricht wurde zurückgezogen. Dein Client unterstüzt dies jedoch nicht"
"image": "Bild",
"video": "Video",
"audio": "Audio",
"file": "Datei",
"sticker": "Sticker",
"retracted": "Die Nachricht wurde zurückgezogen",
"retractedFallback": "Eine vorherige Nachricht wurde zurückgezogen. Dein Client unterstüzt dies jedoch nicht",
"you": "Du"
},
"errors": {
"omemo": {
"couldNotPublish": "Konnte die kryptographische Identität nicht auf dem Server veröffentlichen. Ende-zu-Ende-Verschlüsselung funktioniert eventuell nicht.",
"notEncryptedForDevice": "Die Nachricht wurde nicht für dieses Gerät verschlüsselt",
"invalidHmac": "Die Nachricht konnte nicht entschlüsselt werden",
"noDecryptionKey": "Kein Schlüssel zum Entschlüsseln vorhanden",
"messageInvalidAfixElement": "Ungültige verschlüsselte Nachricht"
},
"connection": {
"connectionTimeout": "Verbindung zum Server nicht möglich"
},
"login": {
"saslFailed": "Ungültige Logindaten",
"startTlsFailed": "Konnte keine sichere Verbindung zum Server aufbauen",
"noConnection": "Konnte keine Verbindung zum Server aufbauen",
"unspecified": "Unbestimmter Fehler"
},
"message": {
"unspecified": "Unbekannter Fehler",
"fileUploadFailed": "Das Hochladen der Datei ist fehlgeschlagen",
"contactDoesntSupportOmemo": "Der Kontakt unterstützt Verschlüsselung mit OMEMO:2 nicht",
"fileDownloadFailed": "Das Herunterladen der Datei ist fehlgeschlagen",
"serviceUnavailable": "Die Nachricht konnte nicht gesendet werden",
"remoteServerTimeout": "Die Nachricht konnte nicht zugestellt werden",
"remoteServerNotFound": "Die Nachricht konnte nicht gesendet werden, da der Empfängerserver unbekannt ist",
"failedToEncrypt": "Die Nachricht konnte nicht verschlüsselt werden",
"failedToEncryptFile": "Die Datei konnte nicht verschlüsselt werden",
"failedToDecryptFile": "Die Datei konnte nicht entschlüsselt werden",
"fileNotEncrypted": "Der Chat ist verschlüsselt, aber die Datei wurde unverschlüsselt übertragen"
}
"general": {
"noInternet": "Keine Internetverbindung."
},
"filePicker": {
"permissionDenied": "Die Speicherberechtigung wurde nicht erteilt."
},
"omemo": {
"couldNotPublish": "Konnte die kryptographische Identität nicht auf dem Server veröffentlichen. Ende-zu-Ende-Verschlüsselung funktioniert eventuell nicht.",
"notEncryptedForDevice": "Die Nachricht wurde nicht für dieses Gerät verschlüsselt",
"invalidHmac": "Die Nachricht konnte nicht entschlüsselt werden",
"noDecryptionKey": "Kein Schlüssel zum Entschlüsseln vorhanden",
"messageInvalidAfixElement": "Ungültige verschlüsselte Nachricht",
"verificationInvalidOmemoUrl": "Ungültiger OMEMO:2 Fingerabdruck",
"verificationWrongJid": "Falsche XMPP-Addresse",
"verificationWrongDevice": "Falsches OMEMO:2 Gerät",
"verificationNotInList": "OMEMO:2 Gerät unbekannt",
"verificationWrongFingerprint": "Falscher OMEMO:2 Fingerabdruck"
},
"connection": {
"connectionTimeout": "Verbindung zum Server nicht möglich",
"saslAccountDisabled": "Dein Konto ist deaktiviert",
"saslInvalidCredentials": "Deine Anmeldedaten sind ungültig",
"unrecoverable": "Verbindung zum Server durch nicht behebbaren Fehler verloren"
},
"login": {
"saslFailed": "Ungültige Logindaten",
"startTlsFailed": "Konnte keine sichere Verbindung zum Server aufbauen",
"noConnection": "Konnte keine Verbindung zum Server aufbauen",
"unspecified": "Unbestimmter Fehler"
},
"message": {
"unspecified": "Unbekannter Fehler",
"fileUploadFailed": "Das Hochladen der Datei ist fehlgeschlagen",
"contactDoesntSupportOmemo": "Der Kontakt unterstützt Verschlüsselung mit OMEMO:2 nicht",
"fileDownloadFailed": "Das Herunterladen der Datei ist fehlgeschlagen",
"serviceUnavailable": "Die Nachricht konnte nicht gesendet werden",
"remoteServerTimeout": "Die Nachricht konnte nicht zugestellt werden",
"remoteServerNotFound": "Die Nachricht konnte nicht gesendet werden, da der Empfängerserver unbekannt ist",
"failedToEncrypt": "Die Nachricht konnte nicht verschlüsselt werden",
"failedToEncryptFile": "Die Datei konnte nicht verschlüsselt werden",
"failedToDecryptFile": "Die Datei konnte nicht entschlüsselt werden",
"fileNotEncrypted": "Der Chat ist verschlüsselt, aber die Datei wurde unverschlüsselt übertragen"
},
"conversation": {
"audioRecordingError": "Fehler beim Fertigstellen der Audioaufnahme",
"openFileNoAppError": "Keine App vorhanden, um die Datei zu öffnen",
"openFileGenericError": "Fehler beim Öffnen der Datei",
"messageErrorDialogTitle": "Fehler"
},
"newChat": {
"remoteServerError": "Konnte den Server nicht erreichen.",
"groupchatUnsupported": "Das Beitreten eines Gruppenchats ist aktuell nicht unterstützt.",
"unknown": "Unbekannter Fehler."
}
},
"warnings": {
"message": {
"integrityCheckFailed": "Konnte Integrität der Datei nicht überprüfen"
}
"message": {
"integrityCheckFailed": "Konnte Integrität der Datei nicht überprüfen"
},
"conversation": {
"holdForLonger": "Button länger gedrückt halten, um eine Sprachnachricht aufzunehmen"
}
},
"pages": {
"intro": {
"noAccount": "Kein XMPP-Account vorhanden? Einen zu erstellen ist sehr einfach.",
"loginButton": "Einloggen",
"registerButton": "Registrieren"
},
"login": {
"title": "Login",
"xmppAddress": "XMPP-Adresse",
"password": "Passwort",
"advancedOptions": "Fortgeschrittene Optionen",
"createAccount": "Account auf dem Server erstellen"
},
"conversations": {
"speeddialNewChat": "Neuer chat",
"speeddialJoinGroupchat": "Gruppenchat beitreten",
"overlaySettings": "Einstellungen",
"noOpenChats": "Du hast keine offenen chats",
"startChat": "Einen chat anfangen"
},
"conversation": {
"unencrypted": "Unverschlüsselt",
"encrypted": "Verschlüsselt",
"closeChat": "Chat schließen",
"closeChatConfirmTitle": "Chat schließen",
"closeChatConfirmSubtext": "Bist Du dir sicher, dass du den Chat schließen möchtest?",
"blockUser": "Nutzer blockieren",
"online": "Online",
"retract": "Nachricht löschen",
"retractBody": "Bist du dir sicher, dass du die Nachricht löschen willst? Bedenke, dass dies nur eine Bitte ist, die dein gegenüber nicht beachten muss.",
"forward": "Weiterleiten",
"edit": "Bearbeiten",
"quote": "Zitieren"
},
"addcontact": {
"title": "Neuen Kontakt hinzufügen",
"xmppAddress": "XMPP-Adresse",
"subtitle": "Du kannst einen Kontakt hinzufügen, indem Du entweder die XMPP-Adresse eingibst oder den QR-Code deines Kontaktes scannst",
"buttonAddToContact": "Kontakt hinzufügen"
},
"newconversation": {
"title": "Neuer chat",
"addContact": "Kontakt hinzufügen",
"createGroupchat": "Gruppenchat erstellen"
},
"crop": {
"setProfilePicture": "Als Profilbild festlegen"
},
"shareselection": {
"shareWith": "Teilen mit...",
"confirmTitle": "Dateien senden?",
"confirmBody": "Einer oder mehr Chats sind unverschlüsselt. Das bedeutet, dass die Dateien dem Server unverschlüsselt vorliegen. Dateien trotzdem senden?"
},
"profile": {
"self": {
"devices": "Geräte"
},
"conversation": {
"muteChatTooltip": "Chat stummschalten",
"unmuteChatTooltip": "Chat lautstellen",
"muteChat": "Stummschalten",
"unmuteChat": "Lautstellen",
"devices": "Geräte"
},
"owndevices": {
"title": "Eigene Geräte",
"thisDevice": "Dieses Gerät",
"otherDevices": "Andere Geräte",
"deleteDeviceConfirmTitle": "Gerät löschen",
"deleteDeviceConfirmBody": "Das bedeutet, dass Kontakte für dieses Gerät nichtmehr verschlüsseln können. Fortfahren?",
"recreateOwnSessions": "Sessions neuerstellen",
"recreateOwnSessionsConfirmTitle": "Eigene Sessions neuerstellen?",
"recreateOwnSessionsConfirmBody": "Das wird alle kryptographischen Sessions mit den eigenen Geräten neuerstellen. Verwende dies nur, wenn deine eigenen Geräte Entschlüsselungsfehler erzeugen.",
"recreateOwnDevice": "Gerät neuerstellen",
"recreateOwnDeviceConfirmTitle": "Gerät neuerstellen?",
"recreateOwnDeviceConfirmBody": "Das wird die kryptographische Identität dieses Geräts neu erstellen. Wenn Kontakte die kryptographische Indentität verifiziert haben, dann müssen diese es erneut tun. Fortfahren?"
},
"devices": {
"title": "Devices",
"recreateSessions": "Rebuild sessions",
"recreateSessionsConfirmTitle": "Rebuild sessions?",
"recreateSessionsConfirmBody": "This will recreate the cryptographic sessions with your own devices. Use only if your own devices throw decryption errors."
}
},
"blocklist": {
"title": "Blockliste",
"noUsersBlocked": "Du hast niemanden blockiert",
"unblockAll": "Alle entblocken",
"unblockAllConfirmTitle": "Alle entblocken",
"unblockAllConfirmBody": "Bist Du dir sicher, dass du alle geblockten Personen entblocken möchtest?",
"unblockJidConfirmTitle": "${jid} entblocken?",
"unblockJidConfirmBody": "Bist du dir sicher, dass du ${jid} entblocken möchtest? Du wirst wieder Nachrichten von dieser Person erhalten können."
},
"settings": {
"settings": {
"title": "Einstellungen",
"conversationsSection": "Unterhaltungen",
"accountSection": "Account",
"signOut": "Abmelden",
"signOutConfirmTitle": "Abmelden",
"signOutConfirmBody": "Du bist dabei dich abzumelden. Fortfahren?",
"miscellaneousSection": "Unterschiedlich",
"debuggingSection": "Debugging"
},
"about": {
"title": "Über",
"licensed": "Lizensiert unter GPL3",
"viewSourceCode": "Quellcode anschauen"
},
"appearance": {
"title": "Aussehen",
"languageSection": "Sprache",
"language": "Appsprache",
"languageSubtext": "Aktuell ausgewählt: $selectedLanguage",
"systemLanguage": "Systemsprache"
},
"licenses": {
"title": "Open-Source Lizenzen",
"licensedUnder": "Lizensiert unter $license"
},
"conversation": {
"title": "Chat",
"appearance": "Aussehen",
"selectBackgroundImage": "Hintergrundbild auswählen",
"selectBackgroundImageDescription": "Dieses Bild wird als Hintergrundbild in allen Chats verwendet",
"removeBackgroundImage": "Hintergrundbild entfernen",
"removeBackgroundImageConfirmTitle": "Hintergrundbild entfernen",
"removeBackgroundImageConfirmBody": "Bist Du dir sicher, dass Du das Hintergrundbild entfernen möchtest?",
"newChatsSection": "Neue Chats",
"newChatsMuteByDefault": "Neue Chats standardmäßig stummschalten",
"newChatsE2EE": "Ende-zu-Ende-Verschlüsselung standardmäßig aktivieren. WARNUNG: Experimentell"
},
"debugging": {
"title": "Debuggingoptionen",
"generalSection": "Generell",
"generalEnableDebugging": "Debugging einschalten",
"generalEncryptionPassword": "Verschlüsselungspasswort",
"generalEncryptionPasswordSubtext": "Die Logs enthalten eventuell sensible Daten. Wähle also daher eine starke Passphrase",
"generalLoggingIp": "Logging-IP",
"generalLoggingIpSubtext": "Die IP-Adresse an die die Logs gesendet werden",
"generalLoggingPort": "Logging-Port",
"generalLoggingPortSubtext": "Der Port an den die Logs gesendet werden"
},
"network": {
"title": "Netzwerk",
"automaticDownloadsSection": "Automatische Downloads",
"automaticDownloadsText": "Moxxy läd Dateien automatisch herunter, wenn verbunden mit...",
"automaticDownloadsMaximumSize": "Maximale Downloadgröße",
"automaticDownloadsMaximumSizeSubtext": "Die maximale Dateigröße, die automatisch heruntergeladen werden soll",
"wifi": "Wifi",
"mobileData": "Mobile Daten"
},
"privacy": {
"title": "Privatsphäre",
"generalSection": "Generell",
"showContactRequests": "Kontaktanfragen zeigen",
"showContactRequestsSubtext": "Dies zeigt Personen in der Chatübersicht an, die Dich zu ihrer Kontaktliste hinzugefügt haben, aber noch keine Nachricht gesendet haben",
"profilePictureVisibility": "Öffentliches Profilbild",
"profilePictureVisibilitSubtext": "Wenn aktiviert, dann kann jeder Dein Profilbild sehen. Wenn deaktiviert, dann können nur Personen aus deiner Kontaktliste kein Profilbild sehen",
"autoAcceptSubscriptionRequests": "Subscriptionanfragen automatisch annehmen",
"autoAcceptSubscriptionRequestsSubtext": "Wenn aktiviert, dann werden Subscriptionanfragen automatisch angenommen, wenn die Person in deiner Kontaktliste ist",
"conversationsSection": "Unterhaltungen",
"sendChatMarkers": "Chatmarker senden",
"sendChatMarkersSubtext": "Dies teilt Deinem Gesprächspartner mit, ob du Nachrichten empfangen oder gelesen hast",
"sendChatStates": "Chatstates senden",
"sendChatStatesSubtext": "Dies teilt Deinem Gesprächspartner mit, ob du gerade im Chat aktiv bist oder schreibst",
"redirectsSection": "Weiterleitungen",
"redirectText": "Dies leitet Links von $serviceName, die du öffnest, an einen Proxydienst weiter, wie zum Beispiel $exampleProxy",
"currentlySelected": "Aktuell ausgewählt: $proxy",
"redirectsTitle": "${serviceName}weiterleitung",
"cannotEnableRedirect": "Kann ${serviceName}weiterleitung nicht aktivieren",
"cannotEnableRedirectSubtext": "Du must zuerst einen Proxydienst auswählen. Dazu berühre das Feld neben dem Schalter.",
"urlEmpty": "URL kann nicht leer sein",
"urlInvalid": "Ungültige URL",
"redirectDialogTitle": "${serviceName}weiterleitung"
}
}
"intro": {
"noAccount": "Kein XMPP-Konto vorhanden? Keine Sorge, es ist ganz einfach, eines zu erstellen.",
"loginButton": "Einloggen",
"registerButton": "Registrieren"
},
"login": {
"title": "Login",
"xmppAddress": "XMPP-Adresse",
"password": "Passwort",
"advancedOptions": "Fortgeschrittene Optionen",
"createAccount": "Konto auf dem Server erstellen"
},
"conversations": {
"speeddialNewChat": "Neuer chat",
"speeddialJoinGroupchat": "Gruppenchat beitreten",
"speeddialAddNoteToSelf": "Notiz an mich",
"overlaySettings": "Einstellungen",
"noOpenChats": "Du hast keine offenen chats",
"startChat": "Einen chat anfangen",
"closeChat": "Chat schließen",
"closeChatBody": "Bist du dir sicher, dass du den Chat mit ${conversationTitle} schließen möchtest?",
"markAsRead": "Als gelesen markieren"
},
"conversation": {
"unencrypted": "Unverschlüsselt",
"encrypted": "Verschlüsselt",
"closeChat": "Chat schließen",
"closeChatConfirmTitle": "Chat schließen",
"closeChatConfirmSubtext": "Bist Du dir sicher, dass du den Chat schließen möchtest?",
"blockShort": "Blockieren",
"blockUser": "Nutzer blockieren",
"online": "Online",
"retract": "Nachricht löschen",
"retractBody": "Bist du dir sicher, dass du die Nachricht löschen willst? Bedenke, dass dies nur eine Bitte ist, die dein gegenüber nicht beachten muss.",
"forward": "Weiterleiten",
"edit": "Bearbeiten",
"quote": "Zitieren",
"copy": "Inhalt kopieren",
"messageCopied": "Nachrichteninhalt in die Zwischenablage kopiert",
"addReaction": "Reaktion hinzufügen",
"showError": "Fehler anzeigen",
"showWarning": "Warnung anzeigen",
"warning": "Warnung",
"addToContacts": "Zu Kontaken hinzufügen",
"addToContactsTitle": "${jid} zu Kontakten hinzufügen",
"addToContactsBody": "Bist du dir sicher, dass du ${jid} zu deinen Kontakten hinzufügen möchtest?",
"stickerPickerNoStickersLine1": "Du hast keine Stickerpacks installiert.",
"stickerPickerNoStickersLine2": "Diese können in den Stickereinstellungen installiert werden.",
"stickerSettings": "Stickereinstellungen",
"newDeviceMessage": {
"one": "Ein neues Gerät wurde hinzugefügt",
"other": "Mehrere neue Geräte wurden hinzugefügt"
},
"replacedDeviceMessage": {
"one": "Ein Gerät hat sich verändert",
"other": "Mehrere Geräte haben sich verändert"
},
"messageHint": "Nachricht senden...",
"sendImages": "Bilder senden",
"sendFiles": "Dateien senden",
"takePhotos": "Bilder aufnehmen"
},
"startchat": {
"title": "Neuer Chat",
"xmppAddress": "XMPP-Adresse",
"subtitle": "Du kannst einen neuen Chat beginnen, indem du entweder eine XMPP-Adresse eingibst oder einen QR-Code scannst.",
"buttonAddToContact": "Neuen Chat beginnen"
},
"newconversation": {
"title": "Neuer Chat",
"startChat": "Neuen Chat beginnen",
"createGroupchat": "Gruppenchat erstellen"
},
"crop": {
"setProfilePicture": "Als Profilbild festlegen"
},
"shareselection": {
"shareWith": "Teilen mit...",
"confirmTitle": "Dateien senden?",
"confirmBody": "Einer oder mehr Chats sind unverschlüsselt. Das bedeutet, dass die Dateien dem Server unverschlüsselt vorliegen. Dateien trotzdem senden?"
},
"profile": {
"general": {
"omemo": "Sicherheit",
"profile": "Profil",
"media": "Medien"
},
"conversation": {
"notifications": "Benachrichtigungen",
"notificationsMuted": "Stumm",
"notificationsEnabled": "Eingeschaltet",
"sharedMedia": "Medien"
},
"owndevices": {
"title": "Eigene Geräte",
"thisDevice": "Dieses Gerät",
"otherDevices": "Andere Geräte",
"deleteDeviceConfirmTitle": "Gerät löschen",
"deleteDeviceConfirmBody": "Das bedeutet, dass Kontakte für dieses Gerät nichtmehr verschlüsseln können. Fortfahren?",
"recreateOwnSessions": "Sessions neuerstellen",
"recreateOwnSessionsConfirmTitle": "Eigene Sessions neuerstellen?",
"recreateOwnSessionsConfirmBody": "Das wird alle kryptographischen Sessions mit den eigenen Geräten neuerstellen. Verwende dies nur, wenn deine eigenen Geräte Entschlüsselungsfehler erzeugen.",
"recreateOwnDevice": "Gerät neuerstellen",
"recreateOwnDeviceConfirmTitle": "Gerät neuerstellen?",
"recreateOwnDeviceConfirmBody": "Das wird die kryptographische Identität dieses Geräts neu erstellen. Wenn Kontakte die kryptographische Indentität verifiziert haben, dann müssen diese es erneut tun. Fortfahren?"
},
"devices": {
"title": "Sicherheit",
"recreateSessions": "Sessions zurücksetzen",
"recreateSessionsConfirmTitle": "Sessions zurücksetzen?",
"recreateSessionsConfirmBody": "Dies wird alle Sessions mit Deinen Geräten neu erstellen. Tue dies nur, wenn deine Geräte Fehler beim Entschlüsseln erzeugen.",
"noSessions": "Es sind keine kryptographischen Sessions vorhanden, die für Ende-zu-Ende-Verschlüsselung verwendet werden."
}
},
"blocklist": {
"title": "Blockliste",
"noUsersBlocked": "Du hast niemanden blockiert",
"unblockAll": "Alle entblocken",
"unblockAllConfirmTitle": "Alle entblocken",
"unblockAllConfirmBody": "Bist Du dir sicher, dass du alle geblockten Personen entblocken möchtest?",
"unblockJidConfirmTitle": "${jid} entblocken?",
"unblockJidConfirmBody": "Bist du dir sicher, dass du ${jid} entblocken möchtest? Du wirst wieder Nachrichten von dieser Person erhalten können."
},
"cropbackground": {
"blur": "Hintergrund weichzeichnen",
"setAsBackground": "Als Hintergrundbild festlegen"
},
"stickerPack": {
"removeConfirmTitle": "Stickerpack entfernen",
"removeConfirmBody": "Bist Du Dir sicher, dass du das Stickerpack entfernen möchtest?",
"installConfirmTitle": "Stickerpack installieren",
"installConfirmBody": "Bist Du Dir sicher, dass Du das Stickerpack installieren möchtest?",
"restricted": "Dieses Stickerpack ist eingeschränkt. Das bedeutet, dass es im Chat angezeigt wird, jedoch nicht versendet werden kann.",
"fetchingFailure": "Konnte das Stickerpack nicht finden"
},
"settings": {
"settings": {
"title": "Einstellungen",
"conversationsSection": "Unterhaltungen",
"accountSection": "Konto",
"signOut": "Abmelden",
"signOutConfirmTitle": "Abmelden",
"signOutConfirmBody": "Du bist dabei dich abzumelden. Fortfahren?",
"miscellaneousSection": "Unterschiedlich",
"debuggingSection": "Debugging",
"general": "Generell"
},
"about": {
"title": "Über",
"licensed": "Lizensiert unter GPL3",
"version": "Version ${version}",
"viewSourceCode": "Quellcode anschauen",
"nMoreToGo": "Noch ${n}...",
"debugMenuShown": "Du bist jetzt ein Entwickler!",
"debugMenuAlreadyShown": "Du bist bereits ein Entwickler!"
},
"appearance": {
"title": "Aussehen",
"languageSection": "Sprache",
"language": "Appsprache",
"languageSubtext": "Aktuell ausgewählt: $selectedLanguage",
"systemLanguage": "Systemsprache"
},
"licenses": {
"title": "Open-Source Lizenzen",
"licensedUnder": "Lizensiert unter $license"
},
"conversation": {
"title": "Chat",
"appearance": "Aussehen",
"selectBackgroundImage": "Hintergrundbild auswählen",
"selectBackgroundImageDescription": "Dieses Bild wird als Hintergrundbild in allen Chats verwendet",
"removeBackgroundImage": "Hintergrundbild entfernen",
"removeBackgroundImageConfirmTitle": "Hintergrundbild entfernen",
"removeBackgroundImageConfirmBody": "Bist Du dir sicher, dass Du das Hintergrundbild entfernen möchtest?",
"newChatsSection": "Neue Chats",
"newChatsMuteByDefault": "Neue Chats standardmäßig stummschalten",
"newChatsE2EE": "Ende-zu-Ende-Verschlüsselung standardmäßig aktivieren. WARNUNG: Experimentell",
"behaviourSection": "Verhalten",
"contactsIntegration": "Kontaktintegration",
"contactsIntegrationBody": "Wenn aktiviert, dann werden Kontakte aus dem Kontaktbuch verwendet, um Chatnamen und Profilbilder anzuzeigen. Dabei werden keine Daten an den Server gesendet."
},
"debugging": {
"title": "Debuggingoptionen",
"generalSection": "Generell",
"generalEnableDebugging": "Debugging einschalten",
"generalEncryptionPassword": "Verschlüsselungspasswort",
"generalEncryptionPasswordSubtext": "Die Logs enthalten eventuell sensible Daten. Wähle also daher eine starke Passphrase",
"generalLoggingIp": "Logging-IP",
"generalLoggingIpSubtext": "Die IP-Adresse an die die Logs gesendet werden",
"generalLoggingPort": "Logging-Port",
"generalLoggingPortSubtext": "Der Port an den die Logs gesendet werden"
},
"network": {
"title": "Netzwerk",
"automaticDownloadsSection": "Automatische Downloads",
"automaticDownloadsText": "Moxxy läd Dateien automatisch herunter, wenn verbunden mit...",
"automaticDownloadsMaximumSize": "Maximale Downloadgröße",
"automaticDownloadsMaximumSizeSubtext": "Die maximale Dateigröße, die automatisch heruntergeladen werden soll",
"automaticDownloadAlways": "Immer",
"wifi": "WLAN",
"mobileData": "Mobile Daten"
},
"privacy": {
"title": "Privatsphäre",
"generalSection": "Generell",
"showContactRequests": "Kontaktanfragen zeigen",
"showContactRequestsSubtext": "Dies zeigt Personen in der Chatübersicht an, die Dich zu ihrer Kontaktliste hinzugefügt haben, aber noch keine Nachricht gesendet haben",
"profilePictureVisibility": "Öffentliches Profilbild",
"profilePictureVisibilitSubtext": "Wenn aktiviert, dann kann jeder Dein Profilbild sehen. Wenn deaktiviert, dann können nur Personen aus deiner Kontaktliste kein Profilbild sehen",
"conversationsSection": "Unterhaltungen",
"sendChatMarkers": "Chatmarker senden",
"sendChatMarkersSubtext": "Dies teilt Deinem Gesprächspartner mit, ob du Nachrichten empfangen oder gelesen hast",
"sendChatStates": "Chatstates senden",
"sendChatStatesSubtext": "Dies teilt Deinem Gesprächspartner mit, ob du gerade im Chat aktiv bist oder schreibst",
"redirectsSection": "Weiterleitungen",
"redirectText": "Dies leitet Links von $serviceName, die du öffnest, an einen Proxydienst weiter, wie zum Beispiel $exampleProxy",
"currentlySelected": "Aktuell ausgewählt: $proxy",
"redirectsTitle": "${serviceName}weiterleitung",
"cannotEnableRedirect": "Kann ${serviceName}weiterleitung nicht aktivieren",
"cannotEnableRedirectSubtext": "Du must zuerst einen Proxydienst auswählen. Dazu berühre das Feld neben dem Schalter.",
"urlEmpty": "URL kann nicht leer sein",
"urlInvalid": "Ungültige URL",
"redirectDialogTitle": "${serviceName}weiterleitung",
"stickersPrivacy": "Stickerliste öffentlich halten",
"stickersPrivacySubtext": "Wenn eingeschaltet, dann kann jeder die Liste Deiner installierten Stickerpacks sehen."
},
"stickers": {
"title": "Stickers",
"stickerSection": "Sticker",
"displayStickers": "Sticker im Chat anzeigen",
"autoDownload": "Sticker automatisch herunterladen",
"autoDownloadBody": "Wenn aktiviert, dann werden Sticker automatisch heruntergeladen, wenn der Sender in der Kontaktliste ist.",
"stickerPacksSection": "Stickerpacks",
"importStickerPack": "Stickerpack importieren",
"importSuccess": "Stickerpack erfolgreich importiert",
"importFailure": "Beim Import des Stickerpacks ist ein Fehler aufgetreten",
"stickerPackSize": "(${size})"
},
"storage": {
"title": "Speicher",
"sizePlaceholder": "Berechne...",
"storageManagement": "Speicherverwaltung",
"removeOldMedia": {
"title": "Alte Medien entfernen",
"description": "Löscht alte Medien vom Gerät"
},
"removeOldMediaDialog": {
"title": "Medien löschen",
"options": {
"all": "Alle Medien",
"oneMonth": "Älter als 1 Monat",
"oneWeek": "Älter als 1 Woche"
},
"delete": "Löschen",
"confirmation": {
"body": "Bist Du dir sicher, dass du alte Medien löschen möchtest?"
}
},
"viewMediaFiles": "Medien anzeigen",
"mediaFiles": "Medien",
"types": {
"media": "Medien",
"stickers": "Sticker"
},
"manageStickers": "Stickerpacks verwalten",
"storageUsed": "Speicherplatz verbraucht: ${size}"
}
},
"sharedMedia": {
"empty": {
"chat": "Keine Medien für diesen Chat vorhanden",
"general": "Keine Medien vorhanden"
}
}
}
}

View File

@@ -0,0 +1,477 @@
{
"language": "English",
"global": {
"title": "Moxxy",
"moxxySubtitle": "An experiment into building a modern, easy and beautiful XMPP client.",
"dialogAccept": "Okay",
"dialogCancel": "Cancel",
"yes": "Yes",
"no": "No"
},
"notifications": {
"permanent": {
"idle": "Idle",
"ready": "Ready to receive messages",
"connecting": "Connecting…",
"disconnect": "Disconnected",
"error": "Error"
},
"message": {
"reply": "Reply",
"markAsRead": "Mark as read"
},
"channels": {
"messagesChannelName": "Messages",
"messagesChannelDescription": "The notification channel for received messages",
"warningChannelName": "Warnings",
"warningChannelDescription": "Warnings related to Moxxy",
"serviceChannelName": "Foreground Service",
"serviceChannelDescription": "Holds the persistent foreground service notification"
},
"titles": {
"error": "Error"
},
"warnings": {
"blockingError": {
"title": "Error while blocking user",
"body": "Could not block user ${jid} because your server does not support blocking."
}
},
"errors": {
"messageError": {
"title": "Message delivery failure",
"body": "Failed to deliver message to ${conversationTitle}"
}
}
},
"permissions": {
"requests": {
"notification": {
"reason": "In order to notify of incoming messages, Moxxy needs permission to show notifications."
},
"batterySaving": {
"reason": "In order to receive messages in the background, Moxxy should be excempt from Android's battery saving."
}
},
"allow": "Allow",
"skip": "Skip"
},
"emojiPicker": {
"noRecents": "No recents"
},
"dateTime": {
"justNow": "Just now",
"nMinutesAgo": "${min}min ago",
"mondayAbbrev": "Mon",
"tuesdayAbbrev": "Tue",
"wednessdayAbbrev": "Wed",
"thursdayAbbrev": "Thu",
"fridayAbbrev": "Fri",
"saturdayAbbrev": "Sat",
"sundayAbbrev": "Sun",
"january": "January",
"february": "February",
"march": "March",
"april": "April",
"may": "May",
"june": "June",
"july": "July",
"august": "August",
"september": "September",
"october": "October",
"november": "November",
"december": "December",
"today": "Today",
"yesterday": "Yesterday"
},
"messages": {
"image": "Image",
"video": "Video",
"audio": "Audio",
"file": "File",
"sticker": "Sticker",
"retracted": "The message has been retracted",
"retractedFallback": "A previous message has been retracted but your client does not support it",
"you": "You"
},
"errors": {
"general": {
"noInternet": "Not connected to the Internet."
},
"filePicker": {
"permissionDenied": "The storage permission has been denied."
},
"omemo": {
"couldNotPublish": "Could not publish the cryptographic identity to the server. This means that end-to-end encryption may not work.",
"notEncryptedForDevice": "This message was not encrypted for this device",
"invalidHmac": "Could not decrypt message",
"noDecryptionKey": "No decryption key available",
"messageInvalidAfixElement": "Invalid encrypted message",
"verificationInvalidOmemoUrl": "Invalid OMEMO:2 fingerprint",
"verificationWrongJid": "Wrong XMPP-address",
"verificationWrongDevice": "Wrong OMEMO:2 device",
"verificationNotInList": "Wrong OMEMO:2 device",
"verificationWrongFingerprint": "Wrong OMEMO:2 fingerprint"
},
"connection": {
"connectionTimeout": "Could not connect to server",
"saslAccountDisabled": "Your account is disabled",
"saslInvalidCredentials": "Your account credentials are invalid",
"unrecoverable": "Connection lost due to unrecoverable error"
},
"login": {
"saslFailed": "Invalid login credentials",
"startTlsFailed": "Failed to establish a secure connection",
"noConnection": "Failed to establish a connection",
"unspecified": "Unspecified error"
},
"message": {
"unspecified": "Unknown error",
"fileUploadFailed": "The file upload failed",
"contactDoesntSupportOmemo": "The contact does not support encryption using OMEMO:2",
"fileDownloadFailed": "The file download failed",
"serviceUnavailable": "The message could not be delivered to the contact",
"remoteServerTimeout": "The message could not be delivered to the contact's server",
"remoteServerNotFound": "The message could not be delivered to the contact's server as it cannot be found",
"failedToEncrypt": "The message could not be encrypted",
"failedToEncryptFile": "The file could not be encrypted",
"failedToDecryptFile": "The file could not be decrypted",
"fileNotEncrypted": "The chat is encrypted but the file is not encrypted"
},
"conversation": {
"audioRecordingError": "Failed to finalize audio recording",
"openFileNoAppError": "No app found to open this file",
"openFileGenericError": "Failed to open file",
"messageErrorDialogTitle": "Error"
},
"newChat": {
"remoteServerError": "Failed to contact the remote server.",
"groupchatUnsupported": "Joining a groupchat is currently not supported.",
"unknown": "Unknown error."
}
},
"warnings": {
"message": {
"integrityCheckFailed": "Could not verify file integrity"
},
"conversation": {
"holdForLonger": "Hold button longer to record a voice message",
"microphoneDenied": "Microphon access denied"
}
},
"pages": {
"intro": {
"noAccount": "Have no XMPP account? No worries, creating one is really easy.",
"loginButton": "Login",
"registerButton": "Register"
},
"login": {
"title": "Login",
"xmppAddress": "XMPP-Address",
"password": "Password",
"advancedOptions": "Advanced options",
"createAccount": "Create account on server",
"login": "Login"
},
"home": {
"addAccount": "Add new account",
"searchNoResults": "No search results...",
"favourite": "Favourite",
"unfavourite": "Unfavourite",
"search": "Search..."
},
"conversations": {
"speeddialNewChat": "New chat",
"speeddialJoinGroupchat": "Join groupchat",
"speeddialAddNoteToSelf": "Note to self",
"overlaySettings": "Settings",
"noOpenChats": "You have no open chats",
"startChat": "Start a chat",
"closeChat": "Close chat",
"closeChatBody": "Are you sure you want to close the chat with ${conversationTitle}?",
"markAsRead": "Mark as read"
},
"conversation": {
"unencrypted": "Unencrypted",
"encrypted": "Encrypted",
"closeChat": "Close chat",
"closeChatConfirmTitle": "Close chat",
"closeChatConfirmSubtext": "Are you sure you want to close this chat?",
"blockShort": "Block",
"blockUser": "Block user",
"online": "Online",
"retract": "Retract message",
"retractBody": "Are you sure you want to retract the message? Keep in mind that this is only a request that the client does not have to honour.",
"forward": "Forward",
"share": "Share",
"edit": "Edit",
"quote": "Quote",
"copy": "Copy content",
"messageCopied": "Message content copied to clipboard",
"addReaction": "Add reaction",
"showError": "Show error",
"showWarning": "Show warning",
"warning": "Warning",
"addToContacts": "Add to contacts",
"addToContactsTitle": "Add ${jid} to contacts",
"addToContactsBody": "Are you sure you want to add ${jid} to your contacts?",
"stickerPickerNoStickersLine1": "You have no sticker packs installed.",
"stickerPickerNoStickersLine2": "They can be installed in the sticker settings.",
"stickerSettings": "Sticker settings",
"newDeviceMessage": {
"one": "A new device has been added",
"other": "Multiple new devices have been added"
},
"replacedDeviceMessage": {
"one": "A device has been changed",
"other": "Multiple devices have been added"
},
"messageHint": "Send a message…",
"sendMedia": "Send media",
"sendFiles": "Send files",
"takePhotos": "Take photos",
"voiceRecording": {
"cancel": "Cancel",
"dragToCancel": "Drag to cancel",
"leaveConfirmation": {
"title": "Cancel recording",
"body": "You are currently recording a voice message. Do you want to discard the recording?",
"affirmative": "Discard",
"negative": "Cancel"
}
}
},
"startchat": {
"title": "New Chat",
"xmppAddress": "XMPP address",
"subtitle": "You can start a new chat by either entering a XMPP address or by scanning their QR code.",
"buttonAddToContact": "Start new chat"
},
"newconversation": {
"title": "New chat",
"startChat": "Start new chat",
"createGroupchat": "New groupchat",
"joinGroupChat": "Join groupchat",
"nullNickname": "Nickname cannot be null!",
"nick": "Nickname",
"enterNickname": "Enter Nickname",
"nicknameSubtitle": "You need to enter a unique nickname in order to join a MUC."
},
"crop": {
"setProfilePicture": "Set as profile picture"
},
"shareselection": {
"shareWith": "Share with…",
"confirmTitle": "Send file",
"confirmBody": "One or more chats are unencrypted. This means that the file will be leaked to the server. Do you still want to continue?"
},
"profile": {
"general": {
"omemo": "Security",
"profile": "Profile",
"media": "Media"
},
"conversation": {
"notifications": "Notifications",
"notificationsMuted": "Muted",
"notificationsEnabled": "Enabled",
"sharedMedia": "Media"
},
"owndevices": {
"title": "Own Devices",
"thisDevice": "This device",
"otherDevices": "Other devices",
"deleteDeviceConfirmTitle": "Delete device",
"deleteDeviceConfirmBody": "This means that contacts will not be able to encrypt for that device. Continue?",
"recreateOwnSessions": "Rebuild sessions",
"recreateOwnSessionsConfirmTitle": "Recreate own sessions?",
"recreateOwnSessionsConfirmBody": "This will recreate the cryptographic sessions with your own devices. Use only if your own devices throw decryption errors.",
"recreateOwnDevice": "Recreate device",
"recreateOwnDeviceConfirmTitle": "Recreate own device?",
"recreateOwnDeviceConfirmBody": "This will recreate this device's cryptographic identity. It will take some time. If contacts verified your device, they will have to do it again. Continue?"
},
"devices": {
"title": "Security",
"recreateSessions": "Rebuild sessions",
"recreateSessionsConfirmTitle": "Rebuild sessions?",
"recreateSessionsConfirmBody": "This will recreate the cryptographic sessions with your own devices. Use only if your own devices throw decryption errors.",
"noSessions": "There are no cryptographic sessions that are used for end-to-end encryption."
},
"groupchat": {
"owner": "Owner",
"admin": "Administrator"
},
"serverInfo": {
"title": "Server Information"
}
},
"blocklist": {
"title": "Blocklist",
"noUsersBlocked": "You have no users blocked",
"unblockAll": "Unblock all",
"unblockAllConfirmTitle": "Are you sure?",
"unblockAllConfirmBody": "Are you sure you want to unblock all users?",
"unblockJidConfirmTitle": "Unblock ${jid}?",
"unblockJidConfirmBody": "Are you sure you want to unblock ${jid}? You will receive messages from this user again."
},
"cropbackground": {
"blur": "Blur background",
"setAsBackground": "Set as background image"
},
"stickerPack": {
"removeConfirmTitle": "Remove sticker pack",
"removeConfirmBody": "Are you sure you want to remove this sticker pack?",
"installConfirmTitle": "Install sticker pack",
"installConfirmBody": "Are you sure you want to install this sticker pack?",
"restricted": "This sticker pack is restricted. That means that the stickers will be displayed but cannot be sent.",
"fetchingFailure": "Could not find the sticker pack"
},
"sharedMedia": {
"empty": {
"chat": "No shared media for this chat",
"general": "No media files available"
}
},
"settings": {
"settings": {
"title": "Settings",
"conversationsSection": "Conversations",
"accountSection": "Account",
"signOut": "Sign out",
"signOutConfirmTitle": "Sign Out",
"signOutConfirmBody": "You are about to sign out. Proceed?",
"miscellaneousSection": "Miscellaneous",
"debuggingSection": "Debugging",
"general": "General"
},
"about": {
"title": "About",
"licensed": "Licensed under GPL3",
"version": "Version ${version}",
"viewSourceCode": "View source code",
"nMoreToGo": "${n} more to go…",
"debugMenuShown": "You are now a developer!",
"debugMenuAlreadyShown": "You are already a developer!",
"specialThanks": {
"iconDesignedBy": "Icon designed by",
"uiDesignedBy": "UI designed by"
}
},
"appearance": {
"title": "Appearance",
"languageSection": "Language",
"language": "App language",
"languageSubtext": "Currently selected: $selectedLanguage",
"systemLanguage": "Default language"
},
"licenses": {
"title": "Open-Source Licenses",
"licensedUnder": "Licensed under $license"
},
"conversation": {
"title": "Chat",
"appearance": "Appearance",
"selectBackgroundImage": "Select background image",
"selectBackgroundImageDescription": "This image will be the background of all your chats",
"removeBackgroundImage": "Remove background image",
"removeBackgroundImageConfirmTitle": "Remove background image",
"removeBackgroundImageConfirmBody": "Are you sure you want to remove your conversation background image?",
"newChatsSection": "New Conversations",
"newChatsMuteByDefault": "Mute new chats by default",
"newChatsE2EE": "Enable end-to-end encryption by default. WARNING: Experimental",
"behaviourSection": "Behaviour",
"contactsIntegration": "Contacts integration",
"contactsIntegrationBody": "When enabled, data from the phonebook will be used to provide chat titles and profile pictures. No data will be sent to the server."
},
"debugging": {
"title": "Debugging options",
"generalSection": "General",
"generalEnableDebugging": "Enable debugging",
"generalEncryptionPassword": "Encryption password",
"generalEncryptionPasswordSubtext": "The logs may contain sensitive information so pick a strong passphrase",
"generalLoggingIp": "Logging IP",
"generalLoggingIpSubtext": "The IP the logs should be sent to",
"generalLoggingPort": "Logging Port",
"generalLoggingPortSubtext": "The IP the logs should be sent to"
},
"network": {
"title": "Network",
"automaticDownloadsSection": "Automatic Downloads",
"automaticDownloadsText": "Moxxy will automatically download files on…",
"automaticDownloadsMaximumSize": "Maximum Download Size",
"automaticDownloadsMaximumSizeSubtext": "The maximum file size for a file to be automatically downloaded",
"automaticDownloadAlways": "Always",
"wifi": "Wifi",
"mobileData": "Mobile data"
},
"privacy": {
"title": "Privacy",
"generalSection": "General",
"showContactRequests": "Show contact requests",
"showContactRequestsSubtext": "This will show people who added you to their contact list but sent no message yet",
"profilePictureVisibility": "Make profile picture public",
"profilePictureVisibilitSubtext": "If enabled, everyone can see your profile picture. If disabled, only users on your contact list can see your profile picture.",
"conversationsSection": "Conversation",
"sendChatMarkers": "Send chat markers",
"sendChatMarkersSubtext": "This will tell your conversation partner if you received or read a message",
"sendChatStates": "Send chat states",
"sendChatStatesSubtext": "This will show your conversation partner if you are typing or looking at the chat",
"redirectsSection": "Redirects",
"redirectText": "This will redirect $serviceName links that you tap to a proxy service, e.g. $exampleProxy",
"currentlySelected": "Currently selected: $proxy",
"redirectsTitle": "$serviceName Redirect",
"cannotEnableRedirect": "Cannot enable $serviceName redirects",
"cannotEnableRedirectSubtext": "You must first set a proxy service to redirect to. To do so, tap the field next to the switch.",
"urlEmpty": "URL cannot be empty",
"urlInvalid": "Invalid URL",
"redirectDialogTitle": "$serviceName Redirect",
"stickersPrivacy": "Keep sticker list public",
"stickersPrivacySubtext": "If enabled, everyone will be able to see your list of installed sticker packs."
},
"stickers": {
"title": "Stickers",
"stickerSection": "Sticker",
"displayStickers": "Display stickers in chat",
"autoDownload": "Automatically download stickers",
"autoDownloadBody": "If enabled, stickers are automatically downloaded when the sender is in your contact list.",
"stickerPacksSection": "Sticker packs",
"importStickerPack": "Import sticker pack",
"importSuccess": "Sticker pack successfully imported",
"importFailure": "Failed to import sticker pack",
"stickerPackSize": "(${size})"
},
"stickerPacks": {
"title": "Sticker Packs"
},
"storage": {
"title": "Storage",
"storageUsed": "Storage used: ${size}",
"sizePlaceholder": "Computing…",
"storageManagement": "Storage Management",
"removeOldMedia": {
"title": "Remove old media",
"description": "Removes old media files from the device"
},
"removeOldMediaDialog": {
"title": "Delete media files",
"options": {
"all": "All media files",
"oneWeek": "Older than 1 week",
"oneMonth": "Older than 1 month"
},
"delete": "Delete",
"confirmation": {
"body": "Are you sure you want to delete old media files?"
}
},
"viewMediaFiles": "View media files",
"mediaFiles": "Media Files",
"types": {
"media": "Media",
"stickers": "Stickers"
},
"manageStickers": "Manage sticker packs"
}
}
}
}

View File

@@ -0,0 +1,435 @@
{
"language": "Français",
"global": {
"yes": "Oui",
"no": "Non",
"title": "Moxxy",
"dialogAccept": "OK",
"dialogCancel": "Annuler",
"moxxySubtitle": "Une expérience de construire un facile, moderne et belle client XMPP."
},
"notifications": {
"permanent": {
"ready": "Prêt pour recevoir des messages",
"disconnect": "Déconnecté",
"error": "Erreur",
"connecting": "Connexion…",
"idle": "Inactif"
},
"message": {
"markAsRead": "Marquer comme lu",
"reply": "Répondre"
},
"channels": {
"messagesChannelName": "Messages",
"warningChannelName": "Alertes",
"warningChannelDescription": "Alertes liées à Moxxy",
"messagesChannelDescription": "La voie des notifications pour des messages reçu"
},
"errors": {
"messageError": {
"body": "N'est pas parvenu à livrée la message au ${conversationTitle}",
"title": "Échec au livraison du message"
}
},
"titles": {
"error": "Erreur"
}
},
"messages": {
"file": "Fichier",
"image": "Image",
"video": "Vidéo",
"audio": "Audio",
"sticker": "Autocollant",
"retracted": "La message a été retirée",
"you": "Toi",
"retractedFallback": "Un précédent message a été retiré, mais votre client ne le prend pas en charge"
},
"permissions": {
"allow": "Autoriser",
"skip": "Sauter",
"requests": {
"notification": {
"reason": "Pour notifier des nouvelles messages, Moxxy à besoin de permission pour afficher les notifications."
},
"batterySaving": {
"reason": "Pour recevoir des messages dans l'arrière-plan, Moxxy devrait être exempté de la fonction d'économie de la batterie d'Android."
}
}
},
"dateTime": {
"justNow": "A l'instant",
"nMinutesAgo": "Il y a ${min}min",
"mondayAbbrev": "Lun",
"tuesdayAbbrev": "Mar",
"wednessdayAbbrev": "Mer",
"thursdayAbbrev": "Jeu",
"fridayAbbrev": "Ven",
"saturdayAbbrev": "Sam",
"sundayAbbrev": "Dim",
"january": "Janvier",
"february": "Février",
"march": "Mars",
"april": "Avril",
"may": "Mai",
"june": "Juin",
"july": "Juillet",
"august": "Août",
"september": "Septembre",
"october": "Octobre",
"november": "Novembre",
"december": "Décembre",
"today": "Aujourdhui",
"yesterday": "Hier"
},
"errors": {
"general": {
"noInternet": "N'est pas connecté au l'Internet."
},
"omemo": {
"notEncryptedForDevice": "Cet message n'était pas chiffré pour cette appareil",
"invalidHmac": "Ne pouvait pas déchiffrer la message",
"noDecryptionKey": "Aucune clé de déchiffrer disponible",
"messageInvalidAfixElement": "Message chiffré invalide",
"verificationWrongJid": "Mauvais adresse XMPP",
"verificationWrongDevice": "Mauvais appareil OMEMO:2",
"verificationNotInList": "Mauvais appareil OMEMO:2",
"verificationWrongFingerprint": "Empreinte OMEMO:2 invalide",
"verificationInvalidOmemoUrl": "Empreinte OMEMO:2 invalide",
"couldNotPublish": "Ne pouvait pas publier l'identité cryptographique au la serveur. Cela signifie que chiffrement de bout en bout risque de ne pas fonctionner."
},
"filePicker": {
"permissionDenied": "L'autorisation de permission de storage a été déclinée."
},
"login": {
"saslFailed": "Identifiants non-valides",
"startTlsFailed": "Impossible d'établir une connexion sécurisée",
"unspecified": "Erreur non-connue",
"noConnection": "Impossible d'établir une connexion"
},
"message": {
"unspecified": "Erreur inconnue",
"fileUploadFailed": "Le téléversement du fichier a échoué",
"contactDoesntSupportOmemo": "Le chiffrement OMEMO:2 n'est pas pris en charge par l'autre personne",
"serviceUnavailable": "Le message n'a pas pu être délivré",
"failedToEncrypt": "Le message n'a pas pu être chiffré",
"failedToEncryptFile": "Le fichier n'a pas pu être chiffré",
"failedToDecryptFile": "Le fichier n'a pas pu être déchiffré",
"fileNotEncrypted": "La conversation est chiffrée, mais pas le fichier",
"fileDownloadFailed": "Le téléchargement du fichier a échoué",
"remoteServerTimeout": "Le message n'a pas pu être délivré au serveur de destination",
"remoteServerNotFound": "Serveur introuvable; Le message n'a pas pu être délivré"
},
"connection": {
"connectionTimeout": "Impossible de joindre le serveur",
"saslAccountDisabled": "Votre compte est désactivé",
"saslInvalidCredentials": "Vos identifiants ne sont pas valides",
"unrecoverable": "Connexion perdue due à une erreur"
},
"conversation": {
"audioRecordingError": "Erreur d'enregistrement audio",
"openFileNoAppError": "Pas d'application pour ce type de fichier",
"messageErrorDialogTitle": "Erreur",
"openFileGenericError": "Impossible d'ouvrir le fichier"
},
"newChat": {
"remoteServerError": "Impossible de contacter le serveur distant.",
"groupchatUnsupported": "Les conversations de groupes ne sont pas encore prises en charge.",
"unknown": "Erreur inconnue."
}
},
"pages": {
"settings": {
"stickers": {
"title": "Autocollants",
"stickerSection": "Autocollant",
"stickerPacksSection": "Paquets d'autocollants",
"stickerPackSize": "(${size})",
"displayStickers": "Afficher les autocollants dans les conversations",
"autoDownload": "Téléchargement automatique des autocollants",
"importStickerPack": "Importer série d'autocollants",
"importSuccess": "Série d'autocollants importée avec succès",
"importFailure": "Erreur lors de l'import de la série d'autocollants",
"autoDownloadBody": "Si activée, les autocollants seront téléchargés automatiquement avec les personnes de votre liste de contact."
},
"stickerPacks": {
"title": "Paquets d'autocollants"
},
"storage": {
"types": {
"stickers": "Autocollants",
"media": "Médias"
},
"title": "Stockage",
"storageUsed": "Espace utilisé: ${size}",
"storageManagement": "Gestion de stockage",
"removeOldMedia": {
"title": "Supprimer les anciens médias",
"description": "Supprimer les anciens fichiers médias de votre appareil"
},
"removeOldMediaDialog": {
"title": "Supprimer les fichiers médias",
"options": {
"oneWeek": "Plus anciens qu'une semaine",
"oneMonth": "Plus anciens qu'un mois",
"all": "Tous les fichiers médias"
},
"delete": "Supprimer",
"confirmation": {
"body": "Confirmer la suppresion des anciens fichiers médias ?"
}
},
"viewMediaFiles": "Voir les fichiers médias",
"mediaFiles": "Fichiers médias",
"manageStickers": "Gestion des autocollants",
"sizePlaceholder": "Calcul…"
},
"settings": {
"title": "Paramètres",
"conversationsSection": "Conversations",
"accountSection": "Compte",
"signOut": "Déconnexion",
"signOutConfirmBody": "Confirmer la déconnexion ?",
"miscellaneousSection": "Autres",
"debuggingSection": "Développement",
"general": "Général",
"signOutConfirmTitle": "Déconnexion"
},
"about": {
"title": "À propos",
"licensed": "Sous licence GPLv3",
"version": "Version ${version}",
"viewSourceCode": "Voir le code source",
"debugMenuShown": "Mode développement disponible !",
"debugMenuAlreadyShown": "Le mode développement est déjà disponible !",
"nMoreToGo": "Plus que ${n}…"
},
"appearance": {
"title": "Apparence",
"languageSection": "Langue",
"language": "Langue de l'application",
"systemLanguage": "Langue par défaut",
"languageSubtext": "Langue actuelle: $selectedLanguage"
},
"licenses": {
"title": "Licences Open-Source",
"licensedUnder": "Sous licence $license"
},
"conversation": {
"title": "Conversation",
"appearance": "Apparence",
"selectBackgroundImage": "Sélectionner l'image d'arrière-plan",
"removeBackgroundImage": "Supprimer l'image d'arrière-plan",
"removeBackgroundImageConfirmTitle": "Suppression de l'image d'arrière-plan",
"removeBackgroundImageConfirmBody": "Confirmer la suppression de l'image d'arrière-plan de conversation ?",
"newChatsSection": "Nouvelles conversations",
"newChatsMuteByDefault": "Mettre les nouvelles conversation en sourdine par défaut",
"behaviourSection": "Comportement",
"contactsIntegration": "Intégration de contacts",
"selectBackgroundImageDescription": "Cette image sera en arrière-plan de toutes vos conversations",
"newChatsE2EE": "Activer le chiffrement de bout en bout par défaut. ATTENTION: Expérimental",
"contactsIntegrationBody": "Lorsque activé, les données de votre appareil fourniront les titres de conversation, et images de profils. Aucune donnée n'est envoyée au serveur."
},
"debugging": {
"title": "Option de développement",
"generalSection": "Général",
"generalEnableDebugging": "Activer le mode développement",
"generalEncryptionPassword": "Mot de passe de chiffrement",
"generalEncryptionPasswordSubtext": "Les journaux contiennent des informations sensibles, choisissez un mot de passe fort",
"generalLoggingIp": "IP de journaux",
"generalLoggingIpSubtext": "L'IP à laquelle envoyer les journaux",
"generalLoggingPort": "Port de journaux",
"generalLoggingPortSubtext": "L'IP où envoyer les journaux"
},
"network": {
"title": "Réseau",
"automaticDownloadsSection": "Téléchargements automatiques",
"automaticDownloadsText": "Moxxy téléchargera automatiquement les fichiers sur…",
"automaticDownloadsMaximumSize": "Taille maximale de téléchargement",
"automaticDownloadAlways": "Toujours",
"wifi": "WiFi",
"mobileData": "Données mobiles",
"automaticDownloadsMaximumSizeSubtext": "La taille maximale des fichiers à télécharger automatiquement"
},
"privacy": {
"title": "Vie privée",
"generalSection": "Général",
"showContactRequests": "Afficher les requêtes de contact",
"showContactRequestsSubtext": "Cela affiche les personnes qui vous ont ajouté à leurs listes de contacts mais ne vous ont pas encore envoyé de message",
"profilePictureVisibility": "Rendre publique l'image de profil",
"profilePictureVisibilitSubtext": "Lorsque activée, tout le monde peut voir votre image de profil. Désactivée, seulement vos contacts peuvent la voir.",
"conversationsSection": "Conversation",
"sendChatMarkers": "Envoyer des marqueurs de conversation",
"sendChatStates": "Envoyer l'état de conversation",
"sendChatStatesSubtext": "Informe votre destinataire si vous être en train d'écrire, ou lire la conversation",
"sendChatMarkersSubtext": "Informe votre destinataire lorsqu'un message est reçu, ou lu",
"redirectsSection": "Redirections",
"redirectText": "Redirige les liens $serviceName vers un service de proxy, comme $exampleProxy",
"currentlySelected": "Service sélectionné: $proxy",
"redirectsTitle": "$serviceName Redirection",
"cannotEnableRedirect": "Impossible d'activer les redirections $serviceName",
"urlEmpty": "URL vide",
"urlInvalid": "URL non-valide",
"redirectDialogTitle": "$serviceName Redirect",
"stickersPrivacy": "Rendre publique la liste d'autoocollants",
"stickersPrivacySubtext": "Si activée, tout le monde pourra voir vos autocollants installés.",
"cannotEnableRedirectSubtext": "Service de redirection manquant, appuyer sur le champ à côté de l'interrupteur"
}
},
"conversation": {
"stickerSettings": "Paramètres des autocollants",
"unencrypted": "Non-chiffré",
"encrypted": "Chiffré",
"closeChat": "Fermer la conversation",
"closeChatConfirmTitle": "Fermeture de conversation",
"blockShort": "Bloquer",
"blockUser": "Bloquer cette personne",
"online": "En ligne",
"retract": "Supprimer le message",
"forward": "Transférer",
"edit": "Modifier",
"quote": "Citer",
"copy": "Copier le contenu",
"messageCopied": "Message copié vers le presse-papier",
"addReaction": "Ajouter une réaction",
"showError": "Afficher l'erreur",
"showWarning": "Afficher l'avertissement",
"warning": "Avertissement",
"addToContacts": "Ajouter aux contacts",
"addToContactsBody": "Confirmer l'ajout de ${jid} aux contacts ?",
"stickerPickerNoStickersLine1": "Pas d'autocollants installés.",
"newDeviceMessage": {
"one": "Un nouvel appareil a été ajouté",
"other": "De nouveaux appareils ont été ajoutés"
},
"replacedDeviceMessage": {
"one": "Un appareil a été modifié",
"other": "Plusieurs appareils ont été ajoutés"
},
"sendImages": "Envoyer des images",
"sendFiles": "Envoyer des fichiers",
"takePhotos": "Prendre des photos",
"messageHint": "Envoyer un message…",
"closeChatConfirmSubtext": "Confirmer la fermeture de cette conversation ?",
"retractBody": "Confirmer la demande de suppression du message ? Cela n'est qu'une demande que le client n'a pas a honorer.",
"addToContactsTitle": "Ajouter ${jid} aux contacts",
"stickerPickerNoStickersLine2": "Des autocollants sont disponibles à l'installation dans les paramètres."
},
"intro": {
"noAccount": "Pas de compte XMPP ? Aucun soucis, en créer un est très simple.",
"loginButton": "Connexion",
"registerButton": "Inscription"
},
"login": {
"title": "Connexion",
"xmppAddress": "Adresse XMPP",
"password": "Mot de passe",
"advancedOptions": "Paramètres avancés",
"createAccount": "Créer un compte sur le serveur"
},
"conversations": {
"speeddialNewChat": "Nouvelle conversation",
"speeddialJoinGroupchat": "Rejoindre une conversation de groupe",
"speeddialAddNoteToSelf": "Note personnelle",
"overlaySettings": "Paramètres",
"noOpenChats": "Aucune conversation d'ouverte",
"startChat": "Démarrer une conversation",
"closeChatBody": "Souhaitez-vous fermer la conversation avec ${conversationTitle} ?",
"markAsRead": "Marquer comme lue",
"closeChat": "Fermer une conversation"
},
"startchat": {
"title": "Nouvelle conversation",
"xmppAddress": "Adresse XMPP",
"buttonAddToContact": "Démarrer une nouvelle conversation",
"subtitle": "Vous pouvez démarrer une conversation à l'aide d'une adresse XMPP ou en scannant un code QR."
},
"newconversation": {
"title": "Nouvelle conversation",
"startChat": "Démarrer une nouvelle conversation",
"createGroupchat": "Nouvelle conversation de groupe",
"joinGroupChat": "Rejoindre une conversation de groupe",
"nullNickname": "Le pseudonyme ne peut pas être vide !",
"nick": "Pseudonyme",
"enterNickname": "Entrer un pseudonyme",
"nicknameSubtitle": "Un pseudonyme unique est requis pour rejoindre une CUM."
},
"crop": {
"setProfilePicture": "Utiliser comme image de profil"
},
"shareselection": {
"shareWith": "Partager avec…",
"confirmTitle": "Envoyer le fichier",
"confirmBody": "Une ou plusieurs conversations ne sont pas chiffrées. Le fichier sera accessible au serveur. Confirmer ?"
},
"profile": {
"general": {
"omemo": "Sécurité",
"profile": "Profil",
"media": "Média"
},
"conversation": {
"notifications": "Notifications",
"notificationsMuted": "En sourdine",
"notificationsEnabled": "Activées",
"sharedMedia": "Média"
},
"owndevices": {
"title": "Appareils disponibles",
"otherDevices": "Autres appareils",
"thisDevice": "Cet appareil",
"deleteDeviceConfirmTitle": "Supprimer appareil",
"recreateOwnSessions": "Recréer sessions",
"recreateOwnSessionsConfirmTitle": "Recréer ses propres sessions ?",
"recreateOwnDevice": "Recréer appareil",
"recreateOwnDeviceConfirmTitle": "Récréer cet appareil ?",
"deleteDeviceConfirmBody": "Vos contact ne pourront pas chiffrer pour cet appareil, continuer ?",
"recreateOwnSessionsConfirmBody": "Cela recréera les sessions cryptographiques de vos appareils. À n'utiliser qu'en cas d'erreur de déchiffrement.",
"recreateOwnDeviceConfirmBody": "Cela récréera l'identité cryptographique de cet appareil. Cette opération peut prendre du temps. Les contact ayant vérifié cet appareils auront à le faire de nouveau, continuer ?"
},
"devices": {
"title": "Sécurité",
"recreateSessions": "Recréer sessions",
"recreateSessionsConfirmTitle": "Recréer les sessions ?",
"noSessions": "Il n'y a pas de sessions cryptographiques utilisées pour du chiffrement de bout en bout.",
"recreateSessionsConfirmBody": "Cela recréera les sessions cryptographiques de vos appareils. À n'utiliser qu'en cas d'erreur de déchiffrement sur ceux-ci."
}
},
"blocklist": {
"title": "Liste de blocage",
"noUsersBlocked": "Aucune personne bloquée",
"unblockAll": "Tout débloquer",
"unblockAllConfirmTitle": "Confirmer ?",
"unblockAllConfirmBody": "Confirmer le débloquage de toutes les personnes ?",
"unblockJidConfirmTitle": "Débloquer ${jid} ?",
"unblockJidConfirmBody": "Confirmer le débloquage de ${jid} ? Vous recevrez de nouveau les messages de sa part."
},
"cropbackground": {
"blur": "Flouter l'arrière-plan",
"setAsBackground": "Utiliser comme arrière-plan"
},
"stickerPack": {
"removeConfirmTitle": "Supprimer autocollant",
"removeConfirmBody": "Confirmer la suppression de cette série d'autocollants ?",
"installConfirmTitle": "Installer autocollant",
"restricted": "Cette série est limitée. Les autocollants s'afficheront mais ne pourront être envoyés.",
"fetchingFailure": "Autocollants introuvables",
"installConfirmBody": "Confirmer l'installation de cette série d'autocollants ?"
},
"sharedMedia": {
"empty": {
"chat": "Pas de média partagé dans cette conversation",
"general": "Pas de fichier média disponible"
}
}
},
"warnings": {
"conversation": {
"holdForLonger": "Maintenez le bouton pressé pour enregistrer un message vocal"
},
"message": {
"integrityCheckFailed": "Impossible de vérifier l'intégrité du fichier"
}
}
}

View File

@@ -0,0 +1,463 @@
{
"dateTime": {
"justNow": "Neste momento",
"nMinutesAgo": "fai ${min}min",
"mondayAbbrev": "Lun",
"tuesdayAbbrev": "Mar",
"wednessdayAbbrev": "Mér",
"thursdayAbbrev": "Xov",
"fridayAbbrev": "Ven",
"saturdayAbbrev": "Sáb",
"sundayAbbrev": "Dom",
"january": "Xaneiro",
"february": "Febreiro",
"march": "Marzo",
"april": "Abril",
"may": "Maio",
"june": "Xuño",
"july": "Xullo",
"august": "Agosto",
"september": "Setembro",
"october": "Outubro",
"november": "Novembro",
"december": "Decembro",
"today": "Hoxe",
"yesterday": "Onte"
},
"messages": {
"image": "Imaxe",
"video": "Vídeo",
"audio": "Audio",
"file": "Ficheiro",
"sticker": "Adhesivo",
"retracted": "A mensaxe foi editada",
"you": "Ti",
"retractedFallback": "Unha mensaxe anterior foi editada pero o teu cliente non soporta esa función"
},
"errors": {
"general": {
"noInternet": "Sen conexión a Internet."
},
"filePicker": {
"permissionDenied": "Non se concedeu permiso de acceso á almacenaxe."
},
"omemo": {
"couldNotPublish": "Non se puido publicar a identidade criptográfica no servidor. Debido a isto a cifraxe de extremo-a-extremo podería non funcionar.",
"invalidHmac": "Non se descifrou a mensaxe",
"noDecryptionKey": "Non se dispón de chave de descifrado",
"messageInvalidAfixElement": "Mensaxe cifrada non válida",
"verificationInvalidOmemoUrl": "Impresión dixital OMEMO:2 non válida",
"verificationWrongJid": "Enderezo XMPP incorrecto",
"verificationWrongDevice": "Dispositivo OMEMO:2 incorrecto",
"verificationNotInList": "Dispositivo OMEMO:2 incorrecto",
"verificationWrongFingerprint": "Impresión dixital OMEMO:2 incorrecta",
"notEncryptedForDevice": "Non se cifrou a mensaxe para este dispositivo"
},
"connection": {
"connectionTimeout": "Non hai conexión co servidor",
"saslAccountDisabled": "A conta está desactivada",
"saslInvalidCredentials": "As credenciais da conta non son válidas",
"unrecoverable": "Perdeuse a conexión debido a un erro irremediable"
},
"login": {
"saslFailed": "Credenciais de acceso incorrectas",
"startTlsFailed": "Non se puido establecer unha conexión segura",
"noConnection": "Fallou o establecemento da conexión",
"unspecified": "Erro inconcreto"
},
"message": {
"unspecified": "Erro descoñecido",
"fileUploadFailed": "Fallou a subida do ficheiro",
"contactDoesntSupportOmemo": "O contacto non ten soporte para a cifraxe usando OMEMO:2",
"fileDownloadFailed": "Fallou a descarga do ficheiro",
"serviceUnavailable": "Non se puido entregar a mensaxe ao contacto",
"remoteServerTimeout": "A mensaxe non se puido entregar ao servidor do contacto",
"remoteServerNotFound": "A mensaxe non se puido entregar ao servidor do contacto e non sabemos onde está",
"failedToEncrypt": "Non se puido cifrar a mensaxe",
"failedToEncryptFile": "Non se puido cifrar o ficheiro",
"failedToDecryptFile": "Non se puido descifrar o ficheiro",
"fileNotEncrypted": "A conversa está cifrada pero o ficheiro non está cifrado"
},
"conversation": {
"audioRecordingError": "Fallou a finalización da gravación de audio",
"openFileNoAppError": "Non se atopa unha app para abrir o ficheiro",
"openFileGenericError": "Non se puido abrir o ficheiro",
"messageErrorDialogTitle": "Erro"
},
"newChat": {
"remoteServerError": "Non se puido contactar co servidor remoto.",
"groupchatUnsupported": "Por agora non hai soporte para unirse a conversas en grupo.",
"unknown": "Erro descoñecido."
}
},
"warnings": {
"message": {
"integrityCheckFailed": "Non se puido verificar a integridade do ficheiro"
},
"conversation": {
"holdForLonger": "Mantén premido o botón para gravar a mensaxe de voz",
"microphoneDenied": "Denegado acceso ao micrófono"
}
},
"pages": {
"intro": {
"noAccount": "Non tes unha conta XMPP? Sen problema, crear unha é doado.",
"loginButton": "Acceder",
"registerButton": "Crear conta"
},
"login": {
"title": "Acceder",
"xmppAddress": "Enderezo-XMPP",
"password": "Contrasinal",
"advancedOptions": "Opcións avanzadas",
"createAccount": "Crear conta no servidor",
"login": "Acceder"
},
"conversations": {
"speeddialNewChat": "Nova conversa",
"speeddialJoinGroupchat": "Entrar na conversa en grupo",
"speeddialAddNoteToSelf": "Notas propias",
"overlaySettings": "Axustes",
"noOpenChats": "Non tes conversas abertas",
"startChat": "Inicia unha conversa",
"closeChat": "Pecha a conversa",
"markAsRead": "Marcar como lido",
"closeChatBody": "Tes a certeza de querer pechar a conversa con ${conversationTitle}?"
},
"conversation": {
"unencrypted": "Sen cifrar",
"encrypted": "Cifrada",
"closeChat": "Pechar conversa",
"closeChatConfirmTitle": "Pechar conversa",
"closeChatConfirmSubtext": "Tes a certeza de querer pechar esta conversa?",
"blockShort": "Bloquear",
"blockUser": "Bloquear conta",
"online": "En liña",
"retract": "Editar mensaxe",
"retractBody": "Tes a certeza de querer editar a mensaxe? Ten en conta que esta só é unha solicitude e que o cliente non ten porque facerlle caso.",
"forward": "Reenviar",
"edit": "Editar",
"quote": "Citar",
"showWarning": "Mostrar aviso",
"warning": "Aviso",
"addToContacts": "Engadir aos contactos",
"addToContactsTitle": "Engadir a ${jid} aos contactos",
"addToContactsBody": "Tes a certeza de querer engadir a ${jid} aos teus contactos?",
"stickerPickerNoStickersLine1": "Non tes paquetes de adhesivos instalados.",
"stickerPickerNoStickersLine2": "Pódelos instalar a través dos axustes dos adhesivos.",
"stickerSettings": "Axustes dos adhesivos",
"newDeviceMessage": {
"one": "Engadiuse un novo dispositivo",
"other": "Engadíronse varios dispositivos novos"
},
"replacedDeviceMessage": {
"one": "Un dos dispositivos cambiou",
"other": "Engadíronse varios dispositivos"
},
"messageHint": "Enviar unha mensaxe…",
"sendImages": "Enviar imaxes",
"sendFiles": "Enviar ficheiros",
"takePhotos": "Facer fotos",
"copy": "Copiar contido",
"messageCopied": "Copiouse o contido da mensaxe no portapapeis",
"addReaction": "Engadir reacción",
"showError": "Mostar erro",
"voiceRecording": {
"dragToCancel": "Arrastra para cancelar",
"leaveConfirmation": {
"negative": "Cancelar",
"title": "Desbotar a gravación",
"body": "Estás a gravar unha mensaxe de voz. Queres desbotar a gravación?",
"affirmative": "Desbotar"
},
"cancel": "Cancelar"
},
"sendMedia": "Enviar multimedia",
"share": "Compartir"
},
"startchat": {
"title": "Nova Conversa",
"xmppAddress": "Enderezo XMPP",
"subtitle": "Podes iniciar unha nova conversa escribindo un enderezo XMPP ou escaneando o seu código QR.",
"buttonAddToContact": "Iniciar nova conversa"
},
"newconversation": {
"title": "Nova conversa",
"startChat": "Iniciar nova conversa",
"createGroupchat": "Nova conversa en grupo",
"nullNickname": "O alcume non pode estar baleiro!",
"nick": "Alcume",
"nicknameSubtitle": "Tes que escribir un alcume único para poder unirte á MUC.",
"joinGroupChat": "Unirse a conversa en grupo",
"enterNickname": "Escribe un alcume"
},
"crop": {
"setProfilePicture": "Establecer imaxe de perfil"
},
"shareselection": {
"shareWith": "Compartir con…",
"confirmTitle": "Enviar ficheiro",
"confirmBody": "Unha ou varias conversas non están cifradas. Isto significa que o ficheiro podería ser visto no servidor. Desexas continuar?"
},
"profile": {
"general": {
"omemo": "Seguridade",
"profile": "Perfil",
"media": "Multimedia"
},
"conversation": {
"notifications": "Notificacións",
"notificationsMuted": "Acalada",
"notificationsEnabled": "Activada",
"sharedMedia": "Multimedia"
},
"owndevices": {
"title": "Dispositivos propios",
"thisDevice": "Este dispositivo",
"otherDevices": "Outros dispositivos",
"deleteDeviceConfirmTitle": "Eliminar dispositivo",
"deleteDeviceConfirmBody": "Isto significa que os contactos non poderán cifrar as mensaxes para ese dispositivo. Continuar?",
"recreateOwnSessions": "Reconstruír sesións",
"recreateOwnSessionsConfirmTitle": "Recrear as sesións propias?",
"recreateOwnSessionsConfirmBody": "Isto volverá a crear as sesións criptográficas dos teus dispositivos. Fai isto só se os teus dispositivos mostran erros ao descrifrar.",
"recreateOwnDevice": "Recrear dispositivo",
"recreateOwnDeviceConfirmTitle": "Recrear o dispositivo propio?",
"recreateOwnDeviceConfirmBody": "Isto recreará a identidade criptográfica deste dispositivo? Podería levarlle un anaco. Se os contactos verificaron este dispositivo, terán que facelo outra vez. Continuar?"
},
"devices": {
"title": "Seguridade",
"recreateSessions": "Reconstruír sesións",
"recreateSessionsConfirmTitle": "Reconstruír sesións?",
"recreateSessionsConfirmBody": "Isto volverá a crear as sesións criptográficas dos teus dispositivos. Usa isto só se os teus dispositivos mostran erros ao descifrar.",
"noSessions": "Non hai sesións criptográficas en uso para a cifraxe de extremo-a-extremo."
},
"serverInfo": {
"title": "Información do servidor"
}
},
"sharedMedia": {
"empty": {
"general": "Non hai dispoñibles ficheiros multimedia",
"chat": "Non hai multimedia compartido nesta conversa"
}
},
"settings": {
"about": {
"debugMenuAlreadyShown": "Xa es desenvolvedora!",
"title": "Acerca de",
"licensed": "Baixo licenza GPL3",
"version": "Versión ${version}",
"viewSourceCode": "Ver código fonte",
"nMoreToGo": "${n} máis para rematar…",
"debugMenuShown": "Agora xa es desenvolvedora!"
},
"settings": {
"title": "Axustes",
"conversationsSection": "Conversas",
"accountSection": "Conta",
"signOut": "Pechar sesión",
"signOutConfirmTitle": "Pechar Sesión",
"signOutConfirmBody": "Vas pechar a sesión. Continuar?",
"miscellaneousSection": "Varios",
"debuggingSection": "Depuración",
"general": "Xeral"
},
"appearance": {
"title": "Aparencia",
"languageSection": "Idioma",
"language": "Idioma da app",
"languageSubtext": "Idioma actual: $selectedLanguage",
"systemLanguage": "Idioma por defecto"
},
"licenses": {
"title": "Licenzas Open-Source",
"licensedUnder": "Baixo licenza $license"
},
"conversation": {
"title": "Conversa",
"appearance": "Aparencia",
"selectBackgroundImage": "Elexir imaxe de fondo",
"selectBackgroundImageDescription": "Esta será a imaxe de fondo en todas as túas conversas",
"removeBackgroundImage": "Retirar imaxe de fondo",
"removeBackgroundImageConfirmTitle": "Retirar imaxe de fondo",
"removeBackgroundImageConfirmBody": "Tes a certeza de querer eliminar a imaxe de fondo da conversa?",
"newChatsSection": "Novas Conversas",
"newChatsMuteByDefault": "Por defecto acalar as novas conversas",
"newChatsE2EE": "Por defecto activar a cifraxe de extremo-a-extremo. AVISO: Experimental",
"behaviourSection": "Comportamento",
"contactsIntegration": "Integración dos contactos",
"contactsIntegrationBody": "Ao activala, os datos da libreta de enderezos usaranse para proporcionar título ás conversas e imaxes de perfil. Non se transmite información ao servidor."
},
"debugging": {
"title": "Opción de depuración",
"generalSection": "Xeral",
"generalEnableDebugging": "Activar depuración",
"generalEncryptionPassword": "Contrasinal de cifraxe",
"generalLoggingIp": "IP de rexistro",
"generalLoggingIpSubtext": "O IP ao que se deben enviar os rexistros",
"generalLoggingPort": "Porto de rexistro",
"generalLoggingPortSubtext": "O IP ao que se deben enviar os rexistros",
"generalEncryptionPasswordSubtext": "Os rexistros poderían conter información sensible así que mellor elixe un contrasinal forte"
},
"network": {
"title": "Rede",
"automaticDownloadsSection": "Descargas automáticas",
"automaticDownloadsText": "Moxxy descargará automáticamente os ficheiros en…",
"automaticDownloadsMaximumSize": "Tamaño máximo de descarga",
"automaticDownloadsMaximumSizeSubtext": "O tamaño máximo dos ficheiros a descargar de xeito automático",
"automaticDownloadAlways": "Sempre",
"wifi": "Wifi",
"mobileData": "Datos móbiles"
},
"privacy": {
"profilePictureVisibility": "Facer pública a foto de perfil",
"profilePictureVisibilitSubtext": "Se o activas, calquera poderá ver a túa foto de perfil. Se o desactivas, só as persoas da túa lista de contactos poderán ver a foto de perfil.",
"conversationsSection": "Conversa",
"sendChatMarkers": "Enviar marcadores de conversa",
"title": "Privacidade",
"generalSection": "Xeral",
"sendChatMarkersSubtext": "Vai indicar aos teus correspondentes se recibiches ou liches a mensaxe",
"showContactRequests": "Mostrar solicitudes de contacto",
"showContactRequestsSubtext": "Mostrarache as persoas que te engadiron á súa lista de contactos pero que aínda non che enviaron mensaxes",
"sendChatStates": "Enviar estados da conversa",
"sendChatStatesSubtext": "Vai indicar aos teus correspondentes se estás escribindo ou lendo na conversa",
"redirectsSection": "Reenvíos",
"redirectsTitle": "Redirixir $serviceName",
"cannotEnableRedirect": "Non se pode activar a redirección $serviceName",
"cannotEnableRedirectSubtext": "Primeiro tes que establecer o servizo proxy ao que queres redirixir. Para facelo toca no campo a carón do botón.",
"urlEmpty": "O URL non pode estar baleiro",
"urlInvalid": "URL non válido",
"redirectDialogTitle": "Redirixir $serviceName",
"stickersPrivacy": "Manter como pública a lista de adhesivos",
"stickersPrivacySubtext": "Se o activas, calquera poderá ver a túa lista de paquetes de adhesivos instalados.",
"redirectText": "Redireccionará as ligazóns a $serviceName nas que premas cara o servizo proxy, ex. $exampleProxy",
"currentlySelected": "Seleccionado actualmente: $proxy"
},
"stickers": {
"title": "Adhesivos",
"stickerSection": "Adhesivo",
"displayStickers": "Mostar adhesivos na conversa",
"autoDownload": "Descargar automaticamente adhesivos",
"autoDownloadBody": "Se o activas, descargarás automaticamente os adhesivos cando o remitente estea na túa lista de contactos.",
"stickerPacksSection": "Paquetes de adhesivos",
"importStickerPack": "Importar paquete de adhesivos",
"importSuccess": "Paquete de adhesivos importado correctamente",
"importFailure": "Fallou a importación do paquete de adhesivos",
"stickerPackSize": "(${size})"
},
"stickerPacks": {
"title": "Paquetes de Adhesivos"
},
"storage": {
"title": "Almacenaxe",
"storageUsed": "Almacenaxe utilizada: ${size}",
"sizePlaceholder": "Calculando…",
"storageManagement": "Xestión da almacenaxe",
"removeOldMedia": {
"title": "Eliminar multimedia antigo",
"description": "Elimina ficheiros multimedia antigos do dispositivo"
},
"removeOldMediaDialog": {
"title": "Eliminar ficheiros multimedia",
"options": {
"all": "Todos os ficheiros multimedia",
"oneWeek": "Anteriores a 1 semana",
"oneMonth": "Anteriores a 1 mes"
},
"delete": "Eliminar",
"confirmation": {
"body": "Tes a certeza de querer eliminar os ficheiros multimedia antigos?"
}
},
"viewMediaFiles": "Ver ficheiros multimedia",
"mediaFiles": "Ficheiros multimedia",
"types": {
"media": "Multimedia",
"stickers": "Adhesivos"
},
"manageStickers": "Xestión dos paquetes de adhesivos"
}
},
"blocklist": {
"title": "Lista de bloqueo",
"noUsersBlocked": "Non tes contas bloqueadas",
"unblockAll": "Desbloquear todo",
"unblockAllConfirmTitle": "Tes certeza?",
"unblockAllConfirmBody": "Tes a certeza de querer desbloquear todas as contas?",
"unblockJidConfirmTitle": "Desbloquear a ${jid}?",
"unblockJidConfirmBody": "Tes a certeza de querer desbloquear a ${jid}? Volverás a recibir mensaxes desde esta conta."
},
"cropbackground": {
"blur": "Desenfocar o fondo",
"setAsBackground": "Establecer como imaxe de fondo"
},
"stickerPack": {
"removeConfirmTitle": "Eliminar paquete de adhesivos",
"removeConfirmBody": "Tes a certeza de querer eliminar este paquete de adhesivos?",
"installConfirmTitle": "Instalar paquete de adhesivos",
"installConfirmBody": "Tes a certeza de querer instalar este paquete de adhesivos?",
"restricted": "Este paquete de adhesivos ten restricións. Significa que poden ser vistos pero non enviados.",
"fetchingFailure": "Non se atopa o paquete de adhesivos"
}
},
"global": {
"title": "Moxxy",
"dialogAccept": "Oká",
"dialogCancel": "Cancelar",
"yes": "Si",
"no": "Non",
"moxxySubtitle": "Un experimento para crear un cliente XMPP moderno, bonito e doado de usar."
},
"notifications": {
"permanent": {
"idle": "Detido",
"ready": "Preparado para recibir mensaxes",
"connecting": "Conectando…",
"disconnect": "Desconectado",
"error": "Erro"
},
"message": {
"reply": "Responder",
"markAsRead": "Marcar como lido"
},
"channels": {
"messagesChannelName": "Mensaxes",
"warningChannelName": "Avisos",
"warningChannelDescription": "Avisos en relación a Moxxy",
"messagesChannelDescription": "A canle de notificación para as mensaxes recibidas",
"serviceChannelName": "Servizo en primeiro plano",
"serviceChannelDescription": "Mantén activa a notificación do servizo en primeiro plano"
},
"titles": {
"error": "Erro"
},
"errors": {
"messageError": {
"title": "Fallou a entrega da mensaxe",
"body": "Fallou a entrega da mensaxe a ${conversationTitle}"
}
},
"warnings": {
"blockingError": {
"title": "Erro ao bloquear a usuaria",
"body": "Non se puido bloquear a ${jid} porque o teu servidor non ten soporte para bloqueos."
}
}
},
"language": "Galego",
"permissions": {
"requests": {
"batterySaving": {
"reason": "Para poder recibir mensaxes cando está en segundo plano, Moxxy ten que evitar que Android force o aforro de batería."
},
"notification": {
"reason": "Para poder informar das mensaxes que chegan, Moxxy precisa permiso para mostrar notificacións."
}
},
"allow": "Permitir",
"skip": "Evitar"
},
"emojiPicker": {
"noRecents": "Sen recentes"
}
}

View File

@@ -0,0 +1,172 @@
{
"language": "日本語",
"global": {
"yes": "はい",
"no": "いいえ",
"dialogCancel": "キャンセル",
"title": "Moxxy",
"dialogAccept": "OK"
},
"dateTime": {
"thursdayAbbrev": "木",
"fridayAbbrev": "金",
"saturdayAbbrev": "土",
"january": "1月",
"february": "2月",
"march": "3月",
"may": "5月",
"june": "6月",
"july": "7月",
"september": "9月",
"october": "10月",
"justNow": "ちょうど今",
"nMinutesAgo": "${min}分前",
"mondayAbbrev": "月",
"tuesdayAbbrev": "火",
"wednessdayAbbrev": "水",
"sundayAbbrev": "日",
"april": "4月",
"august": "8月",
"november": "11月",
"december": "12月",
"today": "今日",
"yesterday": "昨日"
},
"messages": {
"audio": "音声",
"you": "自分",
"image": "画像",
"video": "ビデオ",
"file": "ファイル",
"sticker": "スタンプ",
"retracted": "メッセージ取り消された",
"retractedFallback": "前のメッセージを取り消しましたが、このクライエントはサポートしていません"
},
"errors": {
"connection": {
"connectionTimeout": "サーバー接続中にタイムアウトが発生しました",
"saslInvalidCredentials": "ユーザー名またはパスワードが無効",
"saslAccountDisabled": "あなたのアカウントは停止されています",
"unrecoverable": "回復不能のエラーで接続が途絶えました"
},
"login": {
"noConnection": "接続できませんでした",
"startTlsFailed": "接続できませんでした",
"unspecified": "特定できないエラー",
"saslFailed": "無効なログイン証明書です"
},
"message": {
"fileUploadFailed": "アップロードに失敗しました",
"fileDownloadFailed": "ダウンロードに失敗しました",
"serviceUnavailable": "配信に失敗しました",
"unspecified": "不明なエラー",
"failedToDecryptFile": "そのファイルは復号化できません",
"fileNotEncrypted": "チャットは暗号化されますが、ファイルは暗号化されません"
},
"omemo": {
"notEncryptedForDevice": "このデバイス向けにメッセージは暗号化されませんでした",
"couldNotPublish": "サーバに対して暗号化IDを発行できませんでした、端末間暗号通信はできません",
"invalidHmac": "メッセージを復号できませんでした",
"noDecryptionKey": "復号キーがありません",
"messageInvalidAfixElement": "無効な暗号化メッセージです",
"verificationInvalidOmemoUrl": "無効な OMEMO:2 フィンガープリントです",
"verificationWrongJid": "正しくない XMPP アドレスです",
"verificationWrongDevice": "正しくない OMEMO:2 機器です",
"verificationNotInList": "正しくない OMEMO:2 機器です",
"verificationWrongFingerprint": "正しくない OMEMO:2 フィンガープリントです"
},
"conversation": {
"messageErrorDialogTitle": "エラー",
"audioRecordingError": "音声録音の処理に失敗しました",
"openFileNoAppError": "このファイルを開くアプリケーションを発見できませんでした",
"openFileGenericError": "ファイルを開くのに失敗しました"
},
"filePicker": {
"permissionDenied": "書き込み用メディアへの権限がありません"
},
"general": {
"noInternet": "インターネットに接続されていません"
},
"newChat": {
"remoteServerError": "リモートサーバーへの接続に失敗しました",
"groupchatUnsupported": "グループチャットへの参加は現在サポートされていません",
"unknown": "不明なエラー"
}
},
"warnings": {
"conversation": {
"holdForLonger": "長押しすると音声記録できます"
},
"message": {
"integrityCheckFailed": "ファイルの整合性を確認できませんでした"
}
},
"pages": {
"intro": {
"loginButton": "ログイン",
"registerButton": "新規登録",
"noAccount": "XMPPアドレスをお持ちですかXMPPアドレスの作成は簡単です。"
},
"login": {
"title": "ログイン",
"xmppAddress": "XMPPアドレス",
"password": "パスワード",
"advancedOptions": "詳細設定",
"createAccount": "サーバーにアカウントを作成する"
},
"conversations": {
"speeddialJoinGroupchat": "グループチャットに参加",
"speeddialAddNoteToSelf": "自分用メモ",
"speeddialNewChat": "新しいチャット",
"overlaySettings": "設定"
},
"conversation": {
"blockShort": "ブロック",
"blockUser": "ユーザをブロック",
"retract": "メッセージを取り消す",
"retractBody": "本当にメッセージを取り消しますか? これはただのリクエストであり面目や体裁に関わるものではありません",
"forward": "送る",
"edit": "編集する",
"online": "オンライン",
"quote": "引用する",
"copy": "内容をコピー",
"addReaction": "リアクションを追加する",
"showError": "エラーを表示",
"showWarning": "警告を表示",
"warning": "警告",
"messageCopied": "メッセージはクリップボードにコピーされました"
}
},
"notifications": {
"permanent": {
"connecting": "接続中…",
"error": "エラー",
"idle": "待機中",
"ready": "メッセージを受信する準備ができました",
"disconnect": "切断済み"
},
"message": {
"reply": "返信",
"markAsRead": "既読"
},
"channels": {
"warningChannelName": "警告",
"messagesChannelName": "メッセージ",
"messagesChannelDescription": "受信メッセージのためのお知らせチャンネル",
"warningChannelDescription": "Moxxy に関する警告"
},
"titles": {
"error": "エラー"
},
"errors": {
"messageError": {
"title": "メッセージの送信に失敗しました",
"body": "${conversationTitle} に送信したメッセージは届きませんでした"
}
}
},
"permissions": {
"skip": "スキップ",
"allow": "許可する"
}
}

View File

@@ -0,0 +1,463 @@
{
"language": "Nederlands",
"global": {
"title": "Moxxy",
"dialogAccept": "Oké",
"dialogCancel": "Annuleren",
"yes": "Ja",
"no": "Nee",
"moxxySubtitle": "Een xmpp-experiment: het bouwen van een moderne, eenvoudige en mooie client."
},
"notifications": {
"permanent": {
"idle": "Inactief",
"ready": "Klaar om berichten te ontvangen",
"connecting": "Bezig met verbinden…",
"disconnect": "Verbinding verbroken",
"error": "Foutmelding"
},
"message": {
"reply": "Beantwoorden",
"markAsRead": "Markeren als gelezen"
},
"channels": {
"messagesChannelName": "Berichten",
"warningChannelName": "Waarschuwingen",
"warningChannelDescription": "Aan Moxxy gerelateerde waarschuwingen",
"messagesChannelDescription": "Het meldingskanaal voor het ontvangen van berichten",
"serviceChannelName": "Voorgronddienst",
"serviceChannelDescription": "Toont de vaste voorgronddienstmelding"
},
"titles": {
"error": "Foutmelding"
},
"errors": {
"messageError": {
"title": "Bericht afleveren mislukt",
"body": "Her bericht aan ${conversationTitle} kan niet worden afgeleverd"
}
},
"warnings": {
"blockingError": {
"title": "Blokkeren mislukt",
"body": "De gebruiker ${jid} kan niet worden geblokkeerd omdat je server dit niet ondersteunt."
}
}
},
"dateTime": {
"justNow": "Zojuist",
"nMinutesAgo": "${min} min. geleden",
"mondayAbbrev": "ma",
"tuesdayAbbrev": "di",
"wednessdayAbbrev": "woe",
"thursdayAbbrev": "do",
"fridayAbbrev": "vrij",
"saturdayAbbrev": "za",
"sundayAbbrev": "zo",
"january": "januari",
"february": "februari",
"march": "maart",
"april": "april",
"may": "mei",
"june": "juni",
"july": "juli",
"august": "augustus",
"september": "september",
"october": "oktober",
"november": "november",
"december": "december",
"today": "Vandaag",
"yesterday": "Gisteren"
},
"messages": {
"image": "Afbeelding",
"video": "Video",
"audio": "Audio",
"file": "Bestand",
"sticker": "Sticker",
"retracted": "Dit bericht is herroepen",
"retractedFallback": "Er is een eerder bericht herroepen, maar je client heeft hier geen ondersteuning voor",
"you": "Ik"
},
"errors": {
"filePicker": {
"permissionDenied": "Het opslagrecht is geweigerd."
},
"omemo": {
"notEncryptedForDevice": "Dit bericht is niet versleuteld op dit apparaat",
"invalidHmac": "Het bericht kan niet worden ontsleuteld",
"noDecryptionKey": "Er is geen sleutel beschikbaar",
"messageInvalidAfixElement": "Het bericht is ongeldig versleuteld",
"verificationInvalidOmemoUrl": "Ongeldige OMEMO:2-vingerafdruk",
"verificationWrongJid": "Ongeldig xmpp-adres",
"verificationWrongDevice": "Ongeldig OMEMO:2-apparaat",
"verificationNotInList": "Ongeldig OMEMO:2-apparaat",
"verificationWrongFingerprint": "Ongeldige OMEMO:2-vingerafdruk",
"couldNotPublish": "De versleutelde identiteit kan niet worden gepubliceerd op de server. Hierdoor werkt eind-tot-eindversleuteling mogelijk niet."
},
"connection": {
"saslAccountDisabled": "Je account is uitgeschakeld",
"saslInvalidCredentials": "Je inloggegevens zijn ongeldig",
"unrecoverable": "De verbinding is verbroken wegens een onoplosbare fout",
"connectionTimeout": "Er kan geen verbinding worden gemaakt met de server"
},
"login": {
"saslFailed": "De inloggegevens zijn ongeldig",
"noConnection": "Er kan geen verbinding worden opgezet",
"unspecified": "Onbekende foutmelding",
"startTlsFailed": "Er kan geen beveiligde verbinding worden opgezet"
},
"message": {
"unspecified": "Onbekende foutmelding",
"fileUploadFailed": "Het bestand kan niet worden geüpload",
"contactDoesntSupportOmemo": "Deze contactpersoon heeft geen ondersteuning voon OMEMO:2-versleuteling",
"fileDownloadFailed": "Het bestand kan niet worden opgehaald",
"serviceUnavailable": "Het bericht kan niet worden bezorgd",
"remoteServerTimeout": "Het bericht kan niet worden verstuurd naar de server",
"failedToEncrypt": "Het bericht kan niet worden versleuteld",
"failedToEncryptFile": "Het bestand kan niet worden versleuteld",
"failedToDecryptFile": "Het bestand kan niet worden ontsleuteld",
"fileNotEncrypted": "Het gesprek is versleuteld, maar het bestand niet",
"remoteServerNotFound": "Het bericht kan niet worden verstuurd naar de server omdat het niet bestaat"
},
"conversation": {
"audioRecordingError": "De audio-opname kan niet worden afgerond",
"openFileGenericError": "Het bestand kan niet worden geopend",
"messageErrorDialogTitle": "Foutmelding",
"openFileNoAppError": "Er is geen app die dit bestand kan openen"
},
"general": {
"noInternet": "Er is geen internetverbinding."
},
"newChat": {
"groupchatUnsupported": "Deelnemen aan groepsgesprekken wordt momenteel niet ondersteund.",
"unknown": "Onbekende foutmelding.",
"remoteServerError": "Er kan geen verbinding worden gemaakt met de externe server."
}
},
"warnings": {
"message": {
"integrityCheckFailed": "De bestandsintegriteit kan niet worden vastgesteld"
},
"conversation": {
"holdForLonger": "Houd langer ingedrukt om een spraakbericht op te nemen",
"microphoneDenied": "Microfoontoegang geweigerd"
}
},
"pages": {
"intro": {
"loginButton": "Inloggen",
"registerButton": "Registreren",
"noAccount": "Geen xmpp-account? Geen zorgen: je maakt er in een handomdraai een aan."
},
"login": {
"title": "Inloggen",
"xmppAddress": "Xmpp-adres",
"password": "Wachtwoord",
"advancedOptions": "Geavanceerde opties",
"createAccount": "Account aanmaken op server",
"login": "Inloggen"
},
"conversations": {
"speeddialNewChat": "Nieuw gesprek",
"speeddialJoinGroupchat": "Deelnemen aan groepsgesprek",
"speeddialAddNoteToSelf": "Zelfmemo",
"overlaySettings": "Instellingen",
"noOpenChats": "Er zijn geen openstaande gesprekken",
"startChat": "Gesprek starten",
"closeChat": "Gesprek sluiten",
"closeChatBody": "Weet je zeker dat je het gesprek “${conversationTitle}” wilt sluiten?",
"markAsRead": "Markeren als gelezen"
},
"conversation": {
"unencrypted": "Onversleuteld",
"encrypted": "Versleuteld",
"closeChat": "Gesprek sluiten",
"closeChatConfirmSubtext": "Weet je zeker dat je dit gesprek wilt sluiten?",
"blockShort": "Blokkeren",
"blockUser": "Gebruiker blokkeren",
"online": "Online",
"retract": "Bericht herroepen",
"forward": "Doorsturen",
"edit": "Bewerken",
"quote": "Citeren",
"copy": "Inhoud kopiëren",
"addReaction": "Reageren",
"showError": "Foutmelding tonen",
"showWarning": "Waarschuwing tonen",
"addToContacts": "Toevoegen aan contactpersonen",
"addToContactsTitle": "${jid} toevoegen aan contactpersonen",
"addToContactsBody": "Weet je zeker dat je ${jid} wilt toevoegen aan je contactpersonen?",
"stickerPickerNoStickersLine1": "Er zijn geen stickerpakketten beschikbaar.",
"stickerSettings": "Stickerinstellingen",
"newDeviceMessage": {
"one": "Er is een nieuw apparaat toegevoegd",
"other": "Er zijn meerdere nieuwe apparaten toegevoegd"
},
"replacedDeviceMessage": {
"one": "Er is een apparaat gewijzigd",
"other": "Er zijn meerdere apparaten toegevoegd"
},
"messageHint": "Verstuur een bericht…",
"sendImages": "Afbeeldingen versturen",
"sendFiles": "Bestanden versturen",
"closeChatConfirmTitle": "Gesprek sluiten",
"retractBody": "Weet je zeker dat je dit bericht wilt herroepen? Dit is slechts een verzoek aan de client dat niet in acht hoeft te worden genomen.",
"stickerPickerNoStickersLine2": "Installeer pakketten via de stickerinstellingen.",
"takePhotos": "Foto's maken",
"warning": "Waarschuwing",
"messageCopied": "De berichtinhoud is gekopieerd naar het klembord",
"voiceRecording": {
"dragToCancel": "Versleep om te annuleren",
"leaveConfirmation": {
"negative": "Annuleren",
"title": "Opname stoppen",
"body": "Je bent momenteel een spraakbericht aan het opnemen. Wil je de opname stoppen en wissen?",
"affirmative": "Stoppen en wissen"
},
"cancel": "Annuleren"
},
"sendMedia": "Media versturen",
"share": "Delen"
},
"newconversation": {
"title": "Nieuw gesprek",
"startChat": "Gesprek starten",
"createGroupchat": "Nieuw groepsgesprek",
"nick": "Bijnaam",
"enterNickname": "Voer een bijnaam in",
"nicknameSubtitle": "Voer een unieke bijnaam in om deel te kunnen nemen aan een MUC.",
"joinGroupChat": "Deelnemen aan groepsgesprek",
"nullNickname": "Voer een bijnaam in!"
},
"crop": {
"setProfilePicture": "Instellen als profielfoto"
},
"shareselection": {
"shareWith": "Delen met…",
"confirmTitle": "Bestand versturen",
"confirmBody": "Een of meerdere gesprekken zijn onversleuteld. Dit houdt in dat het bestand kan worden uitgelezen door de server. Weet je zeker dat je wilt doorgaan?"
},
"profile": {
"general": {
"omemo": "Beveiliging",
"profile": "Profiel",
"media": "Media"
},
"conversation": {
"notifications": "Meldingen",
"notificationsMuted": "Gedempt",
"notificationsEnabled": "Ingeschakeld",
"sharedMedia": "Media"
},
"owndevices": {
"title": "Mijn apparaten",
"thisDevice": "Dit apparaat",
"otherDevices": "Overige apparaten",
"deleteDeviceConfirmTitle": "Apparaat verwijderen",
"deleteDeviceConfirmBody": "Let op: hierdoor kunnen contactpersonen het apparaat niet meer versleutelen. Wil je doorgaan?",
"recreateOwnSessions": "Sessies heraanmaken",
"recreateOwnSessionsConfirmTitle": "Wil je je sessies heraanmaken?",
"recreateOwnDevice": "Apparaat heraanmaken",
"recreateOwnDeviceConfirmTitle": "Wil je je apparaat heraanmaken?",
"recreateOwnSessionsConfirmBody": "Hierdoor worden de versleutelde sessies opnieuw aangemaakt op je apparaten. Let op: doe dit alléén als apparaten ontsleutelfoutmeldingen tonen.",
"recreateOwnDeviceConfirmBody": "Hierdoor wordt de versleutelde identiteit van dit apparaat opnieuw aangemaakt. Dit kan even duren. Als contactpersonen je apparaat hebben goedgekeurd, dan dienen ze dit opnieuw te doen. Wil je doorgaan?"
},
"devices": {
"title": "Beveiliging",
"recreateSessions": "Sessies heraanmaken",
"recreateSessionsConfirmTitle": "Wil je je sessies heraanmaken?",
"recreateSessionsConfirmBody": "Hierdoor worden alle versleutelde sessies opnieuw aangemaakt op je apparaten. Let op: doe dit alléén als je apparaten ontsleutelfoutmeldingen tonen.",
"noSessions": "Er zijn geen versleutelde sessies die worden gebruikt voor eind-tot-eindversleuteling."
},
"serverInfo": {
"title": "Serverinformatie"
}
},
"blocklist": {
"title": "Blokkadelijst",
"noUsersBlocked": "Er zijn geen geblokkeerde gebruikers",
"unblockAll": "Iedereen deblokkeren",
"unblockAllConfirmTitle": "Weet je het zeker?",
"unblockAllConfirmBody": "Weet je zeker dat je alle gebruikers wilt deblokkeren?",
"unblockJidConfirmTitle": "Wil je ${jid} deblokkeren?",
"unblockJidConfirmBody": "Weet je zeker dat je ${jid} wilt deblokkeren? Hierdoor ontvang je weer berichten van deze gebruiker."
},
"cropbackground": {
"blur": "Achtergrond vervagen",
"setAsBackground": "Instellen als achtergrond"
},
"stickerPack": {
"removeConfirmTitle": "Stickerpakket verwijderen",
"installConfirmTitle": "Stickerpakket installeren",
"installConfirmBody": "Weet je zeker dat je dit stickerpakket wilt installeren?",
"fetchingFailure": "Het stickerpakket is niet gevonden",
"removeConfirmBody": "Weet je zeker dat je dit stickerpakket wilt verwijderen?",
"restricted": "Dit stickerpakket is beperkt toegankelijk. Dit houdt in dat de stickers kunnen worden getoond, maar niet worden verstuurd."
},
"settings": {
"settings": {
"title": "Instellingen",
"conversationsSection": "Gesprekken",
"accountSection": "Account",
"signOut": "Uitloggen",
"signOutConfirmTitle": "Uitloggen",
"signOutConfirmBody": "Je staat op het punt om uit te loggen. Wil je doorgaan?",
"miscellaneousSection": "Overig",
"debuggingSection": "Foutopsporing",
"general": "Algemeen"
},
"about": {
"title": "Over",
"licensed": "Uitgebracht onder de GPL3-licentie",
"version": "Versie ${version}",
"viewSourceCode": "Broncode bekijken",
"nMoreToGo": "Nog ${n} te gaan…",
"debugMenuShown": "Je bent nu een ontwikkelaar!",
"debugMenuAlreadyShown": "Je bent al een ontwikkelaar!"
},
"appearance": {
"title": "Vormgeving",
"languageSection": "Taal",
"language": "Apptaal",
"languageSubtext": "Huidige taal: $selectedLanguage",
"systemLanguage": "Standaardtaal"
},
"licenses": {
"title": "Opensourcelicenties",
"licensedUnder": "Uitgebracht onder de $license-licentie"
},
"conversation": {
"title": "Gesprek",
"appearance": "Vormgeving",
"selectBackgroundImage": "Kies een achtergrond",
"removeBackgroundImage": "Afbeelding verwijderen",
"removeBackgroundImageConfirmTitle": "Afbeelding verwijderen",
"removeBackgroundImageConfirmBody": "Weet je zeker dat je de huidige gespreksachtergrond wilt verwijderen?",
"newChatsSection": "Nieuwe gesprekken",
"newChatsMuteByDefault": "Nieuwe gesprekken dempen",
"newChatsE2EE": "Eind-tot-eindversleuteling standaard inschakelen (WAARSCHUWING: experimenteel)",
"behaviourSection": "Gedrag",
"contactsIntegration": "Contactpersoonintegratie",
"contactsIntegrationBody": "Schakel in om het adresboek te gebruiken om gesprekstitels en profielfoto's in te stellen. Er worden geen gegevens verstuurd naar de server.",
"selectBackgroundImageDescription": "Deze afbeelding wordt gebruikt als achtergrond in al je gesprekken"
},
"debugging": {
"title": "Foutopsporingsopties",
"generalSection": "Algemeen",
"generalEnableDebugging": "Foutopsporing inschakelen",
"generalEncryptionPassword": "Versleutelwachtwoord",
"generalLoggingIp": "Ip-log",
"generalLoggingIpSubtext": "Het ip-adres waar de logboeken naartoe dienen te worden gestuurd",
"generalLoggingPort": "Logpoort",
"generalLoggingPortSubtext": "Het ip-adres waar de logboeken naartoe dienen te worden gestuurd",
"generalEncryptionPasswordSubtext": "Let op: de logboeken kunnen privéinformatie bevatten, dus stel een sterk wachtwoord in"
},
"network": {
"title": "Netwerk",
"automaticDownloadsSection": "Automatisch ophalen",
"automaticDownloadsMaximumSize": "Maximale downloadomvang",
"automaticDownloadsMaximumSizeSubtext": "De maximale bestandsgrootte van automatisch op te halen bestanden",
"automaticDownloadAlways": "Ieder netwerk",
"wifi": "Wifi",
"mobileData": "Mobiel internet",
"automaticDownloadsText": "Moxxy zal bestanden automatisch ophalen bij gebruik van…"
},
"privacy": {
"title": "Privacy",
"generalSection": "Algemeen",
"showContactRequests": "Contactpersoonverzoeken tonen",
"profilePictureVisibility": "Profielfoto aan iedereen tonen",
"conversationsSection": "Gesprek",
"sendChatMarkersSubtext": "Hiermee kan je gesprekspartner zien of je een bericht gelezen of ontvangen hebt",
"sendChatMarkers": "Gespreksacties versturen",
"sendChatStates": "Gespreksstatussen versturen",
"sendChatStatesSubtext": "Hiermee kan je gesprekspartner zien of je aan het typen of het gesprek aan het bekijken bent",
"redirectsSection": "Doorverwijzingen",
"redirectText": "Hiermee worden $serviceName-links doorverwezen naar een proxy, bijvoorbeeld $exampleProxy",
"currentlySelected": "Huidige proxy: $proxy",
"redirectsTitle": "$serviceName-doorverwijzing",
"cannotEnableRedirect": "$serviceName-doorverwijzingen mislukt",
"cannotEnableRedirectSubtext": "Stel eerst een proxy als doorverwijzing in. Druk hiervoor op het veld naast de schakelaar.",
"urlEmpty": "Voer een url in",
"urlInvalid": "De url is ongeldig",
"redirectDialogTitle": "$serviceName-doorverwijzing",
"stickersPrivacy": "Stickerlijst aan iedereen tonen",
"stickersPrivacySubtext": "Schakel in om je lijst met stickerpakketten aan iedereen te tonen",
"showContactRequestsSubtext": "Hiermee worden verzoeken getoond van personen die je hebben toegevoegd, maar nog geen bericht hebben gestuurd",
"profilePictureVisibilitSubtext": "Schakel in om iedereen je profielfoto te tonen; schakel uit om alleen gebruikers op je lijst je profielfoto te tonen"
},
"stickers": {
"title": "Stickers",
"stickerSection": "Sticker",
"displayStickers": "Stickers in gesprekken tonen",
"autoDownload": "Stickers automatisch ophalen",
"autoDownloadBody": "Schakel in om stickers automatisch op te halen na het toevoegen van de afzender",
"stickerPacksSection": "Stickerpakketten",
"importStickerPack": "Stickerpakket importeren",
"importSuccess": "Het stickerpakket is geïmporteerd",
"importFailure": "Het stickerpakket kan niet worden geïmporteerd",
"stickerPackSize": "(${size})"
},
"storage": {
"title": "Opslag",
"storageUsed": "In gebruik: ${size}",
"sizePlaceholder": "Bezig met berekenen…",
"storageManagement": "Opslagbeheer",
"removeOldMedia": {
"title": "Oude media verwijderen",
"description": "Verwijdert oude mediabestanden van het apparaat"
},
"removeOldMediaDialog": {
"title": "Mediabestanden verwijderen",
"options": {
"all": "Alle mediabestanden",
"oneWeek": "Ouder dan 1 week",
"oneMonth": "Ouder dan 1 maand"
},
"delete": "Verwijderen",
"confirmation": {
"body": "Weet je zeker dat je oude mediabestanden wilt verwijderen?"
}
},
"viewMediaFiles": "Mediabestanden bekijken",
"mediaFiles": "Mediabestanden",
"types": {
"media": "Media",
"stickers": "Stickers"
},
"manageStickers": "Stickerpakketten beheren"
},
"stickerPacks": {
"title": "Stickerpakketten"
}
},
"startchat": {
"title": "Nieuw gesprek",
"xmppAddress": "Xmpp-adres",
"subtitle": "Je kunt een nieuw gesprek starten door een xmpp-adres in te voeren of een QR-code te scannen.",
"buttonAddToContact": "Gesprek starten"
},
"sharedMedia": {
"empty": {
"chat": "Er is geen gedeelde media in dit gesprek",
"general": "Er zijn geen mediabestanden beschikbaar"
}
}
},
"permissions": {
"allow": "Toestaan",
"skip": "Overslaan",
"requests": {
"notification": {
"reason": "Moxxy heeft het recht om meldingen te tonen nodig om meldingen van inkomende berichten te kunnen tonen."
},
"batterySaving": {
"reason": "Sluit Moxxy uit van Androids accubesparing om berichten op de achtergrond te ontvangen."
}
}
},
"emojiPicker": {
"noRecents": "Geen onlangs gebruikte emojis"
}
}

View File

@@ -0,0 +1,144 @@
{
"language": "Polski",
"global": {
"title": "Moxxy",
"dialogAccept": "OK",
"yes": "Tak",
"no": "Nie",
"dialogCancel": "Anuluj"
},
"notifications": {
"permanent": {
"connecting": "Łączenie…",
"disconnect": "Rozłączono",
"error": "Błąd"
},
"channels": {
"messagesChannelName": "Wiadomości",
"warningChannelName": "Ostrzeżenia"
},
"titles": {
"error": "Błąd"
}
},
"permissions": {
"allow": "Zezwól",
"skip": "Pomiń"
},
"dateTime": {
"mondayAbbrev": "Pon",
"tuesdayAbbrev": "Wt",
"wednessdayAbbrev": "Śr",
"thursdayAbbrev": "Czw",
"fridayAbbrev": "Pt",
"saturdayAbbrev": "Sob",
"sundayAbbrev": "Nd",
"november": "Listopad",
"june": "Czerwiec",
"september": "Wrzesień",
"today": "Dzisiaj",
"february": "Luty",
"december": "Grudzień",
"yesterday": "Wczoraj",
"march": "Marzec",
"august": "Sierpień",
"october": "Październik",
"may": "Maj",
"january": "Styczeń",
"april": "Kwiecień",
"july": "Lipiec"
},
"pages": {
"conversation": {
"unencrypted": "Nieszyfrowane",
"blockShort": "Zablokuj",
"online": "Online",
"warning": "Ostrzeżenie",
"encrypted": "Szyfrowane",
"forward": "Przekaż dalej",
"quote": "Cytat",
"edit": "Edytuj"
},
"conversations": {
"overlaySettings": "Ustawienia"
},
"settings": {
"privacy": {
"conversationsSection": "Konwersacja",
"title": "Prywatność",
"generalSection": "Ogólne"
},
"settings": {
"miscellaneousSection": "Róźne",
"conversationsSection": "Konwersacje",
"accountSection": "Konto",
"general": "Ogólne",
"title": "Ustawienia",
"debuggingSection": "Debugowanie"
},
"stickers": {
"stickerSection": "Naklejka",
"title": "Naklejki",
"stickerPackSize": "(${size})"
},
"network": {
"title": "Sieć",
"wifi": "Wi-Fi",
"automaticDownloadAlways": "Zawsze"
},
"conversation": {
"appearance": "Wygląd",
"title": "Czat",
"behaviourSection": "Zachowanie"
},
"appearance": {
"languageSection": "Język",
"title": "Wygląd"
},
"about": {
"title": "O programie"
},
"debugging": {
"generalSection": "Ogólne"
}
},
"login": {
"xmppAddress": "Adres XMPP",
"title": "Logowanie",
"password": "Hasło"
},
"profile": {
"general": {
"omemo": "Bezpieczeństwo",
"profile": "Profil"
},
"devices": {
"title": "Bezpieczeństwo"
},
"conversation": {
"notificationsMuted": "Wyciszono",
"notifications": "Powiadomienia"
}
},
"intro": {
"registerButton": "Zarejestruj się",
"loginButton": "Zaloguj się"
},
"blocklist": {
"title": "Lista zablokowanych"
}
},
"errors": {
"conversation": {
"messageErrorDialogTitle": "Błąd"
}
},
"messages": {
"sticker": "Naklejka",
"audio": "Dźwięk",
"file": "Plik",
"image": "Obraz",
"video": "Film",
"you": "Ty"
}
}

View File

@@ -0,0 +1,438 @@
{
"global": {
"title": "Moxxy",
"dialogAccept": "Принять",
"dialogCancel": "Отмена",
"yes": "Да",
"no": "Нет",
"moxxySubtitle": "Эксперементальный XMPP-клиент, простой, современный и красивый."
},
"notifications": {
"permanent": {
"idle": "Idle",
"ready": "Готов к приему сообщений",
"connecting": "Соединение…",
"disconnect": "Отключен",
"error": "Ошибка"
},
"message": {
"reply": "Ответ",
"markAsRead": "Прочитано"
},
"channels": {
"messagesChannelName": "Сообщения",
"warningChannelName": "Предупреждения",
"warningChannelDescription": "Предупреждения, связанные с Moxxy",
"messagesChannelDescription": "Канал уведомлений о полученных сообщениях",
"serviceChannelName": "Фоновая служба"
},
"titles": {
"error": "Ошибка"
},
"errors": {
"messageError": {
"body": "Не удалось доставить сообщение для ${conversationTitle}",
"title": "Сообщение не доставлено"
}
},
"warnings": {
"blockingError": {
"body": "Не удалось заблокировать пользователя ${jid}, потому что ваш сервер не поддерживает блокировки.",
"title": "Ошибка блокировки пользователя"
}
}
},
"dateTime": {
"justNow": "Сейчас",
"nMinutesAgo": "${min}минут назад",
"mondayAbbrev": "Пн",
"tuesdayAbbrev": "Вт",
"wednessdayAbbrev": "Ср",
"thursdayAbbrev": "Чт",
"fridayAbbrev": "Пт",
"sundayAbbrev": "Вс",
"january": "Январь",
"february": "Февраль",
"may": "Май",
"june": "Июнь",
"october": "Октябрь",
"november": "Ноябрь",
"december": "Декабрь",
"today": "Сегодня",
"saturdayAbbrev": "Сб",
"march": "Март",
"april": "Апрель",
"july": "Июль",
"august": "Август",
"september": "Сентябрь",
"yesterday": "Вчера"
},
"messages": {
"image": "Изображение",
"video": "Видео",
"file": "Файл",
"sticker": "Стикер",
"you": "Ты",
"audio": "Аудио",
"retracted": "Сообщение удалено",
"retractedFallback": "Предыдущее сообщение было удалено, но это не поддерживается Вашим клиентом"
},
"errors": {
"filePicker": {
"permissionDenied": "Доступ к хранилищу не был выдан"
},
"omemo": {
"notEncryptedForDevice": "Сообщение зашифровано, но не для этого устройства",
"invalidHmac": "Не удалось расшифровать сообщение",
"noDecryptionKey": "Нет ключа для расшифровки",
"verificationWrongFingerprint": "Неправильный отпечаток OMEMO:2",
"couldNotPublish": "Не удалось опубликовать ключи шифрования на сервере. Это означает, что сквозное шифрование может не работать.",
"messageInvalidAfixElement": "Ошибка в зашифрованном сообщении",
"verificationInvalidOmemoUrl": "неверный отпечаток OMEMO:2",
"verificationWrongJid": "Неправильный XMPP-адрес",
"verificationWrongDevice": "Неправильное OMEMO:2 устройство",
"verificationNotInList": "Неправильное устройство OMEMO:2"
},
"connection": {
"connectionTimeout": "Нет соединения с сервером",
"saslInvalidCredentials": "Данные учетной записи недействительны",
"unrecoverable": "Соединение прервано из-за ошибки",
"saslAccountDisabled": "Аккаунт отключен"
},
"login": {
"saslFailed": "Неверный логин",
"startTlsFailed": "Не удалось установить безопасное соединение",
"noConnection": "Не удалось установить соединение",
"unspecified": "Неопределенная ошибка"
},
"message": {
"fileDownloadFailed": "Не удалось загрузить файл",
"remoteServerTimeout": "Сообщение не доставлено на сервер получателя",
"unspecified": "Неизвесная ошибка",
"fileUploadFailed": "Не удалось отправить файл",
"contactDoesntSupportOmemo": "Получатель не поддерживает OMEMO:2 шифрование",
"serviceUnavailable": "Сообщение не доставлено получателю",
"remoteServerNotFound": "Cообщение не доставлено, не найден сервер получателя",
"failedToEncrypt": "Сообщение не может быть зашифровано",
"failedToEncryptFile": "Файл не может быть зашифрован",
"failedToDecryptFile": "Файл не может быть расшифрован",
"fileNotEncrypted": "Этот чат зашифрован, но файл нет"
},
"conversation": {
"audioRecordingError": "Не удалось завершить аудиозапись",
"openFileNoAppError": "Приложения для открытия этого файла не найдены",
"openFileGenericError": "Не удалось открыть файл",
"messageErrorDialogTitle": "Ошибка"
},
"newChat": {
"groupchatUnsupported": "Вступление в групповой чат пока не поддерживается.",
"remoteServerError": "Не удалось связаться с удалённым сервером.",
"unknown": "Неизвестная ошибка."
},
"general": {
"noInternet": "Нет подключения к интернету."
}
},
"pages": {
"intro": {
"registerButton": "Регистрация",
"loginButton": "Login",
"noAccount": "Нет XMPP аккаунта? Зарегистрируйтесь на одном из серверов, это не сложно."
},
"login": {
"title": "Login",
"password": "Пароль",
"xmppAddress": "XMPP-адрес",
"advancedOptions": "Расширенные опции",
"createAccount": "Зарегистрироваться на сервере"
},
"conversations": {
"speeddialNewChat": "Написать",
"speeddialJoinGroupchat": "Групповой чат",
"speeddialAddNoteToSelf": "Заметка себе",
"overlaySettings": "Настройки",
"startChat": "Начать диалог",
"markAsRead": "Пометить как прочитанное",
"noOpenChats": "У вас пока нет диалогов",
"closeChat": "Завершить диалог",
"closeChatBody": "Вы уверены, что хотите завершить диалог с ${conversationTitle}?"
},
"conversation": {
"unencrypted": "Незашифрованно",
"encrypted": "Зашифрованно",
"closeChat": "Закрыть чат",
"closeChatConfirmTitle": "Закрыть чат",
"closeChatConfirmSubtext": "Вы уверены что хотите закрыть этот чат?",
"blockShort": "Заблокировать",
"blockUser": "Заблокировать пользователя",
"online": "В сети",
"retract": "Отозвать сообщение",
"copy": "Копировать содержимое",
"addReaction": "Реакция",
"showError": "Показать ошибки",
"showWarning": "Показать предупреждения",
"sendFiles": "Отправить файлы",
"takePhotos": "Сделать фотографии",
"retractBody": "Вы уверены, что хотите отозвать сообщение? Помните, что это всего лишь просьба, которую клиент не обязан выполнять.",
"forward": "Переслать",
"edit": "Изменить",
"quote": "Цитировать",
"addToContacts": "Добавить в контакты",
"addToContactsTitle": "Добавить ${jid} в контакты",
"addToContactsBody": "Вы уверены, что хотите добавить ${jid} в контакты?",
"stickerPickerNoStickersLine1": "Нет установленных стикерпаков.",
"stickerPickerNoStickersLine2": "Их можно установить в настройках стикеров.",
"stickerSettings": "Настройки стикеров",
"newDeviceMessage": {
"one": "Добавлено новое устройство",
"other": "Добавлено несколько новых устройств"
},
"replacedDeviceMessage": {
"one": "Устройство было изменено",
"other": "Добавлено несколько устройств"
},
"messageHint": "Сообщение…",
"sendImages": "Отправить изображение",
"messageCopied": "Сообщение скопировано в буфер",
"warning": "Предупреждение"
},
"startchat": {
"xmppAddress": "XMPP-адрес",
"buttonAddToContact": "Добавить в контакты",
"title": "Добавить контакт",
"subtitle": "Вы можете добавить контакт введя его XMPP адрес или отсканировав QR код"
},
"newconversation": {
"title": "Новый чат",
"startChat": "Добавить контакт",
"createGroupchat": "Создать новый групповой чат",
"joinGroupChat": "Присоединиться к групповому чату"
},
"shareselection": {
"shareWith": "Поделиться с...",
"confirmTitle": "Отправить файл",
"confirmBody": "Один или несколько чатов не зашифрованы, из-за чего файл будет доступен администрации сервера. Вы уверены, что хотите продолжить?"
},
"profile": {
"general": {
"omemo": "Безопасность",
"profile": "Профиль",
"media": "Медиа"
},
"conversation": {
"sharedMedia": "Медиа",
"notifications": "Уведомления",
"notificationsMuted": "Без звука",
"notificationsEnabled": "Включено"
},
"owndevices": {
"thisDevice": "Это устройство",
"recreateOwnDevice": "Восстановить устройство",
"title": "Мои устройства",
"otherDevices": "Другие устройства",
"deleteDeviceConfirmTitle": "Удалить устройство",
"deleteDeviceConfirmBody": "Контакты не смогут быть зашифрованы для этого устройства. Продолжить?",
"recreateOwnSessions": "Пересоздать сеанс",
"recreateOwnSessionsConfirmTitle": "Пересоздать свои сеансы?",
"recreateOwnSessionsConfirmBody": "Создать новые ключи шифрования для этого устройства. Используйте только в крайнем случае.",
"recreateOwnDeviceConfirmTitle": "Восстановить это устройство?",
"recreateOwnDeviceConfirmBody": "Это создаст новый криптографический отпечаток устройства, что займёт некоторое время. Если ваше устройство было подтверждено контактами, им придётся сделать это снова. Продолжить?"
},
"devices": {
"title": "Безопасность",
"recreateSessionsConfirmTitle": "Пересоздать сеанс?",
"noSessions": "Нет криптографических сессий, используемых для сквозного шифрования.",
"recreateSessions": "Пересоздать сеанс",
"recreateSessionsConfirmBody": "Создать новые ключи шифрования для этого устройства. Используйте только в крайнем случае."
}
},
"blocklist": {
"unblockAll": "Разблокировать всех",
"unblockJidConfirmTitle": "Разблокировать ${jid}?",
"title": "Блоклист",
"noUsersBlocked": "Нет заблокированных пользователей",
"unblockAllConfirmTitle": "Вы уверены?",
"unblockAllConfirmBody": "Вы действительно хотите разблокировать всех пользователей?",
"unblockJidConfirmBody": "Вы уверены, что хотите разблокировать ${jid}? Вы снова будете получать сообщения от этого пользователя."
},
"cropbackground": {
"blur": "Размыть фон",
"setAsBackground": "Установить фоновое изображение"
},
"crop": {
"setProfilePicture": "Загрузить аватар"
},
"stickerPack": {
"removeConfirmTitle": "Удалить стикерпак",
"removeConfirmBody": "Вы действительно хотите удалить этот стикерпак?",
"installConfirmTitle": "добавить стикерпак",
"installConfirmBody": "Вы действительно хотите установить этот стикерпак?",
"restricted": "Этот стикерпак ограничен, стикеры будут отображаться, но отправить их нельзя.",
"fetchingFailure": "Стикерпак не найден"
},
"settings": {
"about": {
"version": "Версия ${version}",
"debugMenuShown": "Теперь ты разработчик ^_^",
"debugMenuAlreadyShown": "Ты уже разработчик :^",
"title": "О нас",
"viewSourceCode": "Исходный код",
"nMoreToGo": "Осталось еще ${n}...",
"licensed": "Лицензировано под GPL3"
},
"conversation": {
"removeBackgroundImageConfirmBody": "Вы действительно хотите удалить фон?",
"title": "Чат",
"appearance": "Внешний вид",
"selectBackgroundImage": "Выбрать фон",
"removeBackgroundImage": "Удалить фон",
"removeBackgroundImageConfirmTitle": "Удалить фон",
"newChatsSection": "Новые чаты",
"newChatsMuteByDefault": "Отключать звук в новых чатах по умолчанию",
"newChatsE2EE": "Включить оконечное шифрование по умолчанию",
"behaviourSection": "Поведение",
"contactsIntegration": "Синхронизация контактов",
"selectBackgroundImageDescription": "Это изображение будет фоном для ваших чатов",
"contactsIntegrationBody": "При включении данные из Контактов будут использованы для названий чатов и фото профилей. На сервер ничего отправлено не будет."
},
"debugging": {
"title": "Опции отладки",
"generalSection": "Основные",
"generalEnableDebugging": "Включить отладку",
"generalEncryptionPassword": "Пароль шифрования",
"generalEncryptionPasswordSubtext": "Журналы могут содержать конфиденциальную информацию, поэтому поставте надежный пароль",
"generalLoggingIpSubtext": "IP, на который должны отправляться журналы",
"generalLoggingIp": "IP для логов",
"generalLoggingPort": "порт для логов",
"generalLoggingPortSubtext": "IP, на который должны отправляться журналы"
},
"network": {
"automaticDownloadsSection": "Автоматическая загрузка",
"title": "Сеть",
"automaticDownloadsMaximumSizeSubtext": "Максимальный размер, при котором файлы будут автоматически загружаться",
"automaticDownloadsMaximumSize": "Максимальный размер для загрузки",
"automaticDownloadAlways": "Всегда",
"wifi": "Wifi",
"mobileData": "Мобильный интернет",
"automaticDownloadsText": "Moxxy будет автоматически загружать файлы до..."
},
"privacy": {
"showContactRequests": "Показывать запрос в контакты",
"showContactRequestsSubtext": "Это покажет людей, добавивших вас в свой список контактов",
"generalSection": "Основные",
"profilePictureVisibility": "Сделать фото профиля публичным",
"sendChatMarkers": "Отправлять маркеры",
"redirectText": "Это позволит перенаправлять ссылки с ${serviceName} на прокси, такие как ${exampleProxy}",
"redirectsSection": "Перенаправление",
"currentlySelected": "Выбрано сейчас: $proxy",
"redirectsTitle": "$serviceName Перенаправление",
"urlEmpty": "URL не может быть пустым",
"title": "Приватность",
"conversationsSection": "Диалог",
"sendChatMarkersSubtext": "Это сообщит вашему собеседнику о получении или прочтении сообщения",
"sendChatStates": "Отправлять состояние чата",
"sendChatStatesSubtext": "Собеседник будет видеть, когда вы набираете сообщение или просматриваете чат",
"cannotEnableRedirect": "Невозможно включить перенаправления $serviceName",
"cannotEnableRedirectSubtext": "Сначала нужно добавить прокси сервер. Для этого нажмите слева от переключателя",
"urlInvalid": "Недопустимый URL",
"redirectDialogTitle": "$serviceName Перенаправление",
"stickersPrivacy": "Публиковать список стикеров",
"stickersPrivacySubtext": "Если включено, ваши установленные наборы стикеров будут видны всем.",
"profilePictureVisibilitSubtext": "Когда включено, все видят Ваш аватар; когда выключено - только контакты"
},
"stickers": {
"importSuccess": "Стикерпаки успешно импортированы",
"title": "Стикеры",
"stickerSection": "Стикеры",
"displayStickers": "Показывать стикеры",
"autoDownload": "Загружать стикеры автоматически",
"autoDownloadBody": "Стикеры будут загружаться автоматически, если их отправитель у вас в контактах.",
"stickerPacksSection": "Стикерпаки",
"importStickerPack": "Импортировать стикерпаки",
"importFailure": "Ошибка при импорте стикерпаков",
"stickerPackSize": "(${size})"
},
"settings": {
"title": "Настройки",
"conversationsSection": "Чаты",
"accountSection": "Учётная запись",
"signOut": "Выйти",
"signOutConfirmTitle": "Выйти",
"signOutConfirmBody": "Вы хотите выйти, продолжить?",
"miscellaneousSection": "Другое",
"debuggingSection": "Отладка",
"general": "Основные"
},
"appearance": {
"title": "Внешний вид",
"languageSection": "Язык",
"language": "Язык в приложении",
"systemLanguage": "Как в системе",
"languageSubtext": "Выбранный язык: ${selectedLanguage}"
},
"licenses": {
"title": "Открытые лицензии",
"licensedUnder": "Лицензировано под ${license}"
},
"storage": {
"removeOldMedia": {
"description": "Удалит старые медиафайлы с устройства",
"title": "Удалить старые медиафайлы"
},
"title": "Хранилище",
"sizePlaceholder": "Вычисление...",
"storageManagement": "Управление хранилищем",
"removeOldMediaDialog": {
"options": {
"all": "Все медиафайлы",
"oneWeek": "Старее 1 недели",
"oneMonth": "Старее 1 месяца"
},
"delete": "Удалить",
"title": "Удалить медиафайлы",
"confirmation": {
"body": "Вы точно хотите удалить старые медиафайлы?"
}
},
"viewMediaFiles": "Просмотреть медиафайлы",
"mediaFiles": "Медиафайлы",
"types": {
"media": "Медиа",
"stickers": "Стикеры"
},
"storageUsed": "Использование хранилища: ${size}",
"manageStickers": "Управление наборами стикеров"
},
"stickerPacks": {
"title": "Наборы стикеров"
}
},
"sharedMedia": {
"empty": {
"chat": "Нет общих медиафайлов для этого чата",
"general": "Нет доступных медиаустройств"
}
}
},
"language": "Русский",
"warnings": {
"message": {
"integrityCheckFailed": "Не удалось проверить целостность файла"
},
"conversation": {
"holdForLonger": "Удерживайте для записи"
}
},
"permissions": {
"allow": "Разрешить",
"skip": "Пропустить",
"requests": {
"batterySaving": {
"reason": "Чтобы получать сообщения в фоне, Moxxy должен быть исключён из экономии заряда батареи Android."
},
"notification": {
"reason": "Чтобы оповещать о входящих сообщениях, Moxxy требуется право на показ уведомлений."
}
}
}
}

BIN
assets/images/empty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 102 KiB

BIN
assets/repo/kofi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -5,3 +5,5 @@ targets:
options:
input_directory: assets/i18n
output_directory: lib/i18n
fallback_strategy: base_locale
base_locale: en

43
docs/stickerpacks.md Normal file
View File

@@ -0,0 +1,43 @@
# Sticker Packs
Moxxy supports sending and receiving sticker packs using XEP-0449 version 0.1.1. Sticker
packs can also be imported using a Moxxy specific format.
## File Format
A Moxxy sticker pack is a flat tar archive that contains the following files:
- `urn.xmpp.stickers.0.xml`
- The sticker files
### `urn.xmpp.stickers.0.xml`
This file is the sticker pack's metadata file. It describes the sticker pack the same
way as the examples in XEP-0449 do. There are, however, some differences:
- Each `<file />` element must contain a `<name />` element that matches with a file in the tar archive
- Each sticker MUST contain at least one HTTP(s) source
- The `<hash />` of the `<pack />` element is ignored as Moxxy computes it itself, so it can be omitted
An example for the metadata file is the following:
```xml
<pack xmlns='urn:xmpp:stickers:0'>
<name>Example</name>
<summary>Example sticker pack.</summary>
<item>
<file xmlns='urn:xmpp:file:metadata:0'>
<media-type>image/png</media-type>
<desc>:some-sticker:</desc>
<name>suprise.png</name>
<size>531910</size>
<dimensions>1030x1030</dimensions>
<hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>1Ha4okUGNRAA04KibwWUmklqqBqdhg7+20dfsr/wLik=</hash>
</file>
<sources xmlns='urn:xmpp:sfs:0'>
<url-data xmlns='http://jabber.org/protocol/url-data' target='...' />
</sources>
</item>
<!-- ... -->
</pack>
```

View File

@@ -0,0 +1 @@
Moxxy ist ein experimenteller XMPP-Client, der modern und einfach sein soll.

View File

@@ -0,0 +1 @@
Moxxy

View File

@@ -0,0 +1,12 @@
Many changes in this release are under the hood, but there are many changes nonetheless:
- Messages that are sent while offline are now queued up until we're online again
- Moxxy now makes use of SFS's caching possibilities. Receiving files sent via SFS are thus only downloaded if the file is not already locally available
- Messages and shared media files are now shown in paged lists
- Reworked various pages, like the Conversation page and the profile page
- Rework the reactions UI
- Add a "note to self" feature. This was a teaser task in the context of this year's GSoC
- Chat states are no longer sent if a chat is no longer focused
- Sending a sticker when a message is selected for quoting, the sticker is sent as a reply to that message
- The database design was massively overhauled
- The emoji/sticker picker should no longer jump around when switching from the keyboard

View File

@@ -0,0 +1,7 @@
This is a hotfix release.
Sending a message with no attached file results in a gray
box being displayed over the entire message list. This release
contains a fix for that.
(I also dropped my fork of the Flutter SDK)

View File

@@ -0,0 +1,13 @@
- (Hopefully) fix OMEMO between two Moxxy clients.
- Allow correcting messages older than the last one. Whether all clients will accept such a correction is unclear.
- Add (incomplete) translations for Dutch, French, Galician, Japanese, Polish, and Russian.
- Fix having to long-press a message bubble on its corner to active the selection menu.
- If enabled, read markers are automatically sent.
- Highlight legacy quotes in text messages.
- Fix Moxxy's app icon having a badge because of the foreground service.
- Make the notifications much prettier and compliant with Android 13.
- Prevent Moxxy from crashing on startup on a fresh device.
- Video thumbnails are now generated, if possible, after a video has been downloaded.
- The Moxxy APKs will now be signed by a different key stored on my YubiKey. You will have to uninstall and reinstall Moxxy. This will remove all your data.
- Moxxy now uses Android's direct share shortcuts.
- Moxxy now uses Android 13's new photo picker, whenever possible. This should allow Moxxy to require fewer permissions to work.

View File

@@ -0,0 +1,2 @@
- Migrate to Flutter's Material 3 implementation.
- Rework the voice message recorder.

View File

@@ -0,0 +1,7 @@
* Expose the debug menu by tapping the Moxxy icon on the about page 10 times
* Maybe fix a connection race condition
* Allow sharing media with the app when it was closed
* Make quotes prettier
* Make the bottom part of the conversation page prettier
* Fix roster fetching
* Fix OMEMO key generation

View File

@@ -10,12 +10,14 @@ Currently supported features include:
<li>Typing indicators and message markers</li>
<li>Chat backgrounds</li>
<li>Runs in the background without Push Notifications</li>
<li>OMEMO (Currently not compatible with most apps)</li>
<li>Stickers</li>
</ul>
For the best experience, I recommend a server that:
<ul>
<li>Supports direct TLS/StartTLS on the same domain as in the Jid</li>
<li>Supports SCRAM-SHA-1 or SCRAM-SHA-256</li>
<li>Supports SCRAM-SHA-1, SCRAM-SHA-256 or SCRAM-SHA-512</li>
<li>Supports HTTP File Upload</li>
<li>Supports Stream Management</li>
<li>Supports Client State Indication</li>

View File

@@ -0,0 +1 @@
Moxxy est un client XMPP expérimental qui vise dêtre moderne et facile.

View File

@@ -0,0 +1 @@
Moxxy

View File

@@ -0,0 +1,10 @@
- Offline-berichten worden verstuurd als de verbinding hersteld is;
- SFS-cache, waardoor downloaden alleen plaatsvindt indien niet lokaal beschikbaar;
- Berichten en mediabestanden worden op pagina's getoond;
- Diverse pagina's bijgewerkt;
- Reacties herontworpen;
- Zelfmemofunctie;
- Gespreksstatussen worden niet meer verstuurd indien ongefocust;
- Stickers als antwoord op citaten;
- Nieuw databankontwerp;
- Verbeterde emoji-/stickerkeuze i.c.m. toetsenbord.

View File

@@ -0,0 +1,5 @@
Dit is een oplossingsversie:
Het versturen van een bericht zonder bijlage zorgde voor een grijs vlak op de berichtenlijst. Dat is nu opgelost.
(Ook ben ik gestopt met de ontwikkeling van mijn afsplitsing van de Flutter-sdk.)

View File

@@ -0,0 +1,5 @@
-(Hopelijk) Oplossing voor OMEMO tussen twee Moxxy-clients;
-Oplossing voor het lang ingedrukt houden van een bericht om het keuzemenu te openen;
-Leesmarkeringen worden voortaan automatisch verzonden (indien ingeschakeld);
-Nieuw: (onvolledige) Nederlandse, Japanse en Russische vertalingen;
-Nieuw: bewerken van berichten ouder dan het recentste bericht. Onduidelijk of alle clients dit op de juiste manier tonen.

View File

@@ -0,0 +1,2 @@
- Migratie naar Flutter Material 3 voltooid;
- Spraakopnamescherm opnieuw ontworpen.

View File

@@ -0,0 +1,7 @@
* Expose the debug menu by tapping the Moxxy icon on the about page 10 times
* Maybe fix a connection race condition
* Allow sharing media with the app when it was closed
* Make quotes prettier
* Make the bottom part of the conversation page prettier
* Fix roster fetching
* Fix OMEMO key generation

View File

@@ -0,0 +1,24 @@
Moxxy is een experimentele xmpp-client met als doel modern gebruiksgemak.
Let op: Moxxy is momenteel in de alfafase. Dit houdt in dat er gegarandeerd bugs en
problemen zullen zijn. Gebruik Moxxy dus niet voor belangrijke zaken.
Huidige functies:
<ul>
<li>Verstuur bestanden en afbeeldingen;</li>
<li>Stel je profielfoto in;</li>
<li>Typmeldingen en berichtstatussen;</li>
<li>Gespreksachtergronden;</li>
<li>Draait op de achtergrond zónder pushmeldingen;</li>
<li>OMEMO (momenteel niet compatibel met de meeste apps);</li>
<li>Stickers.</li>
</ul>
Voor de beste gebruikservaring is het belangrijk om een server te gebruiken met:
<ul>
<li>Ondersteuning voor TLS/StartTLS op dezelfde domeinnaam als in de Jid;</li>
<li>Ondersteuning voor SCRAM-SHA-1, SCRAM-SHA-256 of SCRAM-SHA-512;</li>
<li>Ondersteuning voor HTTP-bestandsupload;</li>
<li>Ondersteuning voor streambeheer;</li>
<li>Ondersteuning voor Client State Indication.</li>
</ul>

View File

@@ -0,0 +1 @@
Moxxy is een experimentele xmpp-client met als doel modern gebruiksgemak.

View File

@@ -0,0 +1 @@
Moxxy

105
flake.lock generated
View File

@@ -1,12 +1,52 @@
{
"nodes": {
"flake-utils": {
"bab": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1667395993,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"lastModified": 1727973209,
"narHash": "sha256-5/AQcfdrVXDjvncklSEeZ1NtZYpmC9WvSL0Uc9dZZ6I=",
"ref": "refs/heads/master",
"rev": "affa71f3b8c1de07c3e9dfa46a988c46c4f1b967",
"revCount": 8,
"type": "git",
"url": "https://codeberg.org/PapaTutuWawa/bits-and-bytes.git"
},
"original": {
"type": "git",
"url": "https://codeberg.org/PapaTutuWawa/bits-and-bytes.git"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1689068808,
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1692799911,
"narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44",
"type": "github"
},
"original": {
@@ -17,11 +57,27 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1669165918,
"narHash": "sha256-hIVruk2+0wmw/Kfzy11rG3q7ev3VTi/IKVODeHcVjFo=",
"lastModified": 1689935543,
"narHash": "sha256-6GQ9ib4dA/r1leC5VUpsBo0BmDvNxLjKrX1iyL+h8mc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3b400a525d92e4085e46141ff48cbf89fd89739e",
"rev": "e43e2448161c0a2c4928abec4e16eae1516571bc",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1727586919,
"narHash": "sha256-e/YXG0tO5GWHDS8QQauj8aj4HhXEm602q9swrrlTlKQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2dcd9c55e8914017226f5948ac22c53872a13ee2",
"type": "github"
},
"original": {
@@ -33,8 +89,39 @@
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
"bab": "bab",
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_2"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},

View File

@@ -3,16 +3,24 @@
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils";
bab.url = "git+https://codeberg.org/PapaTutuWawa/bits-and-bytes.git";
};
outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let
outputs = { self, nixpkgs, flake-utils, bab }: flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {
inherit system;
config = {
android_sdk.accept_license = true;
allowUnfree = true;
# Fix to allow building the NDK package
# TODO: Remove once https://github.com/tadfisher/android-nixpkgs/issues/62 is resolved
# permittedInsecurePackages = [
# "python-2.7.18.6"
# "openssl-1.1.1w"
# ];
};
};
# Everything to make Flutter happy
android = pkgs.androidenv.composeAndroidPackages {
# TODO: Find a way to pin these
#toolsVersion = "26.1.1";
@@ -20,16 +28,22 @@
#buildToolsVersions = [ "31.0.0" ];
#includeEmulator = true;
#emulatorVersion = "30.6.3";
platformVersions = [ "28" ];
cmakeVersions = [ "3.18.1" ];
platformVersions = [ "30" "31" "32" "33" "34" ];
ndkVersions = [ "21.4.7075529" "23.1.7779620" ];
buildToolsVersions = [ "30.0.3" "33.0.2" "34.0.0" ];
includeSources = false;
includeSystemImages = true;
includeSystemImages = false;
systemImageTypes = [ "default" ];
abiVersions = [ "x86_64" ];
includeNDK = false;
abiVersions = [ "x86_64" "arm6" ];
includeNDK = true;
useGoogleAPIs = false;
useGoogleTVAddOns = false;
};
pinnedJDK = pkgs.jdk;
lib = pkgs.lib;
babPkgs = bab.packages."${system}";
pinnedJDK = pkgs.jdk17;
flutterVersion = pkgs.flutter;
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
requests pyyaml # For the build scripts
@@ -38,13 +52,58 @@
in {
devShell = pkgs.mkShell {
buildInputs = with pkgs; [
flutter pinnedJDK android.platform-tools dart scrcpy # Flutter/Android
pythonEnv gnumake # Build scripts
gitlint jq # Code hygiene
ripgrep # General utilities
# Android
pinnedJDK android.platform-tools ktlint
scrcpy
# Flutter
flutterVersion
# Build scripts
pythonEnv gnumake
# Code hygiene
gitlint jq ripgrep
];
ANDROID_SDK_ROOT = "${android.androidsdk}/libexec/android-sdk";
ANDROID_HOME = "${android.androidsdk}/libexec/android-sdk";
JAVA_HOME = pinnedJDK;
# Fix an issue with Flutter using an older version of aapt2, which does not know
# an used parameter.
GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${android.androidsdk}/libexec/android-sdk/build-tools/34.0.0/aapt2";
};
apps = let
providerArg = pkgs.writeText "provider-arg.cfg" ''
name = OpenSC-PKCS11
description = SunPKCS11 via OpenSC
library = ${pkgs.opensc}/lib/opensc-pkcs11.so
slotListIndex = 0
'';
mkBuildScript = skipBuild: pkgs.writeShellScript "build-moxxy.sh" ''
${babPkgs.flutter-build}/bin/flutter-build \
--name Moxxy \
--not-signed \
--zipalign ${android.androidsdk}/libexec/android-sdk/build-tools/34.0.0/zipalign \
--apksigner ${android.androidsdk}/libexec/android-sdk/build-tools/34.0.0/apksigner \
--pigeon ./pigeon/quirks.dart \
--flutter ${flutterVersion}/bin/flutter \
--provider-config ${providerArg} ${lib.optionalString skipBuild "--skip-build"}
'';
in {
# Skip the build and just sign
onlySign = {
type = "app";
program = "${mkBuildScript true}";
};
# Build everything and sign
build = {
type = "app";
program = "${mkBuildScript false}";
};
};
});
}

View File

@@ -1,67 +0,0 @@
import 'package:get_it/get_it.dart';
import 'package:logging/logging.dart';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxmpp_socket_tcp/moxxmpp_socket_tcp.dart';
import 'package:moxxyv2/service/connectivity.dart';
import 'package:moxxyv2/service/moxxmpp/reconnect.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:test/test.dart';
class StubConnectivityService extends ConnectivityService {
StubConnectivityService() : super();
@override
ConnectivityResult get currentState => ConnectivityResult.wifi;
}
void main() {
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((record) {
print('${record.level.name}: ${record.time}: ${record.message}');
});
final log = Logger('FailureReconnectionTest');
GetIt.I.registerSingleton<ConnectivityService>(StubConnectivityService());
test('Failing an awaited connection with MoxxyReconnectionPolicy', () async {
var errors = 0;
final connection = XmppConnection(
MoxxyReconnectionPolicy(maxBackoffTime: 1),
TCPSocketWrapper(false),
);
connection.registerFeatureNegotiators([
StartTlsNegotiator(),
]);
connection.registerManagers([
DiscoManager(),
RosterManager(),
PingManager(),
MessageManager(),
PresenceManager('http://moxxmpp.example'),
]);
connection.asBroadcastStream().listen((event) {
if (event is ConnectionStateChangedEvent) {
if (event.state == XmppConnectionState.error) {
errors++;
}
}
});
connection.setConnectionSettings(
ConnectionSettings(
jid: JID.fromString('testuser@no-sasl.badxmpp.eu'),
password: 'abc123',
useDirectTLS: true,
allowPlainAuth: true,
),
);
final result = await connection.connectAwaitable();
log.info('Connection failed as expected');
expect(result.success, false);
expect(errors, 1);
log.info('Waiting 20 seconds for unexpected reconnections');
await Future.delayed(const Duration(seconds: 20));
expect(errors, 1);
}, timeout: Timeout.factor(2));
}

View File

@@ -22,7 +22,8 @@ files:
- JsonImplementation
attributes:
state: String
permissionsToRequest: List<int>
requestNotificationPermission: bool
excludeFromBatteryOptimisation: bool
preferences:
type: PreferencesState
deserialise: true
@@ -36,15 +37,6 @@ files:
roster:
type: List<RosterItem>?
deserialise: true
# Returned by [GetMessagesForJidCommand]
- name: MessagesResultEvent
extends: BackgroundEvent
implements:
- JsonImplementation
attributes:
messages:
type: List<Message>
deserialise: true
# Triggered if a conversation has been added.
# Also returned by [AddConversationCommand]
- name: ConversationAddedEvent
@@ -71,7 +63,7 @@ files:
extends: BackgroundEvent
implements:
- JsonImplementation
# Send by the service if a message has been received or returned by # [SendMessageCommand].
# Send by the service if a message has been received or returned by [SendMessageCommand].
- name: MessageAddedEvent
extends: BackgroundEvent
implements:
@@ -103,13 +95,20 @@ files:
extends: BackgroundEvent
implements:
- JsonImplementation
# Triggered in response to a [GetBlocklistCommand]
- name: GetBlocklistResultEvent
extends: BackgroundEvent
implements:
- JsonImplementation
attributes:
entries: List<String>
# Triggered by DownloadService or UploadService.
- name: ProgressEvent
extends: BackgroundEvent
implements:
- JsonImplementation
attributes:
id: int
id: String
progress: double?
# Triggered by [RosterService] if we receive a roster push.
- name: RosterDiffEvent
@@ -163,6 +162,7 @@ files:
supportsCsi: bool
supportsUserBlocking: bool
supportsHttpFileUpload: bool
supportsCarbons: bool
# Returned by [SignOutCommand]
- name: SignedOutEvent
extends: BackgroundEvent
@@ -199,6 +199,178 @@ files:
device:
type: OmemoDevice
deserialise: true
- name: MessageNotificationTappedEvent
extends: BackgroundEvent
implements:
- JsonImplementation
attributes:
conversationJid: String
title: String
avatarPath: String
- name: StickerPackImportSuccessEvent
extends: BackgroundEvent
implements:
- JsonImplementation
attributes:
stickerPack:
type: StickerPack
deserialise: true
- name: StickerPackImportFailureEvent
extends: BackgroundEvent
implements:
- JsonImplementation
attributes:
- name: FetchStickerPackSuccessResult
extends: BackgroundEvent
implements:
- JsonImplementation
attributes:
stickerPack:
type: StickerPack
deserialise: true
- name: FetchStickerPackFailureResult
extends: BackgroundEvent
implements:
- JsonImplementation
attributes:
- name: StickerPackInstallSuccessEvent
extends: BackgroundEvent
implements:
- JsonImplementation
attributes:
stickerPack:
type: StickerPack
deserialise: true
- name: StickerPackInstallFailureEvent
extends: BackgroundEvent
implements:
- JsonImplementation
attributes:
- name: StickerPackAddedEvent
extends: BackgroundEvent
implements:
- JsonImplementation
attributes:
stickerPack:
type: StickerPack
deserialise: true
# Returned by [GetPagedMessagesCommand]
- name: PagedMessagesResultEvent
extends: BackgroundEvent
implements:
- JsonImplementation
attributes:
messages:
type: List<Message>
deserialise: true
# Returned by [GetReactionsForMessageCommand]
- name: ReactionsForMessageResult
extends: BackgroundEvent
implements:
- JsonImplementation
attributes:
reactions:
type: List<ReactionGroup>
deserialise: true
# Triggered when the stream negotiations have been completed
- name: StreamNegotiationsCompletedEvent
extends: BackgroundEvent
implements:
- JsonImplementation
attributes:
resumed: bool
- name: AvatarUpdatedEvent
extends: BackgroundEvent
implements:
- JsonImplementation
attributes:
jid: String
path: String
# Returned when attempting to start a chat with a groupchat
- name: JidIsGroupchatEvent
extends: BackgroundEvent
implements:
- JsonImplementation
attributes:
jid: String
# Returned when an error occured
- name: ErrorEvent
extends: BackgroundEvent
implements:
- JsonImplementation
attributes:
errorId: int
# Triggered by the service in response to an [JoinGroupchatCommand].
- name: JoinGroupchatResult
extends: BackgroundEvent
implements:
- JsonImplementation
attributes:
conversation:
type: Conversation
deserialise: true
# Returned after a [GetStorageUsageCommand]
- name: GetStorageUsageEvent
extends: BackgroundEvent
implements:
- JsonImplementation
attributes:
# The used storage in bytes for media files
mediaUsage: int
# The used storage in bytes for stickers
stickerUsage: int
# Returned after [DeleteOldMediaFilesCommand]
- name: DeleteOldMediaFilesDoneEvent
extends: BackgroundEvent
implements:
- JsonImplementation
attributes:
# The used storage in bytes after the deletion operation is done
newUsage: int
# The new list of Conversations
conversations:
type: List<Conversation>
deserialize: true
- name: PagedStickerPackResult
extends: BackgroundEvent
implements:
- JsonImplementation
attributes:
stickerPacks:
type: List<StickerPack>
deserialise: true
- name: GetStickerPackByIdResult
extends: BackgroundEvent
implements:
- JsonImplementation
attributes:
stickerPack:
type: StickerPack?
deserialise: true
- name: FetchRecipientInformationResult
extends: BackgroundEvent
implements:
- JsonImplementation
attributes:
items:
type: List<SendFilesRecipient>
deserialise: true
- name: GroupchatMembersResult
extends: BackgroundEvent
implements:
- JsonImplementation
attributes:
members:
type: List<GroupchatMember>
deserialise: true
- name: ConversationSearchResult
extends: BackgroundEvent
implements:
- JsonImplementation
attributes:
results:
type: List<Conversation>
deserialise: true
generate_builder: true
builder_name: "Event"
builder_baseclass: "BackgroundEvent"
@@ -226,14 +398,9 @@ files:
attributes:
title: String
lastMessageBody: String
avatarUrl: String
jid: String
- name: GetMessagesForJidCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
avatarUrl: String?
jid: String
conversationType: String
- name: SetOpenConversationCommand
extends: BackgroundCommand
implements:
@@ -251,6 +418,8 @@ files:
quotedMessage:
type: Message?
deserialise: true
editSid: String?
currentConversationJid: String?
- name: SendFilesCommand
extends: BackgroundCommand
implements:
@@ -294,6 +463,12 @@ files:
- JsonImplementation
attributes:
jid: String
- name: RemoveContactCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
jid: String
- name: RequestDownloadCommand
extends: BackgroundCommand
implements:
@@ -322,6 +497,13 @@ files:
- JsonImplementation
attributes:
jid: String
accountJid: String
- name: ExitConversationCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
conversationType: String
- name: SendChatStateCommand
extends: BackgroundCommand
implements:
@@ -329,6 +511,7 @@ files:
attributes:
state: String
jid: String
conversationType: String
- name: GetFeaturesCommand
extends: BackgroundCommand
implements:
@@ -387,13 +570,187 @@ files:
implements:
- JsonImplementation
attributes:
- name: RetractMessageComment
- name: RetractMessageCommentCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
originId: String
conversationJid: String
- name: MarkConversationAsReadCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
conversationJid: String
- name: MarkMessageAsReadCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
id: String
sendMarker: bool
- name: AddReactionToMessageCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
id: String
emoji: String
- name: RemoveReactionFromMessageCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
id: String
emoji: String
- name: MarkOmemoDeviceAsVerifiedCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
deviceId: int
jid: String
- name: ImportStickerPackCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
path: String
- name: RemoveStickerPackCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
stickerPackId: String
- name: SendStickerCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
sticker:
type: Sticker
deserialise: true
recipient: String
quotes:
type: Message?
deserialise: true
- name: FetchStickerPackCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
stickerPackId: String
jid: String
- name: InstallStickerPackCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
stickerPack:
type: StickerPack
deserialise: true
- name: GetBlocklistCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
- name: GetPagedMessagesCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
conversationJid: String
olderThan: bool
timestamp: int?
- name: GetPagedSharedMediaCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
conversationJid: String?
olderThan: bool
timestamp: int?
- name: GetReactionsForMessageCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
id: String
- name: RequestAvatarForJidCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
jid: String
hash: String?
ownAvatar: bool
isGroupchat: bool
- name: GetStorageUsageCommand
extends: BackgroundCommand
implements:
- JsonImplementation
- name: DeleteOldMediaFilesCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
# Milliseconds from now in the past; The maximum age of a file to not
# get deleted.
timeOffset: int
- name: GetPagedStickerPackCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
olderThan: bool
timestamp: int?
includeStickers: bool
- name: GetStickerPackByIdCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
id: String
- name: DebugCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
id: int
- name: JoinGroupchatCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
jid: String
nick: String
- name: FetchRecipientInformationCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
jids: List<String>
- name: GetMembersForGroupchatCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
jid: String
- name: PerformConversationSearch
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
text: String
- name: ConversationSetFavourite
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
jid: String
accountJid: String
state: bool
generate_builder: true
# get${builder_Name}FromJson
builder_name: "Command"

View File

@@ -1,49 +1,35 @@
import 'dart:async';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get_it/get_it.dart';
import 'package:logging/logging.dart';
import 'package:moxplatform/moxplatform.dart';
import 'package:moxxy_native/moxxy_native.dart';
import 'package:moxxyv2/i18n/strings.g.dart';
import 'package:moxxyv2/service/service.dart';
import 'package:moxxyv2/shared/commands.dart';
import 'package:moxxyv2/ui/bloc/addcontact_bloc.dart';
import 'package:moxxyv2/ui/bloc/blocklist_bloc.dart';
import 'package:moxxyv2/ui/bloc/conversation_bloc.dart';
import 'package:moxxyv2/ui/bloc/conversations_bloc.dart';
import 'package:moxxyv2/ui/bloc/crop_bloc.dart';
import 'package:moxxyv2/ui/bloc/cropbackground_bloc.dart';
import 'package:moxxyv2/ui/bloc/devices_bloc.dart';
import 'package:moxxyv2/ui/bloc/login_bloc.dart';
import 'package:moxxyv2/ui/bloc/navigation_bloc.dart';
import 'package:moxxyv2/ui/bloc/newconversation_bloc.dart';
import 'package:moxxyv2/ui/bloc/own_devices_bloc.dart';
import 'package:moxxyv2/ui/bloc/preferences_bloc.dart';
import 'package:moxxyv2/ui/bloc/profile_bloc.dart';
import 'package:moxxyv2/ui/bloc/sendfiles_bloc.dart';
import 'package:moxxyv2/ui/bloc/server_info_bloc.dart';
import 'package:moxxyv2/ui/bloc/share_selection_bloc.dart';
import 'package:moxxyv2/ui/bloc/sharedmedia_bloc.dart';
import 'package:moxxyv2/shared/synchronized_queue.dart';
import 'package:moxxyv2/ui/constants.dart';
import 'package:moxxyv2/ui/controller/conversation_controller.dart';
import 'package:moxxyv2/ui/events.dart';
/*
import "package:moxxyv2/ui/pages/register/register.dart";
import "package:moxxyv2/ui/pages/postregister/postregister.dart";
*/
import 'package:moxxyv2/ui/pages/addcontact.dart';
import 'package:moxxyv2/ui/pages/blocklist.dart';
import 'package:moxxyv2/ui/pages/conversation/conversation.dart';
import 'package:moxxyv2/ui/pages/conversations.dart';
import 'package:moxxyv2/ui/pages/crop.dart';
import 'package:moxxyv2/ui/pages/home/home.dart';
import 'package:moxxyv2/ui/pages/intro.dart';
import 'package:moxxyv2/ui/pages/login.dart';
import 'package:moxxyv2/ui/pages/newconversation.dart';
import 'package:moxxyv2/ui/pages/profile/devices.dart';
import 'package:moxxyv2/ui/pages/profile/own_devices.dart';
import 'package:moxxyv2/ui/pages/profile/profile.dart';
import 'package:moxxyv2/ui/pages/sendfiles.dart';
import 'package:moxxyv2/ui/pages/sendfiles/sendfiles.dart';
import 'package:moxxyv2/ui/pages/server_info.dart';
import 'package:moxxyv2/ui/pages/settings/about.dart';
import 'package:moxxyv2/ui/pages/settings/appearance/appearance.dart';
@@ -54,53 +40,94 @@ import 'package:moxxyv2/ui/pages/settings/licenses.dart';
import 'package:moxxyv2/ui/pages/settings/network.dart';
import 'package:moxxyv2/ui/pages/settings/privacy/privacy.dart';
import 'package:moxxyv2/ui/pages/settings/settings.dart';
import 'package:moxxyv2/ui/pages/settings/sticker_packs.dart';
import 'package:moxxyv2/ui/pages/settings/stickers.dart';
import 'package:moxxyv2/ui/pages/settings/storage/shared_media.dart';
import 'package:moxxyv2/ui/pages/settings/storage/storage.dart';
import 'package:moxxyv2/ui/pages/share_selection.dart';
import 'package:moxxyv2/ui/pages/sharedmedia.dart';
//import 'package:moxxyv2/ui/pages/sharedmedia.dart';
import 'package:moxxyv2/ui/pages/splashscreen/splashscreen.dart';
import 'package:moxxyv2/ui/service/data.dart';
import 'package:moxxyv2/ui/pages/startchat.dart';
import 'package:moxxyv2/ui/pages/startgroupchat.dart';
import 'package:moxxyv2/ui/pages/sticker_pack.dart';
import 'package:moxxyv2/ui/pages/util/qrcode.dart';
import 'package:moxxyv2/ui/service/avatars.dart';
import 'package:moxxyv2/ui/service/connectivity.dart';
import 'package:moxxyv2/ui/service/progress.dart';
import 'package:moxxyv2/ui/service/read.dart';
import 'package:moxxyv2/ui/service/sharing.dart';
import 'package:moxxyv2/ui/state/account.dart';
import 'package:moxxyv2/ui/state/blocklist.dart';
import 'package:moxxyv2/ui/state/conversation.dart';
import 'package:moxxyv2/ui/state/conversations.dart';
import 'package:moxxyv2/ui/state/crop.dart';
import 'package:moxxyv2/ui/state/cropbackground.dart';
import 'package:moxxyv2/ui/state/devices.dart';
import 'package:moxxyv2/ui/state/groupchat/joingroupchat.dart';
import 'package:moxxyv2/ui/state/login.dart';
import 'package:moxxyv2/ui/state/navigation.dart';
import 'package:moxxyv2/ui/state/newconversation.dart';
import 'package:moxxyv2/ui/state/own_devices.dart';
import 'package:moxxyv2/ui/state/preferences.dart';
import 'package:moxxyv2/ui/state/profile.dart';
import 'package:moxxyv2/ui/state/request.dart';
import 'package:moxxyv2/ui/state/sendfiles.dart';
import 'package:moxxyv2/ui/state/server_info.dart';
import 'package:moxxyv2/ui/state/share_selection.dart';
import 'package:moxxyv2/ui/state/startchat.dart';
import 'package:moxxyv2/ui/state/sticker_pack.dart';
import 'package:moxxyv2/ui/state/stickers.dart';
import 'package:moxxyv2/ui/theme.dart';
import 'package:page_transition/page_transition.dart';
import 'package:share_handler/share_handler.dart';
void setupLogging() {
Logger.root.level = kDebugMode ? Level.ALL : Level.INFO;
Logger.root.onRecord.listen((record) {
// ignore: avoid_print
print('[${record.level.name}] (${record.loggerName}) ${record.time}: ${record.message}');
print(
'[${record.level.name}] (${record.loggerName}) ${record.time}: ${record.message}',
);
});
GetIt.I.registerSingleton<Logger>(Logger('MoxxyMain'));
}
Future<void> setupUIServices() async {
GetIt.I.registerSingleton<UIProgressService>(UIProgressService());
GetIt.I.registerSingleton<UIDataService>(UIDataService());
GetIt.I.registerSingleton<UIAvatarsService>(UIAvatarsService());
GetIt.I.registerSingleton<UISharingService>(UISharingService());
GetIt.I.registerSingleton<UIConnectivityService>(UIConnectivityService());
GetIt.I.registerSingleton<UIReadMarkerService>(UIReadMarkerService());
/// Initialize services
await GetIt.I.get<UIConnectivityService>().initialize();
}
void setupBlocs(GlobalKey<NavigatorState> navKey) {
GetIt.I.registerSingleton<NavigationBloc>(NavigationBloc(navigationKey: navKey));
GetIt.I.registerSingleton<ConversationsBloc>(ConversationsBloc());
GetIt.I.registerSingleton<NewConversationBloc>(NewConversationBloc());
GetIt.I.registerSingleton<ConversationBloc>(ConversationBloc());
GetIt.I.registerSingleton<BlocklistBloc>(BlocklistBloc()); GetIt.I.registerSingleton<ProfileBloc>(ProfileBloc());
GetIt.I.registerSingleton<PreferencesBloc>(PreferencesBloc());
GetIt.I.registerSingleton<AddContactBloc>(AddContactBloc());
GetIt.I.registerSingleton<SharedMediaBloc>(SharedMediaBloc());
GetIt.I.registerSingleton<CropBloc>(CropBloc());
GetIt.I.registerSingleton<SendFilesBloc>(SendFilesBloc());
GetIt.I.registerSingleton<CropBackgroundBloc>(CropBackgroundBloc());
GetIt.I.registerSingleton<ShareSelectionBloc>(ShareSelectionBloc());
GetIt.I.registerSingleton<ServerInfoBloc>(ServerInfoBloc());
GetIt.I.registerSingleton<DevicesBloc>(DevicesBloc());
GetIt.I.registerSingleton<OwnDevicesBloc>(OwnDevicesBloc());
GetIt.I.registerSingleton<Navigation>(
Navigation(navigationKey: navKey),
);
GetIt.I.registerSingleton<NewConversationCubit>(NewConversationCubit());
GetIt.I.registerSingleton<ConversationCubit>(ConversationCubit());
GetIt.I.registerSingleton<BlocklistCubit>(BlocklistCubit());
GetIt.I.registerSingleton<ProfileCubit>(ProfileCubit());
GetIt.I.registerSingleton<PreferencesCubit>(PreferencesCubit());
GetIt.I.registerSingleton<StartChatCubit>(StartChatCubit());
GetIt.I.registerSingleton<CropCubit>(CropCubit());
GetIt.I.registerSingleton<SendFilesCubit>(SendFilesCubit());
GetIt.I.registerSingleton<CropBackgroundCubit>(CropBackgroundCubit());
GetIt.I.registerSingleton<ShareSelectionCubit>(ShareSelectionCubit());
GetIt.I.registerSingleton<ServerInfoCubit>(ServerInfoCubit());
GetIt.I.registerSingleton<DevicesCubit>(DevicesCubit());
GetIt.I.registerSingleton<OwnDevicesCubit>(OwnDevicesCubit());
GetIt.I.registerSingleton<StickersCubit>(StickersCubit());
GetIt.I.registerSingleton<StickerPackCubit>(StickerPackCubit());
GetIt.I.registerSingleton<RequestCubit>(RequestCubit());
GetIt.I.registerSingleton<JoinGroupchatCubit>(JoinGroupchatCubit());
GetIt.I.registerSingleton<ConversationsCubit>(ConversationsCubit());
GetIt.I.registerSingleton<AccountCubit>(AccountCubit());
}
// TODO(Unknown): Replace all Column(children: [ Padding(), Padding, ...]) with a
// Padding(padding: ..., child: Column(children: [ ... ]))
// TODO(Unknown): Theme the switches
void main() async {
GetIt.I.registerSingleton<Completer<void>>(Completer());
setupLogging();
await setupUIServices();
@@ -111,59 +138,70 @@ void main() async {
await initializeServiceIfNeeded();
imageCache.maximumSizeBytes = 500 * 1024 * 1024;
runApp(
MultiBlocProvider(
providers: [
BlocProvider<NavigationBloc>(
create: (_) => GetIt.I.get<NavigationBloc>(),
BlocProvider<LoginCubit>(
create: (_) => LoginCubit(),
),
BlocProvider<LoginBloc>(
create: (_) => LoginBloc(),
BlocProvider<ConversationsCubit>(
create: (_) => GetIt.I.get<ConversationsCubit>(),
),
BlocProvider<ConversationsBloc>(
create: (_) => GetIt.I.get<ConversationsBloc>(),
BlocProvider<NewConversationCubit>(
create: (_) => GetIt.I.get<NewConversationCubit>(),
),
BlocProvider<NewConversationBloc>(
create: (_) => GetIt.I.get<NewConversationBloc>(),
BlocProvider<ConversationCubit>(
create: (_) => GetIt.I.get<ConversationCubit>(),
),
BlocProvider<ConversationBloc>(
create: (_) => GetIt.I.get<ConversationBloc>(),
BlocProvider<BlocklistCubit>(
create: (_) => GetIt.I.get<BlocklistCubit>(),
),
BlocProvider<BlocklistBloc>(
create: (_) => GetIt.I.get<BlocklistBloc>(),
BlocProvider<ProfileCubit>(
create: (_) => GetIt.I.get<ProfileCubit>(),
),
BlocProvider<ProfileBloc>(
create: (_) => GetIt.I.get<ProfileBloc>(),
BlocProvider<PreferencesCubit>(
create: (_) => GetIt.I.get<PreferencesCubit>(),
),
BlocProvider<PreferencesBloc>(
create: (_) => GetIt.I.get<PreferencesBloc>(),
BlocProvider<StartChatCubit>(
create: (_) => GetIt.I.get<StartChatCubit>(),
),
BlocProvider<AddContactBloc>(
create: (_) => GetIt.I.get<AddContactBloc>(),
BlocProvider<CropCubit>(
create: (_) => GetIt.I.get<CropCubit>(),
),
BlocProvider<SharedMediaBloc>(
create: (_) => GetIt.I.get<SharedMediaBloc>(),
BlocProvider<SendFilesCubit>(
create: (_) => GetIt.I.get<SendFilesCubit>(),
),
BlocProvider<CropBloc>(
create: (_) => GetIt.I.get<CropBloc>(),
BlocProvider<CropBackgroundCubit>(
create: (_) => GetIt.I.get<CropBackgroundCubit>(),
),
BlocProvider<SendFilesBloc>(
create: (_) => GetIt.I.get<SendFilesBloc>(),
BlocProvider<ShareSelectionCubit>(
create: (_) => GetIt.I.get<ShareSelectionCubit>(),
),
BlocProvider<CropBackgroundBloc>(
create: (_) => GetIt.I.get<CropBackgroundBloc>(),
BlocProvider<ServerInfoCubit>(
create: (_) => GetIt.I.get<ServerInfoCubit>(),
),
BlocProvider<ShareSelectionBloc>(
create: (_) => GetIt.I.get<ShareSelectionBloc>(),
BlocProvider<DevicesCubit>(
create: (_) => GetIt.I.get<DevicesCubit>(),
),
BlocProvider<ServerInfoBloc>(
create: (_) => GetIt.I.get<ServerInfoBloc>(),
BlocProvider<OwnDevicesCubit>(
create: (_) => GetIt.I.get<OwnDevicesCubit>(),
),
BlocProvider<DevicesBloc>(
create: (_) => GetIt.I.get<DevicesBloc>(),
BlocProvider<StickersCubit>(
create: (_) => GetIt.I.get<StickersCubit>(),
),
BlocProvider<OwnDevicesBloc>(
create: (_) => GetIt.I.get<OwnDevicesBloc>(),
BlocProvider<StickerPackCubit>(
create: (_) => GetIt.I.get<StickerPackCubit>(),
),
BlocProvider<RequestCubit>(
create: (_) => GetIt.I.get<RequestCubit>(),
),
BlocProvider<JoinGroupchatCubit>(
create: (_) => GetIt.I.get<JoinGroupchatCubit>(),
),
BlocProvider<AccountCubit>(
create: (_) => GetIt.I.get<AccountCubit>(),
),
],
child: TranslationProvider(
@@ -174,8 +212,7 @@ void main() async {
}
class MyApp extends StatefulWidget {
const MyApp(this.navigationKey, { super.key });
const MyApp(this.navigationKey, {super.key});
final GlobalKey<NavigatorState> navigationKey;
@override
@@ -188,46 +225,20 @@ class MyAppState extends State<MyApp> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
_initState();
}
/// Async "version" of initState()
Future<void> _initState() async {
WidgetsBinding.instance.addObserver(this);
// Set up receiving share intents
await GetIt.I.get<UISharingService>().initialize();
// Lift the UI block
GetIt.I.get<Completer<void>>().complete();
_setupSharingHandler();
}
Future<void> _handleSharedMedia(SharedMedia media) async {
final attachments = media.attachments ?? [];
GetIt.I.get<ShareSelectionBloc>().add(
ShareSelectionRequestedEvent(
attachments.map((a) => a!.path).toList(),
media.content,
media.content != null ? ShareSelectionType.text : ShareSelectionType.media,
),
);
}
Future<void> _setupSharingHandler() async {
final handler = ShareHandlerPlatform.instance;
final media = await handler.getInitialSharedMedia();
// Shared while the app was closed
if (media != null) {
if (GetIt.I.get<UIDataService>().isLoggedIn) {
await _handleSharedMedia(media);
}
await handler.resetInitialSharedMedia();
}
// Shared while the app is stil running
handler.sharedMediaStream.listen((SharedMedia media) async {
if (GetIt.I.get<UIDataService>().isLoggedIn) {
await _handleSharedMedia(media);
}
await handler.resetInitialSharedMedia();
});
await GetIt.I
.get<SynchronizedQueue<Map<String, dynamic>?>>()
.removeQueueLock();
}
@override
@@ -240,71 +251,148 @@ class MyAppState extends State<MyApp> with WidgetsBindingObserver {
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
final sender = MoxplatformPlugin.handler.getDataSender();
final foreground = getForegroundService();
switch (state) {
case AppLifecycleState.hidden:
case AppLifecycleState.paused:
sender.sendData(
foreground.send(
SetCSIStateCommand(active: false),
);
GetIt.I.get<ConversationBloc>().add(AppStateChanged(false));
break;
BidirectionalConversationController.currentController
?.handleAppStateChange(false);
case AppLifecycleState.resumed:
sender.sendData(
foreground.send(
SetCSIStateCommand(active: true),
);
GetIt.I.get<ConversationBloc>().add(AppStateChanged(true));
break;
BidirectionalConversationController.currentController
?.handleAppStateChange(true);
case AppLifecycleState.detached:
case AppLifecycleState.inactive:
break;
break;
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
locale: TranslationProvider.of(context).flutterLocale,
supportedLocales: LocaleSettings.supportedLocales,
localizationsDelegates: GlobalMaterialLocalizations.delegates,
title: 'Moxxy',
theme: getThemeData(context, Brightness.light),
darkTheme: getThemeData(context, Brightness.dark),
navigatorKey: widget.navigationKey,
onGenerateRoute: (settings) {
switch (settings.name) {
case introRoute: return Intro.route;
case loginRoute: return Login.route;
case conversationsRoute: return ConversationsPage.route;
case newConversationRoute: return NewConversationPage.route;
case conversationRoute: return PageTransition<dynamic>(
type: PageTransitionType.rightToLeft,
settings: settings,
child: const ConversationPage(),
);
case sharedMediaRoute: return SharedMediaPage.route;
case blocklistRoute: return BlocklistPage.route;
case profileRoute: return ProfilePage.route;
case settingsRoute: return SettingsPage.route;
case aboutRoute: return SettingsAboutPage.route;
case licensesRoute: return SettingsLicensesPage.route;
case networkRoute: return NetworkPage.route;
case privacyRoute: return PrivacyPage.route;
case debuggingRoute: return DebuggingPage.route;
case addContactRoute: return AddContactPage.route;
case cropRoute: return CropPage.route;
case sendFilesRoute: return SendFilesPage.route;
case backgroundCroppingRoute: return CropBackgroundPage.route;
case shareSelectionRoute: return ShareSelectionPage.route;
case serverInfoRoute: return ServerInfoPage.route;
case conversationSettingsRoute: return ConversationSettingsPage.route;
case devicesRoute: return DevicesPage.route;
case ownDevicesRoute: return OwnDevicesPage.route;
case appearanceRoute: return AppearanceSettingsPage.route;
}
return DynamicColorBuilder(
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
final light = lightDynamic?.harmonized() ??
ColorScheme.fromSeed(seedColor: primaryColor);
final dark = darkDynamic?.harmonized() ??
ColorScheme.fromSeed(
seedColor: primaryColor,
brightness: Brightness.dark,
);
return null;
return MaterialApp(
locale: TranslationProvider.of(context).flutterLocale,
supportedLocales: AppLocaleUtils.supportedLocales,
localizationsDelegates: GlobalMaterialLocalizations.delegates,
title: 'Moxxy',
theme: ThemeData(
colorScheme: light,
extensions: [
getMoxxyThemeData(Brightness.light),
],
),
darkTheme: ThemeData(
colorScheme: dark,
extensions: [
getMoxxyThemeData(Brightness.dark),
],
),
navigatorKey: widget.navigationKey,
onGenerateRoute: (settings) {
switch (settings.name) {
case introRoute:
return Intro.route;
case loginRoute:
return Login.route;
case homeRoute:
return ConversationsPage.route;
case newConversationRoute:
return NewConversationPage.route;
case conversationRoute:
final args = settings.arguments! as ConversationPageArguments;
return PageTransition<dynamic>(
type: PageTransitionType.rightToLeft,
settings: settings,
child: ConversationPage(
conversationJid: args.conversationJid,
initialText: args.initialText,
conversationType: args.type,
),
);
// case sharedMediaRoute:
// return SharedMediaPage.getRoute(
// settings.arguments! as SharedMediaPageArguments,
// );
case blocklistRoute:
return BlocklistPage.route;
case profileRoute:
return ProfilePage.getRoute(
settings.arguments! as ProfileArguments,
);
case settingsRoute:
return PageTransition<dynamic>(
type: PageTransitionType.rightToLeft,
child: const SettingsPage(),
);
case aboutRoute:
return SettingsAboutPage.route;
case licensesRoute:
return SettingsLicensesPage.route;
case networkRoute:
return NetworkPage.route;
case privacyRoute:
return PrivacyPage.route;
case debuggingRoute:
return DebuggingPage.route;
case addContactRoute:
return StartChatPage.route;
case joinGroupchatRoute:
return JoinGroupchatPage.getRoute(
settings.arguments! as JoinGroupchatArguments,
);
case cropRoute:
return CropPage.route;
case sendFilesRoute:
return SendFilesPage.route;
case backgroundCroppingRoute:
return CropBackgroundPage.route;
case shareSelectionRoute:
return ShareSelectionPage.route;
case serverInfoRoute:
return ServerInfoPage.route;
case conversationSettingsRoute:
return ConversationSettingsPage.route;
case devicesRoute:
return DevicesPage.route;
case ownDevicesRoute:
return OwnDevicesPage.route;
case appearanceRoute:
return AppearanceSettingsPage.route;
case qrCodeScannerRoute:
return QrCodeScanningPage.getRoute(
settings.arguments! as QrCodeScanningArguments,
);
case stickersRoute:
return StickersSettingsPage.route;
case stickerPacksRoute:
return StickerPacksSettingsPage.route;
case stickerPackRoute:
return StickerPackPage.route;
case storageSettingsRoute:
return StorageSettingsPage.route;
case storageSharedMediaSettingsRoute:
return StorageSharedMediaPage.route;
}
return null;
},
home: const Splashscreen(),
);
},
home: const Splashscreen(),
);
}
}

View File

@@ -1,216 +1,564 @@
import 'dart:convert';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:cryptography/cryptography.dart';
import 'package:get_it/get_it.dart';
import 'package:hex/hex.dart';
import 'package:image_size_getter/image_size_getter.dart';
import 'package:logging/logging.dart';
import 'package:moxlib/moxlib.dart';
import 'package:meta/meta.dart';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxyv2/service/cache.dart';
import 'package:moxxyv2/service/conversation.dart';
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/service/groupchat.dart';
import 'package:moxxyv2/service/notifications.dart';
import 'package:moxxyv2/service/preferences.dart';
import 'package:moxxyv2/service/roster.dart';
import 'package:moxxyv2/service/service.dart';
import 'package:moxxyv2/service/xmpp.dart';
import 'package:moxxyv2/shared/avatar.dart';
import 'package:moxxyv2/service/xmpp_state.dart';
import 'package:moxxyv2/shared/events.dart';
/// Removes line breaks and spaces from [original]. This might happen when we request the
/// avatar data. Returns the cleaned version.
String _cleanBase64String(String original) {
var ret = original;
for (final char in ['\n', ' ']) {
ret = ret.replaceAll(char, '');
}
return ret;
}
import 'package:moxxyv2/shared/helpers.dart';
import 'package:path/path.dart' as p;
class AvatarService {
final Logger _log = Logger('AvatarService');
AvatarService() : _log = Logger('AvatarService');
final Logger _log;
/// List of JIDs for which we have already requested the avatar in the current stream.
final List<JID> _requestedInStream = [];
UserAvatarManager _getUserAvatarManager() => GetIt.I.get<XmppConnection>().getManagerById<UserAvatarManager>(userAvatarManager)!;
/// Cached version of the path to the avatar cache. Used to prevent constant calls
/// to the native side.
late final String _avatarCacheDir;
DiscoManager _getDiscoManager() => GetIt.I.get<XmppConnection>().getManagerById<DiscoManager>(discoManager)!;
/// Computes the path to use for cached avatars.
static Future<String> getCachePath() async =>
computeCacheDirectoryPath('avatars');
Future<void> updateAvatarForJid(String jid, String hash, String base64) async {
final cs = GetIt.I.get<ConversationService>();
final rs = GetIt.I.get<RosterService>();
final originalConversation = await cs.getConversationByJid(jid);
var saved = false;
// Clean the raw data. Since this may arrive by chunks, those chunks may contain
// weird data pieces.
final base64Data = base64Decode(_cleanBase64String(base64));
if (originalConversation != null) {
final avatarPath = await saveAvatarInCache(
base64Data,
hash,
jid,
originalConversation.avatarUrl,
);
saved = true;
final conv = await cs.updateConversation(
originalConversation.id,
avatarUrl: avatarPath,
);
sendEvent(ConversationUpdatedEvent(conversation: conv));
} else {
_log.warning('Failed to get conversation');
}
final originalRoster = await rs.getRosterItemByJid(jid);
if (originalRoster != null) {
var avatarPath = '';
if (saved) {
avatarPath = await getAvatarPath(jid, hash);
} else {
avatarPath = await saveAvatarInCache(
base64Data,
hash,
jid,
originalRoster.avatarUrl,
);
}
final roster = await rs.updateRosterItem(
originalRoster.id,
avatarUrl: avatarPath,
avatarHash: hash,
);
sendEvent(RosterDiffEvent(modified: [roster]));
}
@visibleForTesting
void initializeForTesting(String cacheDir) {
_avatarCacheDir = cacheDir;
}
Future<void> fetchAndUpdateAvatarForJid(String jid, String oldHash) async {
final response = await _getDiscoManager().discoItemsQuery(jid);
final items = response.isType<DiscoError>() ?
<DiscoItem>[] :
response.get<List<DiscoItem>>();
final itemNodes = items.map((i) => i.node);
_log.finest('Disco items for $jid:');
for (final item in itemNodes) {
_log.finest('- $item');
}
var base64 = '';
var hash = '';
if (listContains<DiscoItem>(items, (item) => item.node == userAvatarDataXmlns)) {
final avatar = _getUserAvatarManager();
final pubsubHash = await avatar.getAvatarId(jid);
// Don't request if we already have the newest avatar
if (pubsubHash == oldHash) return;
// Query via PubSub
final data = await avatar.getUserAvatar(jid);
if (data == null) return;
base64 = data.base64;
hash = data.hash;
} else {
// Query the vCard
final vm = GetIt.I.get<XmppConnection>().getManagerById<VCardManager>(vcardManager)!;
final vcard = await vm.requestVCard(jid);
if (vcard != null) {
final binval = vcard.photo?.binval;
if (binval != null) {
// Clean the raw data. Since this may arrive by chunks, those chunks may contain
// weird data pieces.
base64 = _cleanBase64String(binval);
final rawHash = await Sha1().hash(base64Decode(base64));
hash = HEX.encode(rawHash.bytes);
vm.setLastHash(jid, hash);
} else {
return;
}
} else {
return;
}
}
await updateAvatarForJid(jid, hash, base64);
Future<void> initialize() async {
_avatarCacheDir = await getCachePath();
}
Future<bool> subscribeJid(String jid) async {
return _getUserAvatarManager().subscribe(jid);
void resetCache() {
_requestedInStream.clear();
}
Future<bool> unsubscribeJid(String jid) async {
return _getUserAvatarManager().unsubscribe(jid);
}
String _computeAvatarPath(String hash) => p.join(_avatarCacheDir, hash);
/// Publishes the data at [path] as an avatar with PubSub ID
/// [hash]. [hash] must be the hex-encoded version of the SHA-1 hash
/// of the avatar data.
Future<bool> publishAvatar(String path, String hash) async {
final file = File(path);
final bytes = await file.readAsBytes();
final base64 = base64Encode(bytes);
final prefs = await GetIt.I.get<PreferencesService>().getPreferences();
final public = prefs.isAvatarPublic;
// Read the image metadata
final imageSize = ImageSizeGetter.getSize(MemoryInput(bytes));
// Publish data and metadata
final manager = _getUserAvatarManager();
await manager.publishUserAvatar(
base64,
hash,
public,
);
await manager.publishUserAvatarMetadata(
UserAvatarMetadata(
hash,
bytes.length,
imageSize.width,
imageSize.height,
// TODO(PapaTutuWawa): Maybe do a check here
'image/png',
),
public,
/// Returns whether we can remove the avatar file at [path] by checking if the
/// avatar is referenced by any other conversation. If [ignoreSelf] is true, then
/// our own avatar is also taken into consideration.
@visibleForTesting
Future<bool> canRemoveAvatar(String path, bool ignoreSelf) async {
final db = GetIt.I.get<DatabaseService>().database;
final result = await db.rawQuery(
'''
SELECT
COUNT(c.avatarPath) as usage_conversation,
COUNT(p.avatarPath) as usage_members
FROM
$conversationsTable as c,
$groupchatMembersTable as p
WHERE
c.avatarPath = ? OR p.avatarPath = ?
''',
[path, path],
);
return true;
final ownModifier =
(await GetIt.I.get<XmppStateService>().state).avatarUrl == path &&
!ignoreSelf
? 1
: 0;
final usageConversation = result.first['usage_conversation']! as int;
final usageGroupchat = result.first['usage_members']! as int;
_log.finest(
'Avatar usage for $path: $usageConversation (conversations) + $usageGroupchat (groupchat)',
);
return usageConversation + usageGroupchat + ownModifier == 0;
}
Future<void> requestOwnAvatar() async {
final avatar = _getUserAvatarManager();
final xmpp = GetIt.I.get<XmppService>();
final state = await xmpp.getXmppState();
final jid = state.jid!;
final id = await avatar.getAvatarId(jid);
if (id == state.avatarHash) return;
_log.info('Mismatch between saved avatar data and server-side avatar data about ourself');
final data = await avatar.getUserAvatar(jid);
if (data == null) {
_log.severe('Failed to fetch our avatar');
/// Remove the avatar file at [path], if [path] is non-null and [canRemoveAvatar] approves.
/// [ignoreSelf] is passed to [canRemoveAvatar]'s ignoreSelf parameter.
Future<void> safeRemoveAvatar(String? path, bool ignoreSelf) async {
if (path == null) {
return;
}
_log.info('Received data for our own avatar');
if (await canRemoveAvatar(path, ignoreSelf) && File(path).existsSync()) {
await File(path).delete();
}
}
final avatarPath = await saveAvatarInCache(
base64Decode(_cleanBase64String(data.base64)),
data.hash,
jid,
state.avatarUrl,
/// Checks if the avatar with the specified hash already exists on disk.
bool _hasAvatar(String hash) => File(_computeAvatarPath(hash)).existsSync();
/// Save the avatar, described by the raw bytes [bytes] and its hash [hash], into
/// the avatar cache directory.
Future<void> _saveAvatarInCache(List<int> bytes, String hash) async {
final dir = Directory(_avatarCacheDir);
if (!dir.existsSync()) {
await dir.create(recursive: true);
}
// Write the avatar
await File(_computeAvatarPath(hash)).writeAsBytes(bytes);
}
/// Fetches the avatar with id [id] for [jid], if we don't already have it locally.
Future<String?> _maybeFetchAvatarForJid(
JID jid,
String id,
bool useVCard,
) async {
// Check if we even have to request it.
if (_hasAvatar(id)) {
return _computeAvatarPath(id);
}
List<int> data;
String hexHash;
if (useVCard) {
// Use XEP-0153/XEP-0054.
// Get the VCard.
final vm = GetIt.I
.get<XmppConnection>()
.getManagerById<VCardManager>(vcardManager)!;
final vcardResult = await vm.requestVCard(jid);
if (vcardResult.isType<VCardError>()) {
_log.warning('Failed to request vcard of $jid');
return null;
}
final vcard = vcardResult.get<VCard>();
// Check if we have a photo
if (vcard.photo?.binval == null) {
_log.warning('VCard of $jid does not contain a photo.');
return null;
}
data = base64Decode(vcard.photo!.binval!);
hexHash = id;
} else {
// Use XEP-0084.
// Request the avatar data and write it to disk.
final rawAvatar = await GetIt.I
.get<XmppConnection>()
.getManagerById<UserAvatarManager>(userAvatarManager)!
.getUserAvatarData(jid, id);
if (rawAvatar.isType<AvatarError>()) {
_log.warning('Failed to request avatar ($jid, $id)');
return null;
}
// Verify the hash
data = rawAvatar.get<UserAvatarData>().data;
hexHash = rawAvatar.get<UserAvatarData>().hash;
final actualHexHash = HEX.encode(
(await Sha1().hash(data)).bytes,
);
if (actualHexHash != hexHash) {
_log.warning(
'Avatar hash of $jid ($hexHash) is not equal to the computed hash ($actualHexHash)',
);
return null;
}
}
await _saveAvatarInCache(
data,
hexHash,
);
await xmpp.modifyXmppState((state) => state.copyWith(
avatarUrl: avatarPath,
avatarHash: data.hash,
),);
return _computeAvatarPath(id);
}
sendEvent(SelfAvatarChangedEvent(path: avatarPath, hash: data.hash));
Future<void> _applyNewAvatarToJid(JID jid, String hash) async {
assert(_hasAvatar(hash), 'The avatar must exist');
final cs = GetIt.I.get<ConversationService>();
final rs = GetIt.I.get<RosterService>();
final accountJid = (await GetIt.I.get<XmppStateService>().getAccountJid())!;
final conversation =
await cs.getConversationByJid(jid.toString(), accountJid);
final rosterItem = await rs.getRosterItemByJid(jid.toString(), accountJid);
// Do nothing if we do not know of the JID.
if (conversation == null && rosterItem == null) {
_log.info('Found no conversation or roster item with jid $jid');
return;
}
// Update the conversation
final avatarPath = _computeAvatarPath(hash);
if (conversation != null) {
final newConversation = await cs.createOrUpdateConversation(
jid.toString(),
accountJid,
update: (c) async {
return cs.updateConversation(
jid.toString(),
accountJid,
avatarPath: avatarPath,
avatarHash: hash,
);
},
);
sendEvent(
ConversationUpdatedEvent(conversation: newConversation!),
);
// Try to delete the old avatar
await safeRemoveAvatar(conversation.avatarPath, false);
}
// Update the roster item
if (rosterItem != null) {
final newRosterItem = await rs.updateRosterItem(
jid.toString(),
accountJid,
avatarPath: avatarPath,
avatarHash: hash,
);
sendEvent(
RosterDiffEvent(modified: [newRosterItem]),
);
// Try to delete the old avatar
await safeRemoveAvatar(rosterItem.avatarPath, false);
}
// Update the UI.
sendEvent(
AvatarUpdatedEvent(jid: jid.toString(), path: avatarPath),
);
}
Future<void> handleAvatarUpdate(UserAvatarUpdatedEvent event) async {
if (event.metadata.isEmpty) {
return;
}
// Add the JID to the pending requests list.
_requestedInStream.add(event.jid);
// Fetch the new avatar.
final metadata = event.metadata
.firstWhereOrNull((element) => element.type == 'image/png');
if (metadata == null) {
_log.warning(
'Avatar metadata from ${event.jid} does not advertise an image/png avatar, which violates XEP-0084',
);
return;
}
final newAvatarPath = await _maybeFetchAvatarForJid(
event.jid,
metadata.id,
false,
);
if (newAvatarPath == null) {
_log.warning('Failed to fetch avatar ${metadata.id} for ${event.jid}');
_requestedInStream.remove(event.jid);
return;
}
// Update the conversation.
await _applyNewAvatarToJid(event.jid, metadata.id);
// Remove the JID from the pending requests list.
_requestedInStream.remove(event.jid);
}
Future<String?> requestGroupchatAvatar(JID roomJid, String? oldHash) async {
// Prevent multiple requests in a row.
if (_requestedInStream.contains(roomJid)) {
return null;
}
_requestedInStream.add(roomJid);
// Perform a disco request.
final id =
await GetIt.I.get<GroupchatService>().getGroupchatAvatarHash(roomJid);
if (id == null) {
return null;
}
// Check if the id changed.
var bypassIdCheck = false;
if (oldHash != null && !File(_computeAvatarPath(oldHash)).existsSync()) {
bypassIdCheck = true;
_log.finest('Avatar hash $oldHash does not exist. Bypass id check');
bypassIdCheck = true;
}
if (id == oldHash && !bypassIdCheck) {
_log.finest(
'Remote id ($id) is equal to local id ($oldHash) for $roomJid. Not fetching avatar.',
);
_requestedInStream.remove(roomJid);
return _computeAvatarPath(id);
}
// Fetch the avatar.
final newAvatarPath = await _maybeFetchAvatarForJid(
roomJid,
id,
true,
);
if (newAvatarPath == null) {
_log.finest('Failed to request MUC avatar for $roomJid');
_requestedInStream.remove(roomJid);
return null;
}
// Update conversation.
await _applyNewAvatarToJid(roomJid, id);
// Remove the JID from the pending requests list.
_requestedInStream.remove(roomJid);
return _computeAvatarPath(id);
}
/// Request the avatar for [jid], given its optional previous avatar hash [oldHash].
Future<String?> requestAvatar(JID jid, String? oldHash) async {
// Prevent multiple requests in a row.
if (_requestedInStream.contains(jid)) {
return null;
}
_requestedInStream.add(jid);
// Request the latest metadata.
final rawMetadata = await GetIt.I
.get<XmppConnection>()
.getManagerById<UserAvatarManager>(userAvatarManager)!
.getLatestMetadata(jid);
if (rawMetadata.isType<AvatarError>()) {
_log.warning('Failed to get metadata for $jid');
_requestedInStream.remove(jid);
return null;
}
// Find the first metadata item that advertises a PNG avatar.
final metadata = rawMetadata.get<List<UserAvatarMetadata>>();
var id =
metadata.firstWhereOrNull((element) => element.type == 'image/png')?.id;
if (id == null) {
_log.warning(
'$jid does not advertise an avatar of type image/png, which violates XEP-0084',
);
if (metadata.isEmpty) {
_log.warning(
'$jid does not advertise any metadata.',
);
_requestedInStream.remove(jid);
return null;
}
// If other avatar types are present, sort the list to make the selection stable
// and then just pick the first one.
final typeSortedMetadata = List.of(metadata)
..sort((a, b) => a.type.compareTo(b.type));
final firstMetadata = typeSortedMetadata.first;
_log.warning(
'Falling back to ${firstMetadata.id} (${firstMetadata.type})',
);
id = firstMetadata.id;
}
// Check if the id changed.
var bypassIdCheck = false;
if (oldHash != null && !File(_computeAvatarPath(oldHash)).existsSync()) {
bypassIdCheck = true;
_log.finest('Avatar hash $oldHash does not exist. Bypass id check');
bypassIdCheck = true;
}
if (id == oldHash && !bypassIdCheck) {
_log.finest(
'Remote id ($id) is equal to local id ($oldHash) for $jid. Not fetching avatar.',
);
_requestedInStream.remove(jid);
return _computeAvatarPath(id);
}
// Request the new avatar.
final newAvatarPath = await _maybeFetchAvatarForJid(
jid,
id,
false,
);
if (newAvatarPath == null) {
_log.warning('Failed to request avatar for $jid');
_requestedInStream.remove(jid);
return null;
}
// Update conversations.
await _applyNewAvatarToJid(jid, id);
// Remove the JID from the pending requests list.
_requestedInStream.remove(jid);
return _computeAvatarPath(id);
}
/// Request the avatar for our own avatar.
Future<bool> requestOwnAvatar() async {
final xss = GetIt.I.get<XmppStateService>();
final jid = JID.fromString((await xss.getAccountJid())!);
// Prevent multiple requests in a row.
if (_requestedInStream.contains(jid)) {
return true;
}
_requestedInStream.add(jid);
// Get the current id.
final state = await xss.state;
final rawMetadata = await GetIt.I
.get<XmppConnection>()
.getManagerById<UserAvatarManager>(userAvatarManager)!
.getLatestMetadata(jid);
if (rawMetadata.isType<AvatarError>()) {
_log.warning('rawMetadata is an AvatarError');
return false;
}
// Find the first metadata item that advertises a PNG avatar.
final id = rawMetadata
.get<List<UserAvatarMetadata>>()
.firstWhereOrNull((element) => element.type == 'image/png')
?.id;
if (id == null) {
_log.warning(
'We ($jid) do not advertise an avatar of type image/png, which violates XEP-0084',
);
return false;
}
// Check if the avatar even changed.
var bypassIdCheck = false;
if (state.avatarUrl != null && !File(state.avatarUrl!).existsSync()) {
bypassIdCheck = true;
_log.finest(
'Avatar path ${state.avatarUrl} does not exist. Bypass id check',
);
bypassIdCheck = true;
}
if (id == state.avatarHash && !bypassIdCheck) {
_log.finest(
'Not requesting our own avatar because the server-side id ($id) is equal to our current id (${state.avatarHash})',
);
_requestedInStream.remove(jid);
return true;
}
// Request the new avatar.
final oldAvatarPath = state.avatarUrl;
final newAvatarPath = await _maybeFetchAvatarForJid(
jid,
id,
false,
);
if (newAvatarPath == null) {
_log.warning('Failed to request own avatar');
_requestedInStream.remove(jid);
return false;
}
// Update the state and the UI.
await xss.modifyXmppState(
(s) {
return s.copyWith(
avatarUrl: newAvatarPath,
avatarHash: id,
);
},
);
sendEvent(SelfAvatarChangedEvent(path: newAvatarPath, hash: id));
// Try to safely delete the old avatar.
await safeRemoveAvatar(oldAvatarPath, true);
// Update the notification UI.
await GetIt.I.get<NotificationsService>().maybeSetAvatarFromState();
// Remove our JID from the pending requests list.
_requestedInStream.remove(jid);
return true;
}
Future<bool> setNewAvatar(String path, String hash) async {
final file = File(path);
final bytes = await file.readAsBytes();
final base64 = base64Encode(bytes);
final isPublic = (await GetIt.I.get<PreferencesService>().getPreferences())
.isAvatarPublic;
// Copy the avatar into the cache, if we don't already have it.
final avatarPath = _computeAvatarPath(hash);
if (!_hasAvatar(hash)) {
await file.copy(avatarPath);
}
// Get image metadata.
final imageSize = (await getImageSizeFromPath(avatarPath))!;
// Publish data and metadata
final am = GetIt.I
.get<XmppConnection>()
.getManagerById<UserAvatarManager>(userAvatarManager)!;
_log.finest('Publishing avatar');
final dataResult = await am.publishUserAvatar(base64, hash, isPublic);
if (dataResult.isType<AvatarError>()) {
_log.warning('Failed to publish avatar data');
return false;
}
// Publish the metadata.
final metadataResult = await am.publishUserAvatarMetadata(
UserAvatarMetadata(
hash,
bytes.length,
imageSize.width.toInt(),
imageSize.height.toInt(),
// TODO(Unknown): Make sure
'image/png',
null,
),
isPublic,
);
if (metadataResult.isType<AvatarError>()) {
_log.warning('Failed to publish avatar metadata');
return false;
}
// Update the state
final xss = GetIt.I.get<XmppStateService>();
final state = await xss.state;
final oldAvatarPath = state.avatarUrl;
await xss.modifyXmppState(
(s) {
return s.copyWith(
avatarUrl: avatarPath,
avatarHash: hash,
);
},
);
// Update the UI
sendEvent(SelfAvatarChangedEvent(path: avatarPath, hash: hash));
// Update the notifications.
await GetIt.I.get<NotificationsService>().maybeSetAvatarFromState();
// Safely remove the old avatar
await safeRemoveAvatar(oldAvatarPath, true);
// Remove the temp file.
await file.delete();
return true;
}
}

View File

@@ -1,43 +1,131 @@
import 'dart:async';
import 'package:get_it/get_it.dart';
import 'package:logging/logging.dart';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/service/service.dart';
import 'package:moxxyv2/service/xmpp_state.dart';
import 'package:moxxyv2/shared/events.dart';
enum BlockPushType {
block,
unblock
}
enum BlockPushType { block, unblock }
class BlocklistService {
BlocklistService();
List<String>? _blocklist;
bool _requested = false;
bool? _supported;
final Logger _log = Logger('BlocklistService');
BlocklistService() :
_blocklistCache = List.empty(growable: true),
_requestedBlocklist = false;
final List<String> _blocklistCache;
bool _requestedBlocklist;
Future<void> _removeBlocklistEntry(String jid, String accountJid) async {
await GetIt.I.get<DatabaseService>().database.delete(
blocklistTable,
where: 'jid = ? AND accountJid = ?',
whereArgs: [jid, accountJid],
);
}
Future<List<String>> _requestBlocklist() async {
final manager = GetIt.I.get<XmppConnection>().getManagerById<BlockingManager>(blockingManager)!;
_blocklistCache
..clear()
..addAll(await manager.getBlocklist());
_requestedBlocklist = true;
return _blocklistCache;
Future<void> _addBlocklistEntry(String jid, String accountJid) async {
await GetIt.I.get<DatabaseService>().database.insert(
blocklistTable,
{
'jid': jid,
'accountJid': accountJid,
},
);
}
void onNewConnection() {
// Invalidate the caches
_blocklist = null;
_requested = false;
_supported = null;
}
Future<bool> _checkSupport() async {
return _supported ??= await GetIt.I
.get<XmppConnection>()
.getManagerById<BlockingManager>(blockingManager)!
.isSupported();
}
Future<void> _requestBlocklist() async {
assert(
_blocklist != null,
'The blocklist must be loaded from the database before requesting',
);
// Check if blocking is supported
if (!(await _checkSupport())) {
_log.warning('Blocklist requested but server does not support it.');
return;
}
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
final blocklist = await GetIt.I
.get<XmppConnection>()
.getManagerById<BlockingManager>(blockingManager)!
.getBlocklist();
// Diff the received blocklist with the cache
final newItems = List<String>.empty(growable: true);
final removedItems = List<String>.empty(growable: true);
for (final item in blocklist) {
if (!_blocklist!.contains(item)) {
await _addBlocklistEntry(item, accountJid!);
_blocklist!.add(item);
newItems.add(item);
}
}
// Diff the cache with the received blocklist
for (final item in _blocklist!) {
if (!blocklist.contains(item)) {
await _removeBlocklistEntry(item, accountJid!);
_blocklist!.remove(item);
removedItems.add(item);
}
}
_requested = true;
// Trigger an UI event if we have anything to tell the UI
if (newItems.isNotEmpty || removedItems.isNotEmpty) {
sendEvent(
BlocklistPushEvent(
added: newItems,
removed: removedItems,
),
);
}
}
/// Returns the blocklist from the database
Future<List<String>> getBlocklist() async {
if (!_requestedBlocklist) {
_blocklistCache
..clear()
..addAll(await _requestBlocklist());
Future<List<String>> getBlocklist(String accountJid) async {
if (_blocklist == null) {
final blocklistRaw = await GetIt.I.get<DatabaseService>().database.query(
blocklistTable,
where: 'accountJid = ?',
whereArgs: [accountJid],
);
_blocklist = blocklistRaw.map((m) => m['jid']! as String).toList();
if (!_requested) {
unawaited(_requestBlocklist());
}
return _blocklist!;
}
return _blocklistCache;
if (!_requested) {
unawaited(_requestBlocklist());
}
return _blocklist!;
}
void onUnblockAllPush() {
_blocklistCache.clear();
_blocklist = List<String>.empty(growable: true);
sendEvent(
BlocklistUnblockAllEvent(),
);
@@ -45,23 +133,28 @@ class BlocklistService {
Future<void> onBlocklistPush(BlockPushType type, List<String> items) async {
// We will fetch it later when getBlocklist is called
if (!_requestedBlocklist) return;
if (!_requested) return;
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
final newBlocks = List<String>.empty(growable: true);
final removedBlocks = List<String>.empty(growable: true);
for (final item in items) {
switch (type) {
case BlockPushType.block: {
if (_blocklistCache.contains(item)) continue;
_blocklistCache.add(item);
newBlocks.add(item);
}
break;
case BlockPushType.unblock: {
_blocklistCache.removeWhere((i) => i == item);
removedBlocks.add(item);
}
break;
case BlockPushType.block:
{
if (_blocklist!.contains(item)) continue;
_blocklist!.add(item);
newBlocks.add(item);
await _addBlocklistEntry(item, accountJid!);
}
case BlockPushType.unblock:
{
_blocklist!.removeWhere((i) => i == item);
removedBlocks.add(item);
await _removeBlocklistEntry(item, accountJid!);
}
}
}
@@ -74,17 +167,60 @@ class BlocklistService {
}
Future<bool> blockJid(String jid) async {
final manager = GetIt.I.get<XmppConnection>().getManagerById<BlockingManager>(blockingManager)!;
return manager.block([ jid ]);
// Check if blocking is supported
if (!(await _checkSupport())) {
_log.warning('Blocking $jid requested but server does not support it.');
return false;
}
_blocklist!.add(jid);
await _addBlocklistEntry(
jid,
(await GetIt.I.get<XmppStateService>().getAccountJid())!,
);
return GetIt.I
.get<XmppConnection>()
.getManagerById<BlockingManager>(blockingManager)!
.block([jid]);
}
Future<bool> unblockJid(String jid) async {
final manager = GetIt.I.get<XmppConnection>().getManagerById<BlockingManager>(blockingManager)!;
return manager.unblock([ jid ]);
// Check if blocking is supported
if (!(await _checkSupport())) {
_log.warning('Unblocking $jid requested but server does not support it.');
return false;
}
_blocklist!.remove(jid);
await _removeBlocklistEntry(
jid,
(await GetIt.I.get<XmppStateService>().getAccountJid())!,
);
return GetIt.I
.get<XmppConnection>()
.getManagerById<BlockingManager>(blockingManager)!
.unblock([jid]);
}
Future<bool> unblockAll() async {
final manager = GetIt.I.get<XmppConnection>().getManagerById<BlockingManager>(blockingManager)!;
return manager.unblockAll();
// Check if blocking is supported
if (!(await _checkSupport())) {
_log.warning(
'Unblocking all JIDs requested but server does not support it.',
);
return false;
}
_blocklist!.clear();
await GetIt.I.get<DatabaseService>().database.delete(
blocklistTable,
where: 'accountJid = ?',
whereArgs: [await GetIt.I.get<XmppStateService>().getAccountJid()],
);
return GetIt.I
.get<XmppConnection>()
.getManagerById<BlockingManager>(blockingManager)!
.unblockAll();
}
}

17
lib/service/cache.dart Normal file
View File

@@ -0,0 +1,17 @@
import 'package:moxxy_native/moxxy_native.dart';
import 'package:path/path.dart' as p;
/// Computes the path to a subdirectory [subdirectory] inside Moxxy's
/// cache directory. Note that this method does not guarantee the returned
/// path's existence.
Future<String> computeCacheDirectoryPath(String subdirectory) async {
return p.join(
await MoxxyPlatformApi().getCacheDataPath(),
subdirectory,
);
}
/// Wrapper around [computeCacheDirectoryPath] for the cache directory that contains
/// copies of picked files.
Future<String> computePickedFileCachePath() async =>
computeCacheDirectoryPath('cache');

View File

@@ -1,22 +1,32 @@
import 'dart:io' show Platform;
import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:get_it/get_it.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxyv2/service/httpfiletransfer/httpfiletransfer.dart';
import 'package:moxxyv2/service/moxxmpp/reconnect.dart';
class ConnectivityEvent {
const ConnectivityEvent(this.regained, this.lost);
final bool regained;
final bool lost;
}
class ConnectivityService {
ConnectivityService() : _log = Logger('ConnectivityService');
final Logger _log;
/// The internal stream controller
final StreamController<ConnectivityEvent> _controller =
StreamController<ConnectivityEvent>.broadcast();
/// The logger
final Logger _log = Logger('ConnectivityService');
/// Caches the current connectivity state
late ConnectivityResult _connectivity;
late List<ConnectivityResult> _connectivity;
Stream<ConnectivityEvent> get stream => _controller.stream;
@visibleForTesting
void setConnectivity(ConnectivityResult result) {
_log.warning('Internal connectivity state changed by request originating from outside ConnectivityService');
void setConnectivity(List<ConnectivityResult> result) {
_log.warning(
'Internal connectivity state changed by request originating from outside ConnectivityService',
);
_connectivity = result;
}
@@ -24,23 +34,24 @@ class ConnectivityService {
final conn = Connectivity();
_connectivity = await conn.checkConnectivity();
// TODO(Unknown): At least on Android, the stream fires directly after listening although the
// network does not change. So just skip it.
// See https://github.com/fluttercommunity/plus_plugins/issues/567
final skipAmount = Platform.isAndroid ? 1 : 0;
conn.onConnectivityChanged.skip(skipAmount).listen((ConnectivityResult result) {
final regained = _connectivity == ConnectivityResult.none && result != ConnectivityResult.none;
final lost = result == ConnectivityResult.none;
conn.onConnectivityChanged.listen((List<ConnectivityResult> result) {
final regained = _connectivity.contains(ConnectivityResult.none) &&
!result.contains(ConnectivityResult.none);
final lost = result.contains(ConnectivityResult.none);
_connectivity = result;
// TODO(PapaTutuWawa): Should we use Streams?
// Notify other services
(GetIt.I.get<XmppConnection>().reconnectionPolicy as MoxxyReconnectionPolicy)
.onConnectivityChanged(regained, lost);
GetIt.I.get<HttpFileTransferService>().onConnectivityChanged(regained);
_controller.add(
ConnectivityEvent(
regained,
lost,
),
);
});
}
ConnectivityResult get currentState => _connectivity;
List<ConnectivityResult> get currentState => _connectivity;
Future<bool> hasConnection() async {
return !_connectivity.contains(ConnectivityResult.none);
}
}

View File

@@ -1,56 +1,75 @@
import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:get_it/get_it.dart';
import 'package:logging/logging.dart';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxyv2/i18n/strings.g.dart';
import 'package:moxxyv2/service/connectivity.dart';
import 'package:moxxyv2/service/notifications.dart';
import 'package:synchronized/synchronized.dart';
class ConnectivityWatcherService {
/// Logger.
final Logger _log = Logger('ConnectivityWatcherService');
ConnectivityWatcherService() : _log = Logger('ConnectivityWatcherService');
final Logger _log;
// Timer counting how much time has passed since we were last connected
/// Timer counting how much time has passed since we were last connected.
Timer? _timer;
/// Lock for accessing _timer
final Lock _lock = Lock();
Future<void> initialize() async {
GetIt.I.get<ConnectivityService>().stream.listen(_onConnectivityEvent);
}
Future<void> _onConnectivityEvent(ConnectivityEvent event) async {
if (event.lost) {
_log.finest('Network connection lost. Stopping timer');
await _stopTimer();
}
}
Future<void> _onTimerElapsed() async {
await _stopTimer();
await GetIt.I.get<NotificationsService>().showWarningNotification(
'Moxxy',
t.errors.connection.connectionTimeout,
);
_stopTimer();
'Moxxy',
t.errors.connection.connectionTimeout,
);
}
/// Stops the currently running timer, if there is one.
void _stopTimer() {
if (_timer != null) {
_timer!.cancel();
Future<void> _stopTimer() async {
await _lock.synchronized(() {
_timer?.cancel();
_timer = null;
}
});
}
/// Starts the timer. If it is already running, it stops the currently running one before
/// starting the new one.
void _startTimer() {
_stopTimer();
Future<void> _startTimer() async {
await _stopTimer();
_timer = Timer(const Duration(minutes: 30), _onTimerElapsed);
}
/// Called when the XMPP connection state changed
Future<void> onConnectionStateChanged(XmppConnectionState before, XmppConnectionState current) async {
if (before == XmppConnectionState.connected && current != XmppConnectionState.connected) {
Future<void> onConnectionStateChanged(
XmppConnectionState before,
XmppConnectionState current,
) async {
if (before == XmppConnectionState.connected &&
current != XmppConnectionState.connected) {
// We somehow lost connection
if (GetIt.I.get<ConnectivityService>().currentState != ConnectivityResult.none) {
if (await GetIt.I.get<ConnectivityService>().hasConnection()) {
_log.finest('Lost connection to server. Starting warning timer...');
_startTimer();
await _startTimer();
} else {
_log.finest('Lost connection to server but no network connectivity available. Stopping warning timer...');
_stopTimer();
_log.finest(
'Lost connection to server but no network connectivity available. Stopping warning timer...',
);
await _stopTimer();
}
} else if (current == XmppConnectionState.connected) {
_stopTimer();
await _stopTimer();
}
}
}

348
lib/service/contacts.dart Normal file
View File

@@ -0,0 +1,348 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:get_it/get_it.dart';
import 'package:logging/logging.dart';
import 'package:moxxyv2/service/conversation.dart';
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/service/preferences.dart';
import 'package:moxxyv2/service/roster.dart';
import 'package:moxxyv2/service/service.dart';
import 'package:moxxyv2/service/xmpp_state.dart';
import 'package:moxxyv2/shared/events.dart';
import 'package:moxxyv2/shared/helpers.dart';
import 'package:moxxyv2/shared/models/roster.dart';
import 'package:permission_handler/permission_handler.dart';
class ContactWrapper {
const ContactWrapper(this.id, this.jid, this.displayName, this.thumbnail);
final String id;
final String jid;
final String displayName;
final Uint8List? thumbnail;
}
class ContactsService {
ContactsService() {
// NOTE: Apparently, this means that if false, contacts that are in 0 groups
// are not returned.
FlutterContacts.config.includeNonVisibleOnAndroid = true;
}
/// Logger.
final Logger _log = Logger('ContactsService');
/// JID -> Id.
Map<String, String>? _contactIds;
/// Contact ID -> Display name from the contact or null if we cached that there is
/// none
final Map<String, String?> _contactDisplayNames = {};
Future<void> initialize() async {
await enable(shouldScan: false);
}
/// Enable listening to contact database events. If [shouldScan] is true, also
/// performs a scan of the contacts database, if we're allowed.
Future<void> enable({bool shouldScan = true}) async {
FlutterContacts.addListener(_onContactsDatabaseUpdate);
if (shouldScan && await _canUseContactIntegration()) {
unawaited(scanContacts());
}
}
/// Disable listening to contact database events. Also removes all roster items
/// that are pseudo roster items.
Future<void> disable() async {
FlutterContacts.removeListener(_onContactsDatabaseUpdate);
await GetIt.I.get<RosterService>().removePseudoRosterItems(
(await GetIt.I.get<XmppStateService>().getAccountJid())!,
);
}
Future<void> _onContactsDatabaseUpdate() async {
_log.finest('Got contacts database update');
await scanContacts();
}
/// Queries the contact list for contacts that include a XMPP URI.
Future<List<ContactWrapper>> _fetchContactsWithJabber() async {
final contacts = await FlutterContacts.getContacts(
withProperties: true,
withThumbnail: true,
);
_log.finest('Got ${contacts.length} contacts');
final jabberContacts = List<ContactWrapper>.empty(growable: true);
for (final c in contacts) {
final index =
c.socialMedias.indexWhere((s) => s.label == SocialMediaLabel.jabber);
if (index == -1) continue;
jabberContacts.add(
ContactWrapper(
c.id,
c.socialMedias[index].userName,
c.displayName,
c.thumbnail,
),
);
}
_log.finest('${jabberContacts.length} contacts have an XMPP address');
return jabberContacts;
}
/// Checks whether the contact integration is enabled by the user in the preferences.
/// Returns true if that is the case. If not, returns false.
Future<bool> isContactIntegrationEnabled() async {
final prefs = await GetIt.I.get<PreferencesService>().getPreferences();
return prefs.enableContactIntegration;
}
/// Checks if we a) have the permission to access the contact list and b) if the
/// user wants to use this integration.
/// Returns true if we can proceed with accessing the contact list. False, if not.
Future<bool> _canUseContactIntegration() async {
if (!(await isContactIntegrationEnabled())) {
_log.finest(
'_canUseContactIntegration: Returning false since enableContactIntegration is false',
);
return false;
}
final permission = await Permission.contacts.status;
if (permission == PermissionStatus.denied) {
_log.finest(
"_canUseContactIntegration: Returning false since we don't have the contacts permission",
);
return false;
}
return true;
}
/// Queries the database for the mapping of JID -> Contact ID. The result is
/// cached after the first call.
Future<Map<String, String>> _getContactIds() async {
if (_contactIds != null) return _contactIds!;
_contactIds = Map<String, String>.fromEntries(
(await GetIt.I.get<DatabaseService>().database.query(contactsTable)).map(
(item) => MapEntry(
item['jid']! as String,
item['id']! as String,
),
),
);
return _contactIds!;
}
/// Queries the contact list, if enabled and allowed, and returns the contact's
/// display name.
///
/// [id] is the id of the contact. A null value indicates that there is no
/// contact and null will be returned immediately.
Future<String?> getContactDisplayName(String? id) async {
if (id == null || !(await _canUseContactIntegration())) return null;
if (_contactDisplayNames.containsKey(id)) return _contactDisplayNames[id];
final result = await FlutterContacts.getContact(
id,
withThumbnail: false,
);
_contactDisplayNames[id] = result?.displayName;
return result?.displayName;
}
/// Returns the contact Id for the JID [jid]. If either the contact integration is
/// disabled, not possible (due to missing permissions) or there is no contact with
/// [jid] as their Jabber attribute, returns null.
Future<String?> getContactIdForJid(String jid) async {
if (!(await _canUseContactIntegration())) return null;
return (await _getContactIds())[jid];
}
/// Returns the path to the avatar file for the contact with JID [jid] as their
/// Jabber attribute. If either the contact integration is disabled, not possible
/// (due to missing permissions) or there is no contact with [jid] as their Jabber
/// attribute, returns null.
Future<String?> getProfilePicturePathForJid(String jid) async {
final id = await getContactIdForJid(jid);
if (id == null) return null;
final avatarPath = await getContactProfilePicturePath(id);
return File(avatarPath).existsSync() ? avatarPath : null;
}
Future<void> scanContacts() async {
final cs = GetIt.I.get<ConversationService>();
final rs = GetIt.I.get<RosterService>();
final contacts = await _fetchContactsWithJabber();
// JID -> Id
final knownContactIds = await _getContactIds();
// Id -> JID
final knownContactIdsReverse =
knownContactIds.map((key, value) => MapEntry(value, key));
final modifiedRosterItems = List<RosterItem>.empty(growable: true);
final addedRosterItems = List<RosterItem>.empty(growable: true);
final removedRosterItems = List<String>.empty(growable: true);
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
for (final id in List<String>.from(knownContactIds.values)) {
final index = contacts.indexWhere((c) => c.id == id);
if (index != -1) continue;
final jid = knownContactIdsReverse[id]!;
await GetIt.I.get<DatabaseService>().database.delete(
contactsTable,
where: 'id = ?',
whereArgs: [id],
);
_contactIds!.remove(knownContactIdsReverse[id]);
// Remove the avatar file, if it existed
final avatarPath = await getContactProfilePicturePath(id);
final avatarFile = File(avatarPath);
if (avatarFile.existsSync()) {
unawaited(avatarFile.delete());
}
// Remove the contact attributes from the conversation, if it existed
final conversation = await cs.createOrUpdateConversation(
jid,
accountJid!,
update: (c) async {
return cs.updateConversation(
jid,
accountJid,
contactId: null,
contactAvatarPath: null,
contactDisplayName: null,
);
},
);
if (conversation != null) {
sendEvent(
ConversationUpdatedEvent(
conversation: conversation,
),
);
}
// Remove the contact attributes from the roster item, if it existed
final r = await rs.getRosterItemByJid(jid, accountJid);
if (r != null) {
if (r.pseudoRosterItem) {
_log.finest('Removing pseudo roster item $jid');
await rs.removeRosterItem(r.jid, accountJid);
removedRosterItems.add(jid);
} else {
final newRosterItem = await rs.updateRosterItem(
r.jid,
accountJid,
contactId: null,
contactAvatarPath: null,
contactDisplayName: null,
);
modifiedRosterItems.add(newRosterItem);
}
}
}
for (final contact in contacts) {
// Add the ID to the cache and the database if it does not already exist
if (!knownContactIds.containsKey(contact.jid)) {
await GetIt.I.get<DatabaseService>().database.insert(
contactsTable,
<String, String>{
'id': contact.id,
'jid': contact.jid,
},
);
_contactIds![contact.jid] = contact.id;
}
// Store the avatar image
// NOTE: We do not check if the file already exists since this function may also
// be triggered by the contact database listener. That listener fires when
// a change happened, without telling us exactly what happened. So, we
// just overwrite it.
final contactAvatarPath = await getContactProfilePicturePath(contact.id);
if (contact.thumbnail != null) {
final file = File(contactAvatarPath);
await file.writeAsBytes(contact.thumbnail!);
}
// Update a possibly existing conversation
final conversation = await cs.createOrUpdateConversation(
contact.jid,
accountJid!,
update: (c) async {
return cs.updateConversation(
contact.jid,
accountJid,
contactId: contact.id,
contactAvatarPath:
contact.thumbnail != null ? contactAvatarPath : null,
contactDisplayName: contact.displayName,
);
},
);
if (conversation != null) {
sendEvent(
ConversationUpdatedEvent(
conversation: conversation,
),
);
}
// Update a possibly existing roster item
final r = await rs.getRosterItemByJid(contact.jid, accountJid);
if (r != null) {
final newRosterItem = await rs.updateRosterItem(
r.jid,
accountJid,
contactId: contact.id,
contactAvatarPath: contactAvatarPath,
contactDisplayName: contact.displayName,
);
modifiedRosterItems.add(newRosterItem);
} else {
final newRosterItem = await rs.addRosterItemFromData(
accountJid,
null,
null,
contact.jid,
contact.jid.split('@').first,
'none',
'none',
true,
contact.id,
contactAvatarPath,
contact.displayName,
);
addedRosterItems.add(newRosterItem);
}
}
if (addedRosterItems.isNotEmpty ||
modifiedRosterItems.isNotEmpty ||
removedRosterItems.isNotEmpty) {
sendEvent(
RosterDiffEvent(
added: addedRosterItems,
modified: modifiedRosterItems,
removed: removedRosterItems,
),
);
}
}
}

View File

@@ -1,119 +1,419 @@
import 'package:get_it/get_it.dart';
import 'package:moxlib/moxlib.dart';
import 'package:logging/logging.dart';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxyv2/service/connectivity.dart';
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/service/database/helpers.dart';
import 'package:moxxyv2/service/groupchat.dart';
import 'package:moxxyv2/service/message.dart';
import 'package:moxxyv2/service/not_specified.dart';
import 'package:moxxyv2/service/preferences.dart';
import 'package:moxxyv2/shared/cache.dart';
import 'package:moxxyv2/service/roster.dart';
import 'package:moxxyv2/shared/models/conversation.dart';
import 'package:moxxyv2/shared/models/groupchat.dart';
import 'package:moxxyv2/shared/models/message.dart';
import 'package:synchronized/synchronized.dart';
typedef CreateConversationCallback = Future<Conversation> Function();
typedef UpdateConversationCallback = Future<Conversation> Function(
Conversation,
);
typedef PreRunConversationCallback = Future<void> Function(Conversation?);
class ConversationService {
ConversationService()
: _conversationCache = LRUCache(100),
_loadedConversations = false;
/// The list of known conversations.
Map<String, Conversation>? _conversationCache;
final LRUCache<int, Conversation> _conversationCache;
bool _loadedConversations;
/// The lock for accessing _conversationCache
final Lock _lock = Lock();
final Logger _log = Logger('ConversationService');
String? _activeConversationJid;
String? get activeConversationJid => _activeConversationJid;
set activeConversationJid(String? jid) {
_log.finest('Setting activeConversationJid to $jid');
_activeConversationJid = jid;
}
/// When called with a JID [jid], then first, if non-null, [preRun] is
/// executed.
/// Next, if a conversation with JID [jid] exists, [update] is called with
/// the conversation as its argument. If not, then [create] is executed.
/// Returns either the result of [create], [update] or null.
Future<Conversation?> createOrUpdateConversation(
String jid,
String accountJid, {
CreateConversationCallback? create,
UpdateConversationCallback? update,
PreRunConversationCallback? preRun,
}) async {
return _lock.synchronized(() async {
final conversation = await _getConversationByJid(jid, accountJid);
// Pre run
if (preRun != null) {
await preRun(conversation);
}
_log.finest(
'[createOrUpdateConversation] Got conversation: $conversation',
);
if (conversation != null) {
// Conversation exists
if (update != null) {
final updatedConversation = await update(conversation);
assert(
updatedConversation.jid == conversation.jid,
'Cannot change the JID of a conversation',
);
assert(
_conversationCache!.containsKey(updatedConversation.jid),
'Conversation JID must already be in cache',
);
// Update the cache
_conversationCache![updatedConversation.jid] = updatedConversation;
return updatedConversation;
}
} else {
// Conversation does not exist
if (create != null) {
final newConversation = await create();
// Update the cache
_conversationCache![newConversation.jid] = newConversation;
return newConversation;
}
}
return null;
});
}
/// Search for conversations associated with the account [accountJid],
/// where the title or the last message's body contains [text].
Future<List<Conversation>> searchConversations(
String accountJid,
String text,
) async {
// TODO(Unknown): Optimize so that we don't have to do a second query (last message)$
// for the matching conversations.
final db = GetIt.I.get<DatabaseService>().database;
final gs = GetIt.I.get<GroupchatService>();
final textQuery = '%$text%';
final conversationsRaw = await db.rawQuery(
'''
SELECT
*
FROM
$conversationsTable AS conversations
LEFT JOIN
$messagesTable lm ON lm.id = conversations.lastMessageId
WHERE
conversations.accountJid = ? AND (
conversations.title LIKE ? OR lm.body LIKE ?
)
ORDER BY
lastChangeTimestamp DESC''',
[
accountJid,
textQuery,
textQuery,
],
);
_log.finest(
'Conversation search returned ${conversationsRaw.length} conversations',
);
final tmp = List<Conversation>.empty(growable: true);
for (final c in conversationsRaw) {
final jid = c['jid']! as String;
final rosterItem = await GetIt.I
.get<RosterService>()
.getRosterItemByJid(jid, accountJid);
Message? lastMessage;
if (c['lastMessageId'] != null) {
lastMessage = await GetIt.I.get<MessageService>().getMessageById(
c['lastMessageId']! as String,
accountJid,
queryReactionPreview: false,
);
}
GroupchatDetails? groupchatDetails;
if (c['type'] == ConversationType.groupchat.value) {
groupchatDetails = await gs.getGroupchatDetailsByJid(
c['jid']! as String,
accountJid,
);
}
tmp.add(
Conversation.fromDatabaseJson(
c,
rosterItem?.showAddToRosterButton ?? true,
lastMessage,
groupchatDetails,
),
);
}
return tmp;
}
/// Loads all conversations from the database and adds them to the state and cache.
Future<List<Conversation>> loadConversations(String accountJid) async {
final db = GetIt.I.get<DatabaseService>().database;
final gs = GetIt.I.get<GroupchatService>();
final conversationsRaw = await db.query(
conversationsTable,
where: 'accountJid = ?',
whereArgs: [
accountJid,
],
orderBy: 'lastChangeTimestamp DESC',
);
final tmp = List<Conversation>.empty(growable: true);
for (final c in conversationsRaw) {
final jid = c['jid']! as String;
final rosterItem = await GetIt.I
.get<RosterService>()
.getRosterItemByJid(jid, accountJid);
Message? lastMessage;
if (c['lastMessageId'] != null) {
lastMessage = await GetIt.I.get<MessageService>().getMessageById(
c['lastMessageId']! as String,
accountJid,
queryReactionPreview: false,
);
}
GroupchatDetails? groupchatDetails;
if (c['type'] == ConversationType.groupchat.value) {
groupchatDetails = await gs.getGroupchatDetailsByJid(
c['jid']! as String,
accountJid,
);
}
tmp.add(
Conversation.fromDatabaseJson(
c,
rosterItem?.showAddToRosterButton ?? true,
lastMessage,
groupchatDetails,
),
);
}
return tmp;
}
/// Wrapper around DatabaseService's loadConversations that adds the loaded
/// to the cache.
Future<void> _loadConversations() async {
final conversations = await GetIt.I.get<DatabaseService>().loadConversations();
for (final c in conversations) {
_conversationCache.cache(c.id, c);
}
}
Future<void> _loadConversationsIfNeeded(String accountJid) async {
if (_conversationCache != null) return;
/// Returns the conversation with jid [jid] or null if not found.
Future<Conversation?> getConversationByJid(String jid) async {
if (!_loadedConversations) {
await _loadConversations();
_loadedConversations = true;
}
final conversations = await loadConversations(accountJid);
_conversationCache = Map<String, Conversation>.fromEntries(
conversations.map((c) => MapEntry(c.jid, c)),
);
return firstWhereOrNull(
// TODO(Unknown): Maybe have it accept an iterable
_conversationCache.getValues(),
(Conversation c) => c.jid == jid,
_log.finest(
'Conversation Cache loaded: ${_conversationCache!.keys.toList()}',
);
}
/// Returns the conversation by its database id or null if it does not exist.
Future<Conversation?> _getConversationById(int id) async {
if (!_loadedConversations) {
await _loadConversations();
_loadedConversations = true;
}
/// Returns the conversation with jid [jid] or null if not found.
Future<Conversation?> _getConversationByJid(
String jid,
String accountJid,
) async {
await _loadConversationsIfNeeded(accountJid);
return _conversationCache.getValue(id);
_log.finest('[_getConversationByJid] Requested JID $jid');
return _conversationCache![jid];
}
/// Wrapper around [ConversationService._getConversationByJid] that aquires
/// the lock for the cache.
Future<Conversation?> getConversationByJid(
String jid,
String accountJid,
) async {
return _lock
.synchronized(() async => _getConversationByJid(jid, accountJid));
}
/// For modifying the cache without writing it to disk. Useful, for example, when
/// changing the chat state.
void setConversation(Conversation conversation) {
_conversationCache.cache(conversation.id, conversation);
_conversationCache![conversation.jid] = conversation;
}
/// Wrapper around [DatabaseService]'s [updateConversation] that modifies the cache.
Future<Conversation> updateConversation(int id, {
String? lastMessageBody,
/// Updates the conversation with JID [jid] inside the database.
///
/// To prevent issues with the cache, only call from within
/// [ConversationService.createOrUpdateConversation].
Future<Conversation> updateConversation(
String jid,
String accountJid, {
int? lastChangeTimestamp,
bool? lastMessageRetracted,
int? lastMessageId,
Message? lastMessage,
bool? open,
int? unreadCounter,
String? avatarUrl,
String? avatarPath,
Object? avatarHash = notSpecified,
ChatState? chatState,
bool? muted,
bool? encrypted,
Object? contactId = notSpecified,
Object? contactAvatarPath = notSpecified,
Object? contactDisplayName = notSpecified,
GroupchatDetails? groupchatDetails,
bool? favourite,
}) async {
final conversation = await _getConversationById(id);
final newConversation = await GetIt.I.get<DatabaseService>().updateConversation(
id,
lastMessageBody: lastMessageBody,
lastMessageRetracted: lastMessageRetracted,
lastMessageId: lastMessageId,
lastChangeTimestamp: lastChangeTimestamp,
open: open,
unreadCounter: unreadCounter,
avatarUrl: avatarUrl,
chatState: conversation?.chatState ?? ChatState.gone,
muted: muted,
encrypted: encrypted,
final conversation = (await _getConversationByJid(jid, accountJid))!;
final c = <String, dynamic>{};
if (lastMessage != null) {
c['lastMessageId'] = lastMessage.id;
}
if (lastChangeTimestamp != null) {
c['lastChangeTimestamp'] = lastChangeTimestamp;
}
if (open != null) {
c['open'] = boolToInt(open);
}
if (unreadCounter != null) {
c['unreadCounter'] = unreadCounter;
}
if (avatarPath != null) {
c['avatarPath'] = avatarPath;
}
if (avatarHash != notSpecified) {
c['avatarHash'] = avatarHash as String?;
}
if (muted != null) {
c['muted'] = boolToInt(muted);
}
if (encrypted != null) {
c['encrypted'] = boolToInt(encrypted);
}
if (contactId != notSpecified) {
c['contactId'] = contactId as String?;
}
if (contactAvatarPath != notSpecified) {
c['contactAvatarPath'] = contactAvatarPath as String?;
}
if (contactDisplayName != notSpecified) {
c['contactDisplayName'] = contactDisplayName as String?;
}
if (favourite != null) {
c['favourite'] = boolToInt(favourite);
}
final result =
await GetIt.I.get<DatabaseService>().database.updateAndReturn(
conversationsTable,
c,
where: 'jid = ? AND accountJid = ?',
whereArgs: [jid, accountJid],
);
_conversationCache.cache(id, newConversation);
final rosterItem =
await GetIt.I.get<RosterService>().getRosterItemByJid(jid, accountJid);
var newConversation = Conversation.fromDatabaseJson(
result,
rosterItem?.showAddToRosterButton ?? true,
lastMessage,
groupchatDetails ?? conversation.groupchatDetails,
);
// Copy over the old lastMessage if a new one was not set
if (conversation.lastMessage != null && lastMessage == null) {
newConversation =
newConversation.copyWith(lastMessage: conversation.lastMessage);
}
_conversationCache![jid] = newConversation;
return newConversation;
}
/// Wrapper around [DatabaseService]'s [addConversationFromData] that updates the cache.
/// Creates a [Conversation] inside the database given the data. This is so that the
/// [Conversation] object can carry its database id.
///
/// To prevent issues with the cache, only call from within
/// [ConversationService.createOrUpdateConversation].
Future<Conversation> addConversationFromData(
String accountJid,
String title,
int lastMessageId,
bool lastMessageRetracted,
String lastMessageBody,
String avatarUrl,
Message? lastMessage,
ConversationType type,
String? avatarPath,
String jid,
int unreadCounter,
int lastChangeTimestamp,
bool open,
bool muted,
bool encrypted,
String? contactId,
String? contactAvatarPath,
String? contactDisplayName,
GroupchatDetails? groupchatDetails,
) async {
final newConversation = await GetIt.I.get<DatabaseService>().addConversationFromData(
final rosterItem =
await GetIt.I.get<RosterService>().getRosterItemByJid(jid, accountJid);
final gs = GetIt.I.get<GroupchatService>();
final newConversation = Conversation(
accountJid,
title,
lastMessageId,
lastMessageRetracted,
lastMessageBody,
avatarUrl,
lastMessage,
avatarPath,
null,
jid,
groupchatDetails,
unreadCounter,
type,
lastChangeTimestamp,
open,
rosterItem?.showAddToRosterButton ?? true,
muted,
encrypted,
ChatState.gone,
contactId: contactId,
contactAvatarPath: contactAvatarPath,
contactDisplayName: contactDisplayName,
);
await GetIt.I.get<DatabaseService>().database.insert(
conversationsTable,
newConversation.toDatabaseJson(),
);
if (_conversationCache != null) {
_conversationCache![newConversation.jid] = newConversation;
}
if (type == ConversationType.groupchat && groupchatDetails != null) {
await gs.addGroupchatDetailsFromData(
jid,
accountJid,
groupchatDetails.nick,
);
}
_conversationCache.cache(newConversation.id, newConversation);
return newConversation;
}
@@ -122,9 +422,44 @@ class ConversationService {
///
/// If the conversation does not exist, then the value of the preference for
/// enableOmemoByDefault is used.
Future<bool> shouldEncryptForConversation(JID jid) async {
Future<bool> shouldEncryptForConversation(JID jid, String accountJid) async {
final prefs = await GetIt.I.get<PreferencesService>().getPreferences();
final conversation = await getConversationByJid(jid.toString());
final conversation = await getConversationByJid(jid.toString(), accountJid);
return conversation?.encrypted ?? prefs.enableOmemoByDefault;
}
/// Send a chat state [state] to [jid], if certain pre-conditions are met:
/// - We have a network connection
/// - Sending chat markers/states are enabled
/// - [jid] != '' (not the self-chat)
/// [type] is the type of chat the chat state should be sent within.
Future<void> sendChatState(
ConversationType type,
String jid,
ChatState state,
) async {
if (type != ConversationType.chat) {
_log.finest('Not sending chat state because chat type is $type');
return;
}
final prefs = await GetIt.I.get<PreferencesService>().getPreferences();
// Only send chat states if the users wants to send them
if (!prefs.sendChatMarkers) return;
// Only send chat states when we're connected
// TODO(Unknown): Maybe queue it up intelligently
if (!(await GetIt.I.get<ConnectivityService>().hasConnection())) return;
final conn = GetIt.I.get<XmppConnection>();
await conn
.getManagerById<ChatStateManager>(chatStateManager)!
.sendChatState(
state,
jid,
messageType: type.toMessageType(),
);
}
}

View File

@@ -2,9 +2,8 @@ import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:logging/logging.dart';
import 'package:moxplatform/moxplatform.dart';
import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxy_native/moxxy_native.dart';
import 'package:moxxyv2/service/cryptography/types.dart';
List<int> _randomBuffer(int length) {
@@ -20,26 +19,35 @@ List<int> _randomBuffer(int length) {
CipherAlgorithm _sfsToCipher(SFSEncryptionType type) {
switch (type) {
case SFSEncryptionType.aes128GcmNoPadding: return CipherAlgorithm.aes128GcmNoPadding;
case SFSEncryptionType.aes256GcmNoPadding: return CipherAlgorithm.aes256GcmNoPadding;
case SFSEncryptionType.aes256CbcPkcs7: return CipherAlgorithm.aes256CbcPkcs7;
case SFSEncryptionType.aes128GcmNoPadding:
return CipherAlgorithm.aes128GcmNoPadding;
case SFSEncryptionType.aes256GcmNoPadding:
return CipherAlgorithm.aes256GcmNoPadding;
case SFSEncryptionType.aes256CbcPkcs7:
return CipherAlgorithm.aes256CbcPkcs7;
}
}
class CryptographyService {
/// Access to hardware-accelerated cryptography
final MoxxyCryptographyApi _api = MoxxyCryptographyApi();
CryptographyService() : _log = Logger('CryptographyService');
final Logger _log;
/// A logger.
final Logger _log = Logger('CryptographyService');
/// Encrypt the file at path [source] and write the encrypted data to [dest]. For the
/// encryption, use the algorithm indicated by [encryption].
Future<EncryptionResult> encryptFile(String source, String dest, SFSEncryptionType encryption) async {
Future<EncryptionResult> encryptFile(
String source,
String dest,
SFSEncryptionType encryption,
) async {
_log.finest('Beginning encryption routine for $source');
final key = encryption == SFSEncryptionType.aes128GcmNoPadding ?
_randomBuffer(16) :
_randomBuffer(32);
final key = encryption == SFSEncryptionType.aes128GcmNoPadding
? _randomBuffer(16)
: _randomBuffer(32);
final iv = _randomBuffer(12);
final result = (await MoxplatformPlugin.crypto.encryptFile(
final result = (await _api.encryptFile(
source,
dest,
Uint8List.fromList(key),
@@ -52,11 +60,11 @@ class CryptographyService {
return EncryptionResult(
key,
iv,
<String, String>{
hashSha256: base64Encode(result.plaintextHash),
<HashFunction, String>{
HashFunction.sha256: base64Encode(result.plaintextHash),
},
<String, String>{
hashSha256: base64Encode(result.ciphertextHash),
<HashFunction, String>{
HashFunction.sha256: base64Encode(result.ciphertextHash),
},
);
}
@@ -70,11 +78,11 @@ class CryptographyService {
SFSEncryptionType encryption,
List<int> key,
List<int> iv,
Map<String, String> plaintextHashes,
Map<String, String> ciphertextHashes,
Map<HashFunction, String> plaintextHashes,
Map<HashFunction, String> ciphertextHashes,
) async {
_log.finest('Beginning decryption for $source');
final result = await MoxplatformPlugin.crypto.encryptFile(
final result = await _api.decryptFile(
source,
dest,
Uint8List.fromList(key),
@@ -88,7 +96,7 @@ class CryptographyService {
var passedPlaintextIntegrityCheck = true;
var passedCiphertextIntegrityCheck = true;
for (final entry in plaintextHashes.entries) {
if (entry.key == hashSha256) {
if (entry.key == HashFunction.sha256) {
if (base64Encode(result!.plaintextHash) != entry.value) {
passedPlaintextIntegrityCheck = false;
} else {
@@ -98,8 +106,8 @@ class CryptographyService {
break;
}
}
for (final entry in ciphertextHashes.entries) {
if (entry.key == hashSha256) {
for (final entry in ciphertextHashes.entries) {
if (entry.key == HashFunction.sha256) {
if (base64Encode(result!.ciphertextHash) != entry.value) {
passedCiphertextIntegrityCheck = false;
} else {
@@ -131,7 +139,7 @@ class CryptographyService {
}
_log.finest('Beginning hash generation of $path');
final data = await MoxplatformPlugin.crypto.hashFile(path, hashSpec);
final data = await _api.hashFile(path, hashSpec);
_log.finest('Hash generation done for $path');
return base64Encode(data!);
}

View File

@@ -1,150 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:cryptography/cryptography.dart';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxyv2/service/cryptography/types.dart';
Future<List<int>> hashFileImpl(HashRequest request) async {
final data = await File(request.path).readAsBytes();
return CryptographicHashManager.hashFromData(data, request.hash);
}
Future<EncryptionResult> encryptFileImpl(EncryptionRequest request) async {
Cipher algorithm;
switch (request.encryption) {
case SFSEncryptionType.aes128GcmNoPadding:
algorithm = AesGcm.with128bits();
break;
case SFSEncryptionType.aes256GcmNoPadding:
algorithm = AesGcm.with256bits();
break;
case SFSEncryptionType.aes256CbcPkcs7:
// TODO(Unknown): Implement
throw Exception();
// ignore: dead_code
break;
}
// Generate a key and an IV for the file
final key = await algorithm.newSecretKey();
final iv = algorithm.newNonce();
final plaintext = await File(request.source).readAsBytes();
final secretBox = await algorithm.encrypt(
plaintext,
secretKey: key,
nonce: iv,
);
final ciphertext = [
...secretBox.cipherText,
...secretBox.mac.bytes,
];
// Write the file
await File(request.dest).writeAsBytes(ciphertext);
return EncryptionResult(
await key.extractBytes(),
iv,
{
hashSha256: base64Encode(
await CryptographicHashManager.hashFromData(plaintext, HashFunction.sha256),
),
},
{
hashSha256: base64Encode(
await CryptographicHashManager.hashFromData(ciphertext, HashFunction.sha256),
),
},
);
}
// TODO(PapaTutuWawa): Somehow fail when the ciphertext hash is not matching the provided data
Future<DecryptionResult> decryptFileImpl(DecryptionRequest request) async {
Cipher algorithm;
switch (request.encryption) {
case SFSEncryptionType.aes128GcmNoPadding:
algorithm = AesGcm.with128bits();
break;
case SFSEncryptionType.aes256GcmNoPadding:
algorithm = AesGcm.with256bits();
break;
case SFSEncryptionType.aes256CbcPkcs7:
// TODO(Unknown): Implement
throw Exception();
// ignore: dead_code
break;
}
final ciphertextRaw = await File(request.source).readAsBytes();
final mac = List<int>.empty(growable: true);
final ciphertext = List<int>.empty(growable: true);
// TODO(PapaTutuWawa): Somehow handle aes256CbcPkcs7
if (request.encryption == SFSEncryptionType.aes128GcmNoPadding ||
request.encryption == SFSEncryptionType.aes256GcmNoPadding) {
mac.addAll(ciphertextRaw.sublist(ciphertextRaw.length - 16));
ciphertext.addAll(ciphertextRaw.sublist(0, ciphertextRaw.length - 16));
}
var passedCiphertextIntegrityCheck = true;
var passedPlaintextIntegrityCheck = true;
// Try to find one hash we can verify
for (final entry in request.ciphertextHashes.entries) {
if ([hashSha256, hashSha512, hashBlake2b512].contains(entry.key)) {
final hash = await CryptographicHashManager.hashFromData(
ciphertext,
hashFunctionFromName(entry.key),
);
if (base64Encode(hash) == entry.value) {
passedCiphertextIntegrityCheck = true;
} else {
passedCiphertextIntegrityCheck = false;
}
break;
}
}
final secretBox = SecretBox(
ciphertext,
nonce: request.iv,
mac: Mac(mac),
);
try {
final data = await algorithm.decrypt(
secretBox,
secretKey: SecretKey(request.key),
);
for (final entry in request.plaintextHashes.entries) {
if ([hashSha256, hashSha512, hashBlake2b512].contains(entry.key)) {
final hash = await CryptographicHashManager.hashFromData(
data,
hashFunctionFromName(entry.key),
);
if (base64Encode(hash) == entry.value) {
passedPlaintextIntegrityCheck = true;
} else {
passedPlaintextIntegrityCheck = false;
}
break;
}
}
await File(request.dest).writeAsBytes(data);
} catch (_) {
return DecryptionResult(
false,
passedPlaintextIntegrityCheck,
passedCiphertextIntegrityCheck,
);
}
return DecryptionResult(
true,
passedPlaintextIntegrityCheck,
passedCiphertextIntegrityCheck,
);
}

View File

@@ -3,18 +3,21 @@ import 'package:moxxmpp/moxxmpp.dart';
@immutable
class EncryptionResult {
const EncryptionResult(this.key, this.iv, this.plaintextHashes, this.ciphertextHashes);
const EncryptionResult(
this.key,
this.iv,
this.plaintextHashes,
this.ciphertextHashes,
);
final List<int> key;
final List<int> iv;
final Map<String, String> plaintextHashes;
final Map<String, String> ciphertextHashes;
final Map<HashFunction, String> plaintextHashes;
final Map<HashFunction, String> ciphertextHashes;
}
@immutable
class EncryptionRequest {
const EncryptionRequest(this.source, this.dest, this.encryption);
final String source;
final String dest;
@@ -23,7 +26,6 @@ class EncryptionRequest {
@immutable
class DecryptionResult {
const DecryptionResult(
this.decryptionOkay,
this.plaintextOkay,
@@ -36,7 +38,6 @@ class DecryptionResult {
@immutable
class DecryptionRequest {
const DecryptionRequest(
this.source,
this.dest,
@@ -51,14 +52,6 @@ class DecryptionRequest {
final SFSEncryptionType encryption;
final List<int> key;
final List<int> iv;
final Map<String, String> plaintextHashes;
final Map<String, String> ciphertextHashes;
}
@immutable
class HashRequest {
const HashRequest(this.path, this.hash);
final String path;
final HashFunction hash;
final Map<HashFunction, String> plaintextHashes;
final Map<HashFunction, String> ciphertextHashes;
}

View File

@@ -1,15 +1,24 @@
const conversationsTable = 'Conversations';
const messsagesTable = 'Messages';
const messagesTable = 'Messages';
const rosterTable = 'RosterItems';
const mediaTable = 'SharedMedia';
const preferenceTable = 'Preferences';
const omemoDeviceTable = 'OmemoDevices';
const omemoDeviceListTable = 'OmemoDeviceList';
const omemoRatchetsTable = 'OmemoSessions';
const omemoTrustCacheTable = 'OmemoTrustCacheList';
const omemoTrustDeviceListTable = 'OmemoTrustDeviceList';
const omemoTrustEnableListTable = 'OmemoTrustEnableList';
const xmppStateTable = 'XmppState';
const contactsTable = 'Contacts';
const stickersTable = 'Stickers';
const stickerPacksTable = 'StickerPacks';
const blocklistTable = 'Blocklist';
const subscriptionsTable = 'SubscriptionRequests';
const fileMetadataTable = 'FileMetadata';
const fileMetadataHashesTable = 'FileMetadataHashes';
const reactionsTable = 'Reactions';
const omemoDevicesTable = 'OmemoDevices';
const omemoDeviceListTable = 'OmemoDeviceList';
const omemoRatchetsTable = 'OmemoRatchets';
const omemoTrustTable = 'OmemoTrustTable';
const notificationsTable = 'Notifications';
const groupchatTable = 'Groupchat';
const groupchatMembersTable = 'GroupchatMembers';
const typeString = 0;
const typeInt = 1;

View File

@@ -1,9 +1,10 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/helpers.dart';
import 'package:moxxyv2/shared/models/preference.dart';
import 'package:sqflite_sqlcipher/sqflite.dart';
Future<void> configureDatabase(Database db) async {
await db.execute('PRAGMA foreign_keys = ON');
await db.execute('PRAGMA foreign_keys = OFF');
}
Future<void> createDatabase(Database db, int version) async {
@@ -11,82 +12,171 @@ Future<void> createDatabase(Database db, int version) async {
await db.execute(
'''
CREATE TABLE $xmppStateTable (
key TEXT PRIMARY KEY,
value TEXT
key TEXT NOT NULL,
accountJid TEXT NOT NULL,
value TEXT,
PRIMARY KEY (key, accountJid)
)''',
);
// Messages
await db.execute(
'''
CREATE TABLE $messsagesTable (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sender TEXT NOT NULL,
body TEXT,
timestamp INTEGER NOT NULL,
sid TEXT NOT NULL,
conversationJid TEXT NOT NULL,
isMedia INTEGER NOT NULL,
CREATE TABLE $messagesTable (
id TEXT NOT NULL PRIMARY KEY,
accountJid TEXT NOT NULL,
sender TEXT NOT NULL,
body TEXT,
timestamp INTEGER NOT NULL,
sid TEXT NOT NULL,
conversationJid TEXT NOT NULL,
isFileUploadNotification INTEGER NOT NULL,
encrypted INTEGER NOT NULL,
errorType INTEGER,
warningType INTEGER,
mediaUrl TEXT,
mediaType TEXT,
thumbnailData TEXT,
mediaWidth INTEGER,
mediaHeight INTEGER,
srcUrl TEXT,
key TEXT,
iv TEXT,
encryptionScheme TEXT,
received INTEGER,
displayed INTEGER,
acked INTEGER,
originId TEXT,
quote_id INTEGER,
filename TEXT,
plaintextHashes TEXT,
ciphertextHashes TEXT,
isDownloading INTEGER NOT NULL,
isUploading INTEGER NOT NULL,
mediaSize INTEGER,
isRetracted INTEGER,
CONSTRAINT fk_quote FOREIGN KEY (quote_id) REFERENCES $messsagesTable (id)
encrypted INTEGER NOT NULL,
errorType INTEGER,
warningType INTEGER,
received INTEGER,
displayed INTEGER,
acked INTEGER,
originId TEXT,
quote_id TEXT,
file_metadata_id TEXT,
isDownloading INTEGER NOT NULL,
isUploading INTEGER NOT NULL,
isRetracted INTEGER,
isEdited INTEGER NOT NULL,
containsNoStore INTEGER NOT NULL,
stickerPackId TEXT,
occupantId TEXT,
pseudoMessageType INTEGER,
pseudoMessageData TEXT,
CONSTRAINT fk_quote
FOREIGN KEY (quote_id)
REFERENCES $messagesTable (id)
CONSTRAINT fk_file_metadata
FOREIGN KEY (file_metadata_id)
REFERENCES $fileMetadataTable (id)
)''',
);
await db.execute(
'CREATE INDEX idx_messages_id ON $messagesTable (id, sid, originId)',
);
// Reactions
await db.execute(
'''
CREATE TABLE $reactionsTable (
accountJid TEXT NOT NULL,
message_id TEXT NOT NULL,
senderJid TEXT NOT NULL,
emoji TEXT NOT NULL,
PRIMARY KEY (accountJid, senderJid, emoji, message_id),
CONSTRAINT fk_message
FOREIGN KEY (message_id)
REFERENCES $messagesTable (id)
ON DELETE CASCADE
)''',
);
await db.execute(
'CREATE INDEX idx_reactions_message_id ON $reactionsTable (message_id, accountJid, senderJid)',
);
// Notifications
await db.execute(
'''
CREATE TABLE $notificationsTable (
id INTEGER NOT NULL,
conversationJid TEXT NOT NULL,
accountJid TEXT NOT NULL,
sender TEXT,
senderJid TEXT,
avatarPath TEXT,
body TEXT NOT NULL,
mime TEXT,
path TEXT,
timestamp INTEGER NOT NULL,
PRIMARY KEY (id, conversationJid, senderJid, timestamp, accountJid)
)''',
);
await db.execute(
'CREATE INDEX idx_notifications ON $notificationsTable (conversationJid, accountJid)',
);
// File metadata
await db.execute(
'''
CREATE TABLE $fileMetadataTable (
id TEXT NOT NULL PRIMARY KEY,
path TEXT,
sourceUrls TEXT,
mimeType TEXT,
thumbnailType TEXT,
thumbnailData TEXT,
width INTEGER,
height INTEGER,
plaintextHashes TEXT,
encryptionKey TEXT,
encryptionIv TEXT,
encryptionScheme TEXT,
cipherTextHashes TEXT,
filename TEXT NOT NULL,
size INTEGER
)''',
);
await db.execute(
'''
CREATE TABLE $fileMetadataHashesTable (
algorithm TEXT NOT NULL,
value TEXT NOT NULL,
id TEXT NOT NULL,
CONSTRAINT f_primarykey PRIMARY KEY (algorithm, value),
CONSTRAINT fk_id FOREIGN KEY (id) REFERENCES $fileMetadataTable (id)
ON DELETE CASCADE
)''',
);
await db.execute(
'CREATE INDEX idx_file_metadata_message_id ON $fileMetadataTable (id)',
);
// Conversations
await db.execute(
'''
CREATE TABLE $conversationsTable (
id INTEGER PRIMARY KEY AUTOINCREMENT,
jid TEXT NOT NULL,
title TEXT NOT NULL,
avatarUrl TEXT NOT NULL,
jid TEXT NOT NULL,
accountJid TEXT NOT NULL,
title TEXT NOT NULL,
avatarPath TEXT,
avatarHash TEXT,
type TEXT NOT NULL,
lastChangeTimestamp INTEGER NOT NULL,
unreadCounter INTEGER NOT NULL,
lastMessageBody TEXT NOT NULL,
open INTEGER NOT NULL,
muted INTEGER NOT NULL,
encrypted INTEGER NOT NULL,
lastMessageId INTEGER NOT NULL,
lastMessageRetracted INTEGER NOT NULL,
unreadCounter INTEGER NOT NULL,
open INTEGER NOT NULL,
muted INTEGER NOT NULL,
encrypted INTEGER NOT NULL,
lastMessageId TEXT,
contactId TEXT,
contactAvatarPath TEXT,
contactDisplayName TEXT,
favourite INTEGER NOT NULL,
PRIMARY KEY (jid, accountJid),
CONSTRAINT fk_last_message
FOREIGN KEY (lastMessageId)
REFERENCES $messagesTable (id),
CONSTRAINT fk_contact_id
FOREIGN KEY (contactId)
REFERENCES $contactsTable (id)
ON DELETE SET NULL
)''',
);
await db.execute(
'CREATE INDEX idx_conversation_id ON $conversationsTable (jid, accountJid)',
);
// Shared media
// Contacts
await db.execute(
'''
CREATE TABLE $mediaTable (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT NOT NULL,
mime TEXT,
timestamp INTEGER NOT NULL,
conversation_id INTEGER NOT NULL,
message_id INTEGER,
FOREIGN KEY (conversation_id) REFERENCES $conversationsTable (id),
FOREIGN KEY (message_id) REFERENCES $messsagesTable (id)
CREATE TABLE $contactsTable (
id TEXT PRIMARY KEY,
jid TEXT NOT NULL
)''',
);
@@ -94,76 +184,121 @@ Future<void> createDatabase(Database db, int version) async {
await db.execute(
'''
CREATE TABLE $rosterTable (
id INTEGER PRIMARY KEY AUTOINCREMENT,
jid TEXT NOT NULL,
title TEXT NOT NULL,
avatarUrl TEXT NOT NULL,
avatarHash TEXT NOT NULL,
subscription TEXT NOT NULL,
ask TEXT NOT NULL
jid TEXT NOT NULL,
accountJid TEXT NOT NULL,
title TEXT NOT NULL,
avatarPath TEXT,
avatarHash TEXT,
subscription TEXT NOT NULL,
ask TEXT NOT NULL,
contactId TEXT,
contactAvatarPath TEXT,
contactDisplayName TEXT,
pseudoRosterItem INTEGER NOT NULL,
CONSTRAINT fk_contact_id
FOREIGN KEY (contactId)
REFERENCES $contactsTable (id)
ON DELETE SET NULL
)''',
);
// Stickers
await db.execute(
'''
CREATE TABLE $stickersTable (
id TEXT PRIMARY KEY,
desc TEXT NOT NULL,
suggests TEXT NOT NULL,
file_metadata_id TEXT NOT NULL,
stickerPackId TEXT NOT NULL,
CONSTRAINT fk_sticker_pack FOREIGN KEY (stickerPackId) REFERENCES $stickerPacksTable (id)
ON DELETE CASCADE,
CONSTRAINT fk_file_metadata FOREIGN KEY (file_metadata_id) REFERENCES $fileMetadataTable (id)
)''',
);
await db.execute(
'''
CREATE TABLE $stickerPacksTable (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT NOT NULL,
hashAlgorithm TEXT NOT NULL,
hashValue TEXT NOT NULL,
restricted INTEGER NOT NULL,
addedTimestamp INTEGER NOT NULL
)''',
);
// Blocklist
await db.execute(
'''
CREATE TABLE $blocklistTable (
jid TEXT NOT NULL,
accountJid TEXT NOT NULL,
PRIMARY KEY (accountJid, jid)
);
''',
);
// OMEMO
await db.execute(
'''
CREATE TABLE $omemoRatchetsTable (
id INTEGER NOT NULL,
CREATE TABLE $omemoDevicesTable (
jid TEXT NOT NULL PRIMARY KEY,
id INTEGER NOT NULL,
ikPub TEXT NOT NULL,
ik TEXT NOT NULL,
spkPub TEXT NOT NULL,
spk TEXT NOT NULL,
spkId INTEGER NOT NULL,
spkSig TEXT NOT NULL,
oldSpkPub TEXT,
oldSpk TEXT,
oldSpkId INTEGER,
opks TEXT NOT NULL
)''',
);
await db.execute(
'''
CREATE TABLE $omemoDeviceListTable (
jid TEXT NOT NULL,
accountJid TEXT NOT NULL,
devices TEXT NOT NULL,
PRIMARY KEY (accountJid, jid)
)''',
);
await db.execute(
'''
CREATE TABLE $omemoRatchetsTable (
jid TEXT NOT NULL,
accountJid TEXT NOT NULL,
device INTEGER NOT NULL,
dhsPub TEXT NOT NULL,
dhs TEXT NOT NULL,
dhs_pub TEXT NOT NULL,
dhr TEXT,
dhrPub TEXT,
rk TEXT NOT NULL,
cks TEXT,
ckr TEXT,
ns INTEGER NOT NULL,
nr INTEGER NOT NULL,
pn INTEGER NOT NULL,
ik_pub TEXT NOT NULL,
session_ad TEXT NOT NULL,
acknowledged INTEGER NOT NULL,
mkskipped TEXT NOT NULL,
kex_timestamp INTEGER NOT NULL,
kex TEXT,
PRIMARY KEY (jid, id)
ik TEXT NOT NULL,
ad TEXT NOT NULL,
skipped TEXT NOT NULL,
kex TEXT NOT NULL,
acked INTEGER NOT NULL,
PRIMARY KEY (accountJid, jid, device)
)''',
);
await db.execute(
'''
CREATE TABLE $omemoTrustCacheTable (
key TEXT PRIMARY KEY NOT NULL,
trust INTEGER NOT NULL
)''',
);
await db.execute(
'''
CREATE TABLE $omemoTrustDeviceListTable (
jid TEXT NOT NULL,
device INTEGER NOT NULL
)''',
);
await db.execute(
'''
CREATE TABLE $omemoTrustEnableListTable (
key TEXT PRIMARY KEY NOT NULL,
enabled INTEGER NOT NULL
)''',
);
await db.execute(
'''
CREATE TABLE $omemoDeviceTable (
jid TEXT NOT NULL,
id INTEGER NOT NULL,
data TEXT NOT NULL,
PRIMARY KEY (jid, id)
)''',
);
await db.execute(
'''
CREATE TABLE $omemoDeviceListTable (
jid TEXT NOT NULL,
id INTEGER NOT NULL,
PRIMARY KEY (jid, id)
CREATE TABLE $omemoTrustTable (
jid TEXT NOT NULL,
accountJid TEXT NOT NULL,
device INTEGER NOT NULL,
trust INTEGER NOT NULL,
enabled INTEGER NOT NULL,
PRIMARY KEY (accountJid, jid, device)
)''',
);
@@ -173,9 +308,25 @@ Future<void> createDatabase(Database db, int version) async {
CREATE TABLE $preferenceTable (
key TEXT NOT NULL PRIMARY KEY,
type INTEGER NOT NULL,
value TEXT NOT NULL
value TEXT NULL
)''',
);
// Groupchat
await db.execute(
'''
CREATE TABLE $groupchatTable (
jid TEXT NOT NULL,
accountJid TEXT NOT NULL,
nick TEXT NOT NULL,
PRIMARY KEY (jid, accountJid),
CONSTRAINT fk_groupchat
FOREIGN KEY (jid, accountJid)
REFERENCES $conversationsTable (jid, accountJid)
ON DELETE CASCADE
)''',
);
await db.insert(
preferenceTable,
Preference(
@@ -229,7 +380,7 @@ Future<void> createDatabase(Database db, int version) async {
Preference(
'backgroundPath',
typeString,
'',
null,
).toDatabaseJson(),
);
await db.insert(
@@ -240,14 +391,6 @@ Future<void> createDatabase(Database db, int version) async {
'true',
).toDatabaseJson(),
);
await db.insert(
preferenceTable,
Preference(
'autoAcceptSubscriptionRequests',
typeBool,
'false',
).toDatabaseJson(),
);
await db.insert(
preferenceTable,
Preference(
@@ -336,4 +479,28 @@ Future<void> createDatabase(Database db, int version) async {
'default',
).toDatabaseJson(),
);
await db.insert(
preferenceTable,
Preference(
'enableContactIntegration',
typeBool,
'false',
).toDatabaseJson(),
);
await db.insert(
preferenceTable,
Preference(
'isStickersNodePublic',
typeBool,
'true',
).toDatabaseJson(),
);
await db.insert(
preferenceTable,
Preference(
'showDebugMenu',
typeBool,
boolToString(false),
).toDatabaseJson(),
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,3 +7,17 @@ bool stringToBool(String s) => s == 'true' ? true : false;
String intToString(int i) => '$i';
int stringToInt(String s) => int.parse(s);
/// Given a map [map], extract all key-value pairs from [map] where the key starts with
/// [prefix]. Combine those key-value pairs into a new map, where the leading [prefix]
/// is removed from all key names.
Map<String, T> getPrefixedSubMap<T>(Map<String, T> map, String prefix) {
return Map<String, T>.fromEntries(
map.entries.where((entry) => entry.key.startsWith(prefix)).map(
(entry) => MapEntry<String, T>(
entry.key.substring(prefix.length),
entry.value,
),
),
);
}

View File

@@ -0,0 +1,55 @@
import 'package:logging/logging.dart';
/// A function to be called when a migration should be performed.
typedef MigrationCallback<T> = Future<void> Function(T);
/// This class represents a single database migration.
class Migration<T> {
const Migration(this.version, this.migration);
/// The version this migration upgrades the database to.
final int version;
/// The migration callback. Called the the database version is less than [version].
final MigrationCallback<T> migration;
}
/// Given the migration [param], which is passed to every migration, with the current version
/// [version], goes through the list of
/// migrations [migrations] and applies all migrations with a version greater than
/// [version]. [migrations] is sorted before usage.
///
/// NOTE: This entire setup is written as a generic to make testing easier. We cannot easily
/// mock, or better "instantiate", a Database object. Thus, to avoid having nullable
/// database argument, just pass in whatever (the tests use an integer).
Future<void> runMigrations<T>(
Logger log,
T param,
List<Migration<T>> migrations,
int version,
String typeName, {
Future<void> Function(int)? commitVersion,
}) async {
final sortedMigrations = List<Migration<T>>.from(migrations)
..sort(
(a, b) => a.version.compareTo(b.version),
);
var currentVersion = version;
var hasRunMigration = false;
for (final migration in sortedMigrations) {
if (version < migration.version) {
log.info(
'Running $typeName migration $currentVersion -> ${migration.version}',
);
await migration.migration(param);
currentVersion = migration.version;
hasRunMigration = true;
}
}
// Commit the version, if specified.
if (commitVersion != null && hasRunMigration) {
log.info('Committing migration version $currentVersion');
await commitVersion(currentVersion);
}
}

View File

@@ -0,0 +1,14 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
Future<void> upgradeFromV22ToV23(DatabaseMigrationData data) async {
final (db, _) = data;
await db.execute(
'''
CREATE TABLE $blocklistTable (
jid TEXT PRIMARY KEY
);
''',
);
}

View File

@@ -0,0 +1,74 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/shared/models/preference.dart';
Future<void> upgradeFromV13ToV14(DatabaseMigrationData data) async {
final (db, _) = data;
// Create the new table
await db.execute('''
CREATE TABLE $contactsTable (
id TEXT PRIMARY KEY,
jid TEXT NOT NULL
)''');
// Migrate the conversations
await db.execute(
'''
CREATE TABLE ${conversationsTable}_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
jid TEXT NOT NULL,
title TEXT NOT NULL,
avatarUrl TEXT NOT NULL,
lastChangeTimestamp INTEGER NOT NULL,
unreadCounter INTEGER NOT NULL,
open INTEGER NOT NULL,
muted INTEGER NOT NULL,
encrypted INTEGER NOT NULL,
lastMessageId INTEGER,
contactId TEXT,
CONSTRAINT fk_last_message FOREIGN KEY (lastMessageId) REFERENCES $messagesTable (id),
CONSTRAINT fk_contact_id FOREIGN KEY (contactId) REFERENCES $contactsTable (id)
ON DELETE SET NULL
)''',
);
await db.execute(
'INSERT INTO ${conversationsTable}_new SELECT *, NULL from $conversationsTable',
);
await db.execute('DROP TABLE $conversationsTable;');
await db.execute(
'ALTER TABLE ${conversationsTable}_new RENAME TO $conversationsTable;',
);
// Migrate the roster items
await db.execute(
'''
CREATE TABLE ${rosterTable}_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
jid TEXT NOT NULL,
title TEXT NOT NULL,
avatarUrl TEXT NOT NULL,
avatarHash TEXT NOT NULL,
subscription TEXT NOT NULL,
ask TEXT NOT NULL,
contactId TEXT,
CONSTRAINT fk_contact_id FOREIGN KEY (contactId) REFERENCES $contactsTable (id)
ON DELETE SET NULL
)''',
);
await db.execute(
'INSERT INTO ${rosterTable}_new SELECT *, NULL from $rosterTable',
);
await db.execute('DROP TABLE $rosterTable;');
await db.execute('ALTER TABLE ${rosterTable}_new RENAME TO $rosterTable;');
// Introduce the new preference key
await db.insert(
preferenceTable,
Preference(
'enableContactIntegration',
typeBool,
'false',
).toDatabaseJson(),
);
}

View File

@@ -0,0 +1,19 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
Future<void> upgradeFromV14ToV15(DatabaseMigrationData data) async {
final (db, _) = data;
await db.execute(
'ALTER TABLE $conversationsTable ADD COLUMN contactAvatarPath TEXT DEFAULT NULL;',
);
await db.execute(
'ALTER TABLE $rosterTable ADD COLUMN contactAvatarPath TEXT DEFAULT NULL;',
);
await db.execute(
'ALTER TABLE $conversationsTable ADD COLUMN contactDisplayName TEXT DEFAULT NULL;',
);
await db.execute(
'ALTER TABLE $rosterTable ADD COLUMN contactDisplayName TEXT DEFAULT NULL;',
);
}

View File

@@ -0,0 +1,11 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/service/database/helpers.dart';
Future<void> upgradeFromV15ToV16(DatabaseMigrationData data) async {
final (db, _) = data;
await db.execute(
'ALTER TABLE $rosterTable ADD COLUMN pseudoRosterItem INTEGER NOT NULL DEFAULT ${boolToInt(false)};',
);
}

View File

@@ -0,0 +1,13 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
Future<void> upgradeFromV6ToV7(DatabaseMigrationData data) async {
final (db, _) = data;
await db.execute(
'ALTER TABLE $conversationsTable ADD COLUMN lastMessageState INTEGER NOT NULL DEFAULT 0;',
);
await db.execute(
"ALTER TABLE $conversationsTable ADD COLUMN lastMessageSender TEXT NOT NULL DEFAULT '';",
);
}

View File

@@ -0,0 +1,19 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
Future<void> upgradeFromV7ToV8(DatabaseMigrationData data) async {
final (db, _) = data;
await db.execute(
'ALTER TABLE $conversationsTable DROP COLUMN lastMessageState;',
);
await db.execute(
'ALTER TABLE $conversationsTable DROP COLUMN lastMessageSender;',
);
await db.execute(
'ALTER TABLE $conversationsTable DROP COLUMN lastMessageBody;',
);
await db.execute(
'ALTER TABLE $conversationsTable DROP COLUMN lastMessageRetracted;',
);
}

View File

@@ -0,0 +1,49 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
Future<void> upgradeFromV8ToV9(DatabaseMigrationData data) async {
final (db, _) = data;
// Step 1
//await db.execute('PRAGMA foreign_keys = 0;');
// Step 2
// Step 4
await db.execute(
'''
CREATE TABLE ${conversationsTable}_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
jid TEXT NOT NULL,
title TEXT NOT NULL,
avatarUrl TEXT NOT NULL,
lastChangeTimestamp INTEGER NOT NULL,
unreadCounter INTEGER NOT NULL,
open INTEGER NOT NULL,
muted INTEGER NOT NULL,
encrypted INTEGER NOT NULL,
lastMessageId INTEGER,
CONSTRAINT fk_last_message FOREIGN KEY (lastMessageId) REFERENCES $messagesTable (id)
)''',
);
// Step 5
await db.execute(
'INSERT INTO ${conversationsTable}_new SELECT * from $conversationsTable',
);
// Step 6
await db.execute('DROP TABLE $conversationsTable;');
// Step 7
await db.execute(
'ALTER TABLE ${conversationsTable}_new RENAME TO $conversationsTable;',
);
// Step 10
//await db.execute('PRAGMA foreign_key_check;');
// Step 11
// Step 12
//await db.execute('PRAGMA foreign_keys=ON;');
}

View File

@@ -1,8 +1,10 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/shared/models/preference.dart';
import 'package:sqflite_sqlcipher/sqflite.dart';
Future<void> upgradeFromV2ToV3(Database db) async {
Future<void> upgradeFromV2ToV3(DatabaseMigrationData data) async {
final (db, _) = data;
// Set a default locale
await db.insert(
preferenceTable,

View File

@@ -0,0 +1,12 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/service/database/helpers.dart';
Future<void> upgradeFromV9ToV10(DatabaseMigrationData data) async {
final (db, _) = data;
// Mark all messages as not edited
await db.execute(
'ALTER TABLE $messagesTable ADD COLUMN isEdited INTEGER NOT NULL DEFAULT ${boolToInt(false)};',
);
}

View File

@@ -0,0 +1,15 @@
import 'package:moxxyv2/service/database/database.dart';
Future<void> upgradeFromV12ToV13(DatabaseMigrationData data) async {
final (db, _) = data;
await db.execute(
'''
CREATE TABLE OmemoFingerprintCache (
jid TEXT NOT NULL,
id INTEGER NOT NULL,
fingerprint TEXT NOT NULL,
PRIMARY KEY (jid, id)
)''',
);
}

View File

@@ -0,0 +1,13 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
Future<void> upgradeFromV23ToV24(DatabaseMigrationData data) async {
final (db, _) = data;
await db.execute(
'ALTER TABLE $messagesTable ADD COLUMN pseudoMessageType INTEGER;',
);
await db.execute(
'ALTER TABLE $messagesTable ADD COLUMN pseudoMessageData TEXT;',
);
}

View File

@@ -0,0 +1,10 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
Future<void> upgradeFromV10ToV11(DatabaseMigrationData data) async {
final (db, _) = data;
await db.execute(
"ALTER TABLE $messagesTable ADD COLUMN reactions TEXT NOT NULL DEFAULT '[]';",
);
}

View File

@@ -0,0 +1,11 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/service/database/helpers.dart';
Future<void> upgradeFromV11ToV12(DatabaseMigrationData data) async {
final (db, _) = data;
await db.execute(
'ALTER TABLE $messagesTable ADD COLUMN containsNoStore INTEGER NOT NULL DEFAULT ${boolToInt(false)};',
);
}

View File

@@ -1,11 +1,12 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/service/database/helpers.dart';
import 'package:moxxyv2/shared/models/preference.dart';
import 'package:sqflite_sqlcipher/sqflite.dart';
Future<void> upgradeFromV3ToV4(Database db) async {
Future<void> upgradeFromV3ToV4(DatabaseMigrationData data) async {
final (db, _) = data;
// Mark all messages as not retracted
await db.execute(
'ALTER TABLE $messsagesTable ADD COLUMN isRetracted INTEGER DEFAULT ${boolToInt(false)};',
'ALTER TABLE $messagesTable ADD COLUMN isRetracted INTEGER DEFAULT ${boolToInt(false)};',
);
}

View File

@@ -1,9 +1,10 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/service/database/helpers.dart';
import 'package:moxxyv2/shared/models/preference.dart';
import 'package:sqflite_sqlcipher/sqflite.dart';
Future<void> upgradeFromV4ToV5(Database db) async {
Future<void> upgradeFromV4ToV5(DatabaseMigrationData data) async {
final (db, _) = data;
// Give all conversations a pseudo last message data
await db.execute(
'ALTER TABLE $conversationsTable ADD COLUMN lastMessageId INTEGER NOT NULL DEFAULT 0;',

View File

@@ -1,10 +1,11 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/shared/models/preference.dart';
import 'package:sqflite_sqlcipher/sqflite.dart';
import 'package:moxxyv2/service/database/database.dart';
Future<void> upgradeFromV5ToV6(DatabaseMigrationData data) async {
final (db, _) = data;
Future<void> upgradeFromV5ToV6(Database db) async {
// Allow shared media to reference a message
await db.execute(
'ALTER TABLE $mediaTable ADD COLUMN message_id INTEGER REFERENCES $messsagesTable (id);',
'ALTER TABLE $mediaTable ADD COLUMN message_id INTEGER REFERENCES $messagesTable (id);',
);
}

View File

@@ -0,0 +1,61 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/shared/models/preference.dart';
Future<void> upgradeFromV16ToV17(DatabaseMigrationData data) async {
final (db, _) = data;
await db.execute(
'''
CREATE TABLE $stickersTable (
id INTEGER PRIMARY KEY AUTOINCREMENT,
mediaType TEXT NOT NULL,
desc TEXT NOT NULL,
size INTEGER NOT NULL,
width INTEGER,
height INTEGER,
hashes TEXT NOT NULL,
urlSources TEXT NOT NULL,
path TEXT NOT NULL,
stickerPackId TEXT NOT NULL,
CONSTRAINT fk_sticker_pack FOREIGN KEY (stickerPackId) REFERENCES $stickerPacksTable (id)
ON DELETE CASCADE
)''',
);
await db.execute(
'''
CREATE TABLE $stickerPacksTable (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT NOT NULL,
hashAlgorithm TEXT NOT NULL,
hashValue TEXT NOT NULL
)''',
);
// Add the sticker attributes to Messages
await db.execute(
'ALTER TABLE $messagesTable ADD COLUMN stickerPackId TEXT;',
);
await db.execute(
'ALTER TABLE $messagesTable ADD COLUMN stickerId INTEGER;',
);
// Add the new preferences
await db.insert(
preferenceTable,
Preference(
'enableStickers',
typeBool,
'true',
).toDatabaseJson(),
);
await db.insert(
preferenceTable,
Preference(
'autoDownloadStickersFromContacts',
typeBool,
'true',
).toDatabaseJson(),
);
}

View File

@@ -0,0 +1,47 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
Future<void> upgradeFromV17ToV18(DatabaseMigrationData data) async {
final (db, _) = data;
// Update messages
await db.execute(
'ALTER TABLE $messagesTable DROP COLUMN stickerId;',
);
await db.execute(
'ALTER TABLE $messagesTable ADD COLUMN stickerHashKey TEXT;',
);
// Drop stickers
await db.execute('DROP TABLE $stickerPacksTable;');
await db.execute('DROP TABLE $stickersTable;');
await db.execute(
'''
CREATE TABLE $stickersTable (
hashKey TEXT PRIMARY KEY,
mediaType TEXT NOT NULL,
desc TEXT NOT NULL,
size INTEGER NOT NULL,
width INTEGER,
height INTEGER,
hashes TEXT NOT NULL,
urlSources TEXT NOT NULL,
path TEXT NOT NULL,
stickerPackId TEXT NOT NULL,
CONSTRAINT fk_sticker_pack FOREIGN KEY (stickerPackId) REFERENCES $stickerPacksTable (id)
ON DELETE CASCADE
)''',
);
await db.execute(
'''
CREATE TABLE $stickerPacksTable (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT NOT NULL,
hashAlgorithm TEXT NOT NULL,
hashValue TEXT NOT NULL,
stickerHashKey TEXT NOT NULL
)''',
);
}

View File

@@ -0,0 +1,10 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
Future<void> upgradeFromV18ToV19(DatabaseMigrationData data) async {
final (db, _) = data;
await db.execute(
'ALTER TABLE $stickerPacksTable DROP COLUMN stickerHashKey;',
);
}

View File

@@ -0,0 +1,14 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/service/database/helpers.dart';
Future<void> upgradeFromV19ToV20(DatabaseMigrationData data) async {
final (db, _) = data;
await db.execute(
'ALTER TABLE $stickerPacksTable ADD COLUMN restricted DEFAULT ${boolToInt(false)};',
);
await db.execute(
'ALTER TABLE $stickersTable ADD COLUMN suggests DEFAULT "";',
);
}

View File

@@ -0,0 +1,21 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/service/database/helpers.dart';
Future<void> upgradeFromV20ToV21(DatabaseMigrationData data) async {
final (db, _) = data;
await db.execute(
'ALTER TABLE $stickerPacksTable DROP COLUMN restricted;',
);
await db.execute(
'ALTER TABLE $stickersTable DROP COLUMN suggests;',
);
await db.execute(
'ALTER TABLE $stickerPacksTable ADD COLUMN restricted INTEGER NOT NULL DEFAULT ${boolToInt(false)};',
);
await db.execute(
'ALTER TABLE $stickersTable ADD COLUMN suggests TEXT NOT NULL DEFAULT "";',
);
}

View File

@@ -0,0 +1,14 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
Future<void> upgradeFromV21ToV22(DatabaseMigrationData data) async {
final (db, _) = data;
await db.execute(
'ALTER TABLE $stickersTable DROP COLUMN suggests;',
);
await db.execute(
'ALTER TABLE $stickersTable ADD COLUMN suggests TEXT NOT NULL DEFAULT "{}";',
);
}

View File

@@ -0,0 +1,16 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/shared/models/preference.dart';
Future<void> upgradeFromV24ToV25(DatabaseMigrationData data) async {
final (db, _) = data;
await db.insert(
preferenceTable,
Preference(
'isStickersNodePublic',
typeBool,
'true',
).toDatabaseJson(),
);
}

View File

@@ -1,7 +1,9 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:sqflite_sqlcipher/sqflite.dart';
import 'package:moxxyv2/service/database/database.dart';
Future<void> upgradeFromV1ToV2(DatabaseMigrationData data) async {
final (db, _) = data;
Future<void> upgradeFromV1ToV2(Database db) async {
// Create the table
await db.execute(
'''

View File

@@ -0,0 +1,39 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:sqflite_sqlcipher/sqflite.dart';
Future<void> upgradeFromV29ToV30(DatabaseMigrationData data) async {
final (db, _) = data;
await db.execute(
'ALTER TABLE $conversationsTable ADD COLUMN sharedMediaAmount INTEGER NOT NULL DEFAULT 0;',
);
// Get all conversations
final conversations = await db.query(
conversationsTable,
);
for (final conversation in conversations) {
// Count the amount of shared media
final jid = conversation['jid']! as String;
final result = Sqflite.firstIntValue(
await db.rawQuery(
'SELECT COUNT(*) FROM $mediaTable WHERE conversation_jid = ?',
[jid],
),
) ??
0;
final c = Map<String, Object?>.from(conversation)..remove('id');
await db.update(
conversationsTable,
{
...c,
'sharedMediaAmount': result,
},
where: 'jid = ?',
whereArgs: [jid],
);
}
}

View File

@@ -0,0 +1,79 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
Future<void> upgradeFromV27ToV28(DatabaseMigrationData data) async {
final (db, _) = data;
// Collect conversations so that we have a mapping id -> jid
final idMap = <int, String>{};
final conversations = await db.query(conversationsTable);
for (final c in conversations) {
idMap[c['id']! as int] = c['jid']! as String;
}
// Migrate the conversations
await db.execute(
'''
CREATE TABLE ${conversationsTable}_new (
jid TEXT NOT NULL PRIMARY KEY,
title TEXT NOT NULL,
avatarUrl TEXT NOT NULL,
lastChangeTimestamp INTEGER NOT NULL,
unreadCounter INTEGER NOT NULL,
open INTEGER NOT NULL,
muted INTEGER NOT NULL,
encrypted INTEGER NOT NULL,
lastMessageId INTEGER,
contactId TEXT,
contactAvatarPath TEXT,
contactDisplayName TEXT,
CONSTRAINT fk_last_message FOREIGN KEY (lastMessageId) REFERENCES $messagesTable (id),
CONSTRAINT fk_contact_id FOREIGN KEY (contactId) REFERENCES $contactsTable (id)
ON DELETE SET NULL
)''',
);
await db.execute(
'INSERT INTO ${conversationsTable}_new SELECT jid, title, avatarUrl, lastChangeTimestamp, unreadCounter, open, muted, encrypted, lastMessageId, contactid, contactAvatarPath, contactDisplayName from $conversationsTable',
);
await db.execute('DROP TABLE $conversationsTable;');
await db.execute(
'ALTER TABLE ${conversationsTable}_new RENAME TO $conversationsTable;',
);
// Add the jid column to shared media
await db.execute(
"ALTER TABLE $mediaTable ADD COLUMN conversation_jid TEXT NOT NULL DEFAULT '';",
);
// Update all shared media items
for (final entry in idMap.entries) {
await db.update(
mediaTable,
{
'conversation_jid': entry.value,
},
where: 'conversation_id = ?',
whereArgs: [entry.key],
);
}
// Migrate shared media
await db.execute(
'''
CREATE TABLE ${mediaTable}_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT NOT NULL,
mime TEXT,
timestamp INTEGER NOT NULL,
conversation_jid TEXT NOT NULL,
message_id INTEGER,
FOREIGN KEY (conversation_jid) REFERENCES $conversationsTable (jid),
FOREIGN KEY (message_id) REFERENCES $messagesTable (id)
)''',
);
await db.execute(
'INSERT INTO ${mediaTable}_new SELECT id, path, mime, timestamp, message_id, conversation_jid from $mediaTable',
);
await db.execute('DROP TABLE $mediaTable;');
await db.execute('ALTER TABLE ${mediaTable}_new RENAME TO $mediaTable;');
}

View File

@@ -0,0 +1,10 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
Future<void> upgradeFromV30ToV31(DatabaseMigrationData data) async {
final (db, _) = data;
await db.execute(
'ALTER TABLE $conversationsTable ADD COLUMN type TEXT NOT NULL DEFAULT "chat";',
);
}

View File

@@ -0,0 +1,17 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/service/database/helpers.dart';
import 'package:moxxyv2/shared/models/preference.dart';
Future<void> upgradeFromV25ToV26(DatabaseMigrationData data) async {
final (db, _) = data;
await db.insert(
preferenceTable,
Preference(
'showDebugMenu',
typeBool,
boolToString(false),
).toDatabaseJson(),
);
}

View File

@@ -0,0 +1,11 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
Future<void> upgradeFromV28ToV29(DatabaseMigrationData data) async {
final (db, _) = data;
await db.delete(
preferenceTable,
where: 'key = "autoAcceptSubscriptionRequests"',
);
}

View File

@@ -0,0 +1,11 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
Future<void> upgradeFromV26ToV27(DatabaseMigrationData data) async {
final (db, _) = data;
await db.execute('''
CREATE TABLE $subscriptionsTable(
jid TEXT PRIMARY KEY
)''');
}

View File

@@ -0,0 +1,228 @@
import 'dart:convert';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/service/database/helpers.dart';
import 'package:moxxyv2/service/files.dart';
import 'package:moxxyv2/shared/models/file_metadata.dart';
import 'package:path/path.dart' as path;
Future<void> upgradeFromV31ToV32(DatabaseMigrationData data) async {
final (db, _) = data;
// Create the tracking table
await db.execute('''
CREATE TABLE $fileMetadataTable (
id TEXT NOT NULL PRIMARY KEY,
path TEXT,
sourceUrls TEXT,
mimeType TEXT,
thumbnailType TEXT,
thumbnailData TEXT,
width INTEGER,
height INTEGER,
plaintextHashes TEXT,
encryptionKey TEXT,
encryptionIv TEXT,
encryptionScheme TEXT,
cipherTextHashes TEXT,
filename TEXT NOT NULL,
size INTEGER
)''');
await db.execute('''
CREATE TABLE $fileMetadataHashesTable (
algorithm TEXT NOT NULL,
value TEXT NOT NULL,
id TEXT NOT NULL,
CONSTRAINT f_primarykey PRIMARY KEY (algorithm, value),
CONSTRAINT fk_id FOREIGN KEY (id) REFERENCES $fileMetadataTable (id)
ON DELETE CASCADE
)''');
// Add the file_metadata_id column
await db.execute(
'ALTER TABLE $messagesTable ADD COLUMN file_metadata_id TEXT DEFAULT NULL;',
);
// Migrate the media messages' attributes to new table
final messages = await db.query(
messagesTable,
where: 'isMedia = ${boolToInt(true)}',
);
for (final message in messages) {
// Do we know of a hash?
String id;
if (message['plaintextHashes'] != null) {
// Plaintext hashes available (SFS)
final plaintextHashes = deserializeHashMap(
message['plaintextHashes']! as String,
);
final result = await db.query(
fileMetadataHashesTable,
where: 'algorithm = ? AND value = ?',
whereArgs: [
plaintextHashes.entries.first.key,
plaintextHashes.entries.first.value,
],
limit: 1,
);
if (result.isEmpty) {
final metadata = FileMetadata(
getStrongestHashFromMap(plaintextHashes) ??
DateTime.now().millisecondsSinceEpoch.toString(),
message['mediaUrl'] as String?,
message['srcUrl'] != null ? [message['srcUrl']! as String] : null,
message['mediaType'] as String?,
message['mediaSize'] as int?,
message['thumbnailData'] != null ? 'blurhash' : null,
message['thumbnailData'] as String?,
message['mediaWidth'] as int?,
message['mediaHeight'] as int?,
plaintextHashes,
message['key'] as String?,
message['iv'] as String?,
message['encryptionScheme'] as String?,
message['plaintextHashes'] == null
? null
: deserializeHashMap(message['ciphertextHashes']! as String),
message['filename']! as String,
);
// Create the metadata
await db.insert(
fileMetadataTable,
metadata.toDatabaseJson(),
);
id = metadata.id;
} else {
id = result[0]['id']! as String;
}
} else {
// No plaintext hashes are available (OOB data)
int? size;
int? height;
int? width;
Map<HashFunction, String>? hashes;
String? filePath;
String? urlSource;
String? mediaType;
String? filename;
if (message['filename'] == null) {
// We are dealing with a sticker
assert(
message['stickerPackId'] != null,
'The message must contain a sticker',
);
assert(
message['stickerHashKey'] != null,
'The message must contain a sticker',
);
final sticker = (await db.query(
stickersTable,
where: 'stickerPackId = ? AND hashKey = ?',
whereArgs: [message['stickerPackId'], message['stickerHashKey']],
limit: 1,
))
.first;
size = sticker['size']! as int;
width = sticker['width'] as int?;
height = sticker['height'] as int?;
hashes = deserializeHashMap(sticker['hashes']! as String);
filePath = sticker['path']! as String;
urlSource =
(jsonDecode(sticker['urlSources']! as String) as List<dynamic>)
.cast<String>()
.first;
mediaType = sticker['mediaType']! as String;
filename = path.basename(sticker['path']! as String);
} else {
size = message['mediaSize'] as int?;
width = message['mediaWidth'] as int?;
height = message['mediaHeight'] as int?;
filePath = message['mediaUrl'] as String?;
urlSource = message['srcUrl'] as String?;
mediaType = message['mediaType'] as String?;
filename = message['filename'] as String?;
}
final metadata = FileMetadata(
DateTime.now().millisecondsSinceEpoch.toString(),
filePath,
urlSource != null ? [urlSource] : null,
mediaType,
size,
message['thumbnailData'] != null ? 'blurhash' : null,
message['thumbnailData'] as String?,
width,
height,
hashes,
message['key'] as String?,
message['iv'] as String?,
message['encryptionScheme'] as String?,
null,
filename!,
);
// Create the metadata
await db.insert(
fileMetadataTable,
metadata.toDatabaseJson(),
);
id = metadata.id;
}
// Update the message
await db.update(
messagesTable,
{
'file_metadata_id': id,
},
where: 'id = ?',
whereArgs: [message['id']],
);
}
// Remove columns and add foreign key
await db.execute(
'''
CREATE TABLE ${messagesTable}_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sender TEXT NOT NULL,
body TEXT,
timestamp INTEGER NOT NULL,
sid TEXT NOT NULL,
conversationJid TEXT NOT NULL,
isFileUploadNotification INTEGER NOT NULL,
encrypted INTEGER NOT NULL,
errorType INTEGER,
warningType INTEGER,
received INTEGER,
displayed INTEGER,
acked INTEGER,
originId TEXT,
quote_id INTEGER,
file_metadata_id TEXT,
isDownloading INTEGER NOT NULL,
isUploading INTEGER NOT NULL,
isRetracted INTEGER,
isEdited INTEGER NOT NULL,
reactions TEXT NOT NULL,
containsNoStore INTEGER NOT NULL,
stickerPackId TEXT,
stickerHashKey TEXT,
pseudoMessageType INTEGER,
pseudoMessageData TEXT,
CONSTRAINT fk_quote FOREIGN KEY (quote_id) REFERENCES $messagesTable (id)
CONSTRAINT fk_file_metadata FOREIGN KEY (file_metadata_id) REFERENCES $fileMetadataTable (id)
)''',
);
await db.execute(
'INSERT INTO ${messagesTable}_new SELECT id, sender, body, timestamp, sid, conversationJid, isFileUploadNotification, encrypted, errorType, warningType, received, displayed, acked, originId, quote_id, file_metadata_id, isDownloading, isUploading, isRetracted, isEdited, reactions, containsNoStore, stickerPackId, stickerHashKey, pseudoMessageType, pseudoMessageData FROM $messagesTable',
);
await db.execute('DROP TABLE $messagesTable');
await db.execute(
'ALTER TABLE ${messagesTable}_new RENAME TO $messagesTable;',
);
}

View File

@@ -0,0 +1,26 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
Future<void> upgradeFromV36ToV37(DatabaseMigrationData data) async {
final (db, _) = data;
// Queries against messages by id (and sid/originId happen regularly)
await db.execute(
'CREATE INDEX idx_messages_id ON $messagesTable (id, sid, originId)',
);
// Conversations are often queried by their jid
await db.execute(
'CREATE INDEX idx_conversation_id ON $conversationsTable (jid)',
);
// Reactions must be quickly queried
await db.execute(
'CREATE INDEX idx_reactions_message_id ON $reactionsTable (message_id, senderJid)',
);
// File metadata should also be quickly queriable by its id
await db.execute(
'CREATE INDEX idx_file_metadata_message_id ON $fileMetadataTable (id)',
);
}

View File

@@ -0,0 +1,62 @@
import 'dart:convert';
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/service/database/helpers.dart';
Future<void> upgradeFromV34ToV35(DatabaseMigrationData data) async {
final (db, _) = data;
// Create the table
await db.execute('''
CREATE TABLE $reactionsTable (
senderJid TEXT NOT NULL,
emoji TEXT NOT NULL,
message_id INTEGER NOT NULL,
CONSTRAINT pk_sender PRIMARY KEY (senderJid, emoji, message_id),
CONSTRAINT fk_message FOREIGN KEY (message_id) REFERENCES $messagesTable (id)
ON DELETE CASCADE
)''');
// Figure out our JID
final rawJid = await db.query(
xmppStateTable,
where: "key = 'jid'",
limit: 1,
);
String? jid;
if (rawJid.isNotEmpty) {
jid = rawJid.first['value']! as String;
}
// Migrate messages
final messages = await db.query(
messagesTable,
where: "reactions IS NOT '[]'",
);
for (final message in messages) {
final reactions =
(jsonDecode(message['reactions']! as String) as List<dynamic>)
.cast<Map<String, Object?>>();
for (final reaction in reactions) {
final senders = [
...reaction['senders']! as List<String>,
if (intToBool(reaction['reactedBySelf']! as int) && jid != null) jid,
];
for (final sender in senders) {
await db.insert(
reactionsTable,
{
'senderJid': sender,
'emoji': reaction['emoji']! as String,
'message_id': message['id']! as int,
},
);
}
}
}
// Remove the column
await db.execute('ALTER TABLE $messagesTable DROP COLUMN reactions');
}

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