1
0
mirror of https://github.com/vector-im/riotX-android synced 2025-10-06 00:02:48 +02:00

Compare commits

...

342 Commits

Author SHA1 Message Date
ganfra
d1e49624c7 Merge branch 'release/1.4.0' into main 2022-02-09 12:50:30 +01:00
ganfra
5b83ba9320 Release: update CHANGES 2022-02-09 11:25:11 +01:00
ganfra
a6bf921d64 Release: update versions 2022-02-09 11:18:03 +01:00
ganfra
a75e2d5fa8 Merge pull request #5186 from vector-im/feature/fga/fix_ui_test
Feature/fga/fix UI test
2022-02-09 10:13:05 +01:00
ganfra
e72593304f UITests: remove search menu item test case (not visible anymore in E2E room) 2022-02-08 19:35:33 +01:00
ganfra
a19642cd8e Analytics: fix always null identity property 2022-02-08 19:34:41 +01:00
ganfra
8d7e9baf85 Merge pull request #5103 from vector-im/feature/ons/fix_turkish_translations
Fix Turkish translation errors - TO BE MERGED DURING WEBLATE SYNC
2022-02-08 18:34:22 +01:00
ganfra
33648963c4 Merge branch 'develop' into feature/ons/fix_turkish_translations 2022-02-08 18:06:10 +01:00
ganfra
96f38637b5 Merge pull request #5184 from RiotTranslateBot/weblate-element-android-element-app
Translations update from Weblate
2022-02-08 18:02:37 +01:00
Weblate
d06a0af947 Merge branch 'origin/develop' into Weblate. 2022-02-08 14:47:47 +00:00
Linerly
67be89bad2 Translated using Weblate (Indonesian)
Currently translated at 100.0% (2752 of 2752 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/id/
2022-02-08 14:29:29 +00:00
ganfra
49a0555704 Merge pull request #5176 from vector-im/feature/ons/map_wellknown_support
Retrieve map style url from .well-known
2022-02-08 11:57:48 +01:00
Jeff Huang
30f7464797 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (47 of 47 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/zh_Hant/
2022-02-08 04:26:33 +00:00
LinAGKar
64a0c53405 Translated using Weblate (Swedish)
Currently translated at 99.9% (2751 of 2752 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/sv/
2022-02-08 04:26:32 +00:00
Jozef Gaal
12f495c9b8 Translated using Weblate (Slovak)
Currently translated at 91.2% (2512 of 2752 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/sk/
2022-02-08 04:26:32 +00:00
ganfra
b75fd18959 Merge pull request #5142 from vector-im/feature/adm/set-usecase-on-account-creation
Aligning use case tracking with iOS
2022-02-07 19:04:07 +01:00
Onuray Sahin
22a03abdc4 Code review fix. 2022-02-07 20:08:31 +03:00
Onuray Sahin
9d83bafa7b Retrieve map style url from .well-known. 2022-02-07 13:38:54 +03:00
Adam Brown
4438d513a2 Merge pull request #5174 from daniellekirkwood/develop
Fix WTF automation
2022-02-07 10:21:30 +00:00
daniellekirkwood
a416e76e54 Create 5173.misc 2022-02-07 10:16:16 +00:00
daniellekirkwood
4e355665ed Fix WTF automation 2022-02-07 10:15:49 +00:00
Adam Brown
ed78e54bce Merge pull request #5121 from daniellekirkwood/develop
Update workflow for FTUE issues on FTUE board
2022-02-07 10:00:25 +00:00
Danial Behzadi
fade3d1d0d Translated using Weblate (Persian)
Currently translated at 100.0% (47 of 47 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/fa/
2022-02-06 16:26:55 +00:00
Nikita Epifanov
2478ec41f2 Translated using Weblate (Russian)
Currently translated at 76.5% (36 of 47 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/ru/
2022-02-06 16:26:54 +00:00
Szimszon
a40ee20c20 Translated using Weblate (Hungarian)
Currently translated at 100.0% (47 of 47 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/hu/
2022-02-06 16:26:53 +00:00
LinAGKar
0c43bffb2e Translated using Weblate (Swedish)
Currently translated at 99.9% (2751 of 2752 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/sv/
2022-02-06 16:26:52 +00:00
Jozef Gaal
00e8cd4fb9 Translated using Weblate (Slovak)
Currently translated at 84.2% (2318 of 2752 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/sk/
2022-02-06 16:26:51 +00:00
Nikita Epifanov
4bc0dc204a Translated using Weblate (Russian)
Currently translated at 100.0% (2752 of 2752 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/ru/
2022-02-06 16:26:26 +00:00
Danial Behzadi
ab0c00df74 Translated using Weblate (Persian)
Currently translated at 100.0% (2752 of 2752 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/fa/
2022-02-06 16:26:17 +00:00
Zet
6283598da0 Translated using Weblate (Arabic)
Currently translated at 38.1% (1051 of 2752 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/ar/
2022-02-06 16:26:16 +00:00
Adam Brown
3f3cac0489 updating changelog to include analytics prefix 2022-02-04 17:33:43 +00:00
Benoit Marty
ef3baee35b Split long lines 2022-02-04 16:59:20 +01:00
Benoit Marty
150d557d39 Merge pull request #5145 from vector-im/feature/ons/fix_failed_maps_rendering
Fix location rendering in timeline if map cannot be loaded
2022-02-04 16:58:04 +01:00
Benoit Marty
792042ea09 Merge pull request #5150 from vector-im/revert-5082-feature/bma/agp_7_1_0
Revert "AGP 7.1.0"
2022-02-04 16:56:42 +01:00
Onuray Sahin
fbc8866394 Merge branch 'develop' into feature/ons/fix_failed_maps_rendering
* develop: (54 commits)
  Bubbles: add CHANGELOG file
  Bubble: get LayoutDirection (RTL) from current Locale
  Version++
  fastlane
  towncrier
  Version 1.3.18
  changelog
  Improve missing state event detection to missing state events only one joined rooms (ignore LEFT room) Should reduce the number of initial sync Co-authors: ganfra and billcarsonfr
  Changelog added.
  taking the use case screen into account when accessing the sign up flows in the sanity tests
  updating copy split to match designs
  applying design feedback
  promoting use case strings for translation
  enabling the use case feature by default
  Code review fixes.
  ktlint
  Bubbles: clean up after review
  Sync: avoid deleting root event of CurrentState on gappy sync
  Code review fixes.
  Support generic location pin.
  ...

# Conflicts:
#	vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt
#	vector/src/main/res/layout/item_timeline_event_location_stub.xml
2022-02-04 14:54:39 +03:00
random
9cccdc1c9d Translated using Weblate (Italian)
Currently translated at 100.0% (47 of 47 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/it/
2022-02-04 11:26:31 +00:00
Hasan
7b08a55294 Translated using Weblate (Turkish)
Currently translated at 74.9% (2062 of 2752 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/tr/
2022-02-04 11:26:30 +00:00
Fjoerfoks
d133aeb88c Translated using Weblate (Frisian)
Currently translated at 48.0% (1321 of 2752 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/fy/
2022-02-04 11:26:15 +00:00
Aris Kotsomitopoulos
077c0eea98 Revert "AGP 7.1.0" 2022-02-04 12:20:20 +02:00
Benoit Marty
4ce1ab2665 Merge pull request #4937 from vector-im/feature/fga/message_bubbles
Feature/fga/message bubbles
2022-02-03 19:00:38 +01:00
ganfra
02de636955 Bubbles: add CHANGELOG file 2022-02-03 17:57:10 +01:00
ganfra
04234318e5 Bubble: get LayoutDirection (RTL) from current Locale 2022-02-03 17:53:25 +01:00
daniellekirkwood
a44f49fa34 Create 5148.misc 2022-02-03 16:39:47 +00:00
daniellekirkwood
4b7dcf634f Add automation for WTF labels to WTF board 2022-02-03 16:39:11 +00:00
daniellekirkwood
0dce0ad0bb Merge branch 'vector-im:develop' into develop 2022-02-03 16:37:46 +00:00
ganfra
2e2da16a6b Bubbles: merge develop 2022-02-03 17:10:13 +01:00
Adam Brown
dbfd7e6703 Merge pull request #5106 from vector-im/feature/adm/enable-use-case-feature
Enabling FTUE Use Case
2022-02-03 15:14:49 +00:00
Benoit Marty
dba9a47c63 Merge pull request #5127 from vector-im/feature/ons/generic_location_pin
Support generic location pin
2022-02-03 16:02:38 +01:00
Benoit Marty
aa12a73a05 Version++ 2022-02-03 15:51:04 +01:00
Benoit Marty
105118f831 Merge hotfix 1.3.18 to develop 2022-02-03 15:48:51 +01:00
Onuray Sahin
a57ad92d51 Changelog added. 2022-02-03 16:32:39 +03:00
Onuray Sahin
a7678241f2 Fix location rendering in timeline if map cannot be loaded. 2022-02-03 16:17:33 +03:00
Adam Brown
26beb97f1e adding changelog entry 2022-02-03 11:34:09 +00:00
Adam Brown
227c93b014 avoiding tracking the use case until account creation, also means we no longer need to reset the tracking value 2022-02-03 11:29:09 +00:00
Adam Brown
48b339075b filtering null values from the identify properties
- resetting values to null may cause inconsistent cross device tracking
2022-02-03 11:29:09 +00:00
Adam Brown
63a937c096 taking the use case screen into account when accessing the sign up flows in the sanity tests 2022-02-03 11:28:33 +00:00
Adam Brown
9b3af7cfc9 updating copy split to match designs 2022-02-03 11:28:33 +00:00
Adam Brown
bac60677d5 applying design feedback 2022-02-03 11:28:33 +00:00
Adam Brown
5ed1f34df3 promoting use case strings for translation 2022-02-03 11:28:33 +00:00
Adam Brown
61bc443bdf enabling the use case feature by default 2022-02-03 11:28:33 +00:00
Benoit Marty
cd6ba5265b Merge pull request #5141 from vector-im/feature/mna/4641-remove-room-search-menu
Issue-4641: Show search menu item only if available
2022-02-03 11:13:11 +01:00
Onuray Sahin
c590bbdb9b Code review fixes. 2022-02-03 12:53:59 +03:00
Maxime Naturel
8e0f7ca373 Issue-4641: Show search menu item only if available 2022-02-03 09:38:06 +01:00
Benoit Marty
3e212655eb Merge pull request #5117 from vector-im/feature/adm/use-case-store-crash
Handling use case screen in sanity tests
2022-02-03 00:53:20 +01:00
Benoit Marty
6ebba4e3dd Merge pull request #5131 from vector-im/feature/nfe/spaces_promo_removal
spaces restricted promo removed
2022-02-03 00:44:07 +01:00
Benoit Marty
60840403ff Merge pull request #5130 from vector-im/feature/bma/release_checklist
Update the release recipe
2022-02-03 00:43:41 +01:00
Benoit Marty
c66849834a Merge pull request #5051 from vector-im/feature/bma/cleanup
Remove some usage of MatrixCallback
2022-02-03 00:25:54 +01:00
Besnik Bleta
83e94df7fa Translated using Weblate (Albanian)
Currently translated at 97.8% (46 of 47 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/sq/
2022-02-02 20:26:37 +00:00
Linerly
d08bbc8b02 Translated using Weblate (Indonesian)
Currently translated at 100.0% (47 of 47 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/id/
2022-02-02 20:26:36 +00:00
waclaw66
a92bc36782 Translated using Weblate (Czech)
Currently translated at 100.0% (47 of 47 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/cs/
2022-02-02 20:26:35 +00:00
Priit Jõerüüt
18e8b8cd11 Translated using Weblate (Estonian)
Currently translated at 100.0% (47 of 47 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/et/
2022-02-02 20:26:34 +00:00
Ihor Hordiichuk
c077279c03 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (47 of 47 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/uk/
2022-02-02 20:26:33 +00:00
LinAGKar
48c3e2ce89 Translated using Weblate (Swedish)
Currently translated at 100.0% (47 of 47 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/sv/
2022-02-02 20:26:32 +00:00
Jozef Gaal
8fd6f5c5ad Translated using Weblate (Slovak)
Currently translated at 100.0% (47 of 47 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/sk/
2022-02-02 20:26:30 +00:00
lvre
0e93c40921 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (47 of 47 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/pt_BR/
2022-02-02 20:26:29 +00:00
Glandos
c64354ba62 Translated using Weblate (French)
Currently translated at 100.0% (47 of 47 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/fr/
2022-02-02 20:26:28 +00:00
libexus
16db1356ea Translated using Weblate (German)
Currently translated at 100.0% (47 of 47 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/de/
2022-02-02 20:26:27 +00:00
Hasan
75b2b5146d Translated using Weblate (Turkish)
Currently translated at 67.6% (1863 of 2752 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/tr/
2022-02-02 20:26:26 +00:00
LinAGKar
dc37e9236b Translated using Weblate (Swedish)
Currently translated at 99.9% (2751 of 2752 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/sv/
2022-02-02 20:26:20 +00:00
Johan Smits
6acaaf9347 Translated using Weblate (Dutch)
Currently translated at 100.0% (2752 of 2752 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/nl/
2022-02-02 20:26:17 +00:00
libexus
4120170dae Translated using Weblate (German)
Currently translated at 99.6% (2741 of 2752 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/de/
2022-02-02 20:26:14 +00:00
ganfra
9e0086742c Bubbles: clean up after review 2022-02-02 19:56:21 +01:00
Benoit Marty
78fda0ca9d typo 2022-02-02 18:43:49 +01:00
Benoit Marty
17a0c17dde Merge pull request #5132 from vector-im/feature/adm/missing-notifications
Fixing google play variant push notifications being skipped
2022-02-02 18:42:51 +01:00
Benoit Marty
9c72c61fc6 Merge pull request #5133 from vector-im/feature/nfe/obsolete_filter_class_removed
removed obsolete room list filter class
2022-02-02 18:42:21 +01:00
Benoit Marty
35c77c10c4 Merge pull request #5135 from vector-im/feature/adm/missing-medium-weight-fonts
Fixing ignored font weights
2022-02-02 18:41:27 +01:00
Adam Brown
96efeaefed adding changelog entry 2022-02-02 16:59:05 +00:00
Adam Brown
ba86834c6b removing forced san fontFamily, fixes other weights being ignored 2022-02-02 16:58:19 +00:00
Onuray Sahin
c2daab4211 Code review fixes. 2022-02-02 19:33:34 +03:00
Adam Brown
0db38567ed adding javadoc to the data store provider 2022-02-02 16:04:22 +00:00
Adam Brown
3212bc2266 ensuring we use the application context for the datastore to avoid any local activity leaks 2022-02-02 16:04:22 +00:00
Adam Brown
d77e18f810 allowing nullable posthog properties to be submitted
- fixes crash when attempting to identify with empty properties
- will need rebasing with https://github.com/matrix-org/matrix-analytics-events/pull/20
2022-02-02 16:04:22 +00:00
Adam Brown
741f9fabbb providing a way to lazily read dynamic datastore instances
- fixes crash where multiple session store instances attempt to read from the same datastore backing file
2022-02-02 15:53:04 +00:00
Benoit Marty
2ffc89d863 Merge pull request #5125 from vector-im/feature/nfe/space_card_layout
space and room card layout changed in explore rooms, space invite dia…
2022-02-02 16:08:24 +01:00
Benoit Marty
c479073583 Merge pull request #5129 from vector-im/feature/aris/threads_fallback_permalink
Fix fallback permalink when threads are disabled
2022-02-02 16:06:34 +01:00
NIkita Fedrunov
10f7673be7 removed obsolete room list filter class 2022-02-02 15:53:20 +01:00
Adam Brown
6406e97d4b adding changelog entry 2022-02-02 14:46:43 +00:00
NIkita Fedrunov
440c5f7516 spaces restricted promo removed 2022-02-02 15:42:28 +01:00
Benoit Marty
6922c72174 Update the release recipe 2022-02-02 15:41:54 +01:00
NIkita Fedrunov
c64c204878 code quality changes 2022-02-02 15:19:48 +01:00
Adam Brown
e2d8c73761 fixing push notifications being skipped
- reworking the isEventRead logic to always check if the new event exists locally, there's a race condition between eventFastLane and syncs where we end up mark unseen events as read if our last local message in a room is from ourselves
- this may also fix some  events being instantly marked as read when being received
2022-02-02 12:32:42 +00:00
ganfra
1bf2523437 Merge branch 'develop' into feature/fga/message_bubbles 2022-02-02 13:01:43 +01:00
fedrunov
65fd5489a3 "Invite users to space" dialog now closed when user choose invite method (#5126) 2022-02-02 12:54:45 +01:00
ariskotsomitopoulos
36f0283d96 Fix fallback permalink when threads are disabled 2022-02-02 13:51:45 +02:00
Onuray Sahin
a131d28b3e Merge branch 'develop' into feature/ons/generic_location_pin
* develop: (146 commits)
  exhaustive not needed anymore
  Invert if condition and split long line
  Use kotlin string builder
  Same issue but in the test
  Format
  Fix a crash: java.util.IllegalFormatPrecisionException https://github.com/matrix-org/element-android-rageshakes/issues/33398
  add changelog file for threads feature
  add changelog file for threads feature
  Formatting
  Improve hidden events for threads
  Add TODO for the next Weblate sync
  ktlint format
  PR remarks
  Fix a lint false positive? Anyway this was not used. Restricted API ../../../matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt:61: ListenableWorker.getTaskExecutor can only be called from within the same library group (referenced groupId=androidx.work from groupId=element-android)
  It seems that now lint rule `MissingQuantity` is an error and not a warning by default.
  Whitelist group 'org.webjars' on MavenCentral to fix lint execution
  Fix conflicts
  Formating & remove unused comments
  Fix error in unit test
  ktlint format
  ...

# Conflicts:
#	vector/src/main/java/im/vector/app/features/navigation/Navigator.kt
2022-02-02 14:35:30 +03:00
Onuray Sahin
37d35c9a7f Support generic location pin. 2022-02-02 14:25:54 +03:00
Benoit Marty
ecf356f6eb Merge pull request #5124 from vector-im/feature/bma/fix_timber_crash
Fix timber crash
2022-02-02 12:25:30 +01:00
Benoit Marty
407e065b90 Merge pull request #5041 from vector-im/feature/nfe/qr_scanner_fragments_merge
qr scanner fragments merged into one
2022-02-02 12:24:28 +01:00
Benoit Marty
10841ab0a1 exhaustive not needed anymore 2022-02-02 12:24:02 +01:00
Benoit Marty
54fb28c912 Merge pull request #5089 from vector-im/feature/dla/fix_incorrect_call_status
CallTileTimelineItem.CallStatus.MISSED renders both missed and reject…
2022-02-02 12:06:11 +01:00
Benoit Marty
a78d88fc0a Invert if condition and split long line 2022-02-02 12:03:38 +01:00
Benoit Marty
7a44f16372 More cleanup 2022-02-02 11:38:09 +01:00
Benoit Marty
1a8c31f926 Inject the context to simplify the API
And more cleanup
2022-02-02 11:38:09 +01:00
Benoit Marty
4893429d73 Remove usage of MatrixCallback 2022-02-02 11:38:09 +01:00
NIkita Fedrunov
664c05757e changelog added 2022-02-02 10:51:25 +01:00
Benoit Marty
2bf36c74e3 Use kotlin string builder 2022-02-02 10:46:31 +01:00
NIkita Fedrunov
9dccc398b4 space and room card layout changed in explore rooms, space invite dialog, room invite dialog 2022-02-02 10:45:43 +01:00
Benoit Marty
cd16d3f19b Same issue but in the test 2022-02-02 10:42:39 +01:00
Benoit Marty
581dbda2c1 Format 2022-02-02 10:34:08 +01:00
Benoit Marty
cd1171eb9d Fix a crash: java.util.IllegalFormatPrecisionException
https://github.com/matrix-org/element-android-rageshakes/issues/33398
2022-02-02 10:33:19 +01:00
ganfra
cfda76b2d4 Timeline: avoid overflow on getViewType 2022-02-01 20:31:09 +01:00
ganfra
50810065a2 Bubbles: update media sizing (including LocationItem) 2022-02-01 20:19:35 +01:00
daniellekirkwood
1d2da0cf95 Merge pull request #2 from daniellekirkwood/daniellekirkwood-patch-1 2022-02-01 17:59:34 +00:00
daniellekirkwood
a6636b8188 Add changelog entry 2022-02-01 17:57:53 +00:00
daniellekirkwood
9a303ec923 Update workflow for FTUE issues on FTUE board 2022-02-01 17:54:58 +00:00
Benoit Marty
0391684334 Merge pull request #5119 from vector-im/feature/aris/fix_github_action_merging_error
Fix CI/CD errors after merges
2022-02-01 17:52:06 +01:00
ariskotsomitopoulos
8e90864e1f add changelog file for threads feature 2022-02-01 17:37:05 +02:00
ariskotsomitopoulos
57f3b132cb add changelog file for threads feature 2022-02-01 16:20:14 +02:00
Aris Kotsomitopoulos
3d5f8ed7e7 Merge pull request #4746 from vector-im/feature/aris/threads
Threads P0 Release
2022-02-01 15:58:32 +02:00
Benoit Marty
79b4c76a09 Merge pull request #5082 from vector-im/feature/bma/agp_7_1_0
AGP 7.1.0
2022-02-01 13:57:15 +01:00
ariskotsomitopoulos
ed992ddc72 Formatting 2022-02-01 14:40:00 +02:00
ariskotsomitopoulos
877c9bec97 Improve hidden events for threads 2022-02-01 14:07:16 +02:00
Benoit Marty
8db5b76676 Add TODO for the next Weblate sync 2022-02-01 11:41:59 +01:00
ariskotsomitopoulos
cfa52d83b4 ktlint format 2022-02-01 12:16:52 +02:00
ariskotsomitopoulos
fcc095a239 PR remarks 2022-02-01 12:13:10 +02:00
ganfra
825c2ca989 Merge branch 'develop' into feature/fga/message_bubbles 2022-02-01 10:05:51 +01:00
Benoit Marty
c8ddb2e85e Fix a lint false positive? Anyway this was not used.
Restricted API
../../../matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt:61: ListenableWorker.getTaskExecutor can only be called from within the same library group (referenced groupId=androidx.work from groupId=element-android)
2022-02-01 00:30:30 +01:00
Benoit Marty
c18a614e72 It seems that now lint rule MissingQuantity is an error and not a warning by default. 2022-02-01 00:26:10 +01:00
Benoit Marty
56e52fd917 Whitelist group 'org.webjars' on MavenCentral to fix lint execution 2022-01-31 23:34:56 +01:00
Benoit Marty
6213cc73c0 Merge pull request #5109 from vector-im/dependabot/gradle/com.github.yalantis-ucrop-2.2.8-native
Bump ucrop from 2.2.7 to 2.2.8
2022-01-31 22:58:50 +01:00
Benoit Marty
983fb598bd Merge pull request #5110 from vector-im/feature/adm/prioritize-maven-central
Attempting to fix missing dependabot updates
2022-01-31 22:44:39 +01:00
ganfra
38f1bbdab2 RTL: better support for some TextViews 2022-01-31 19:20:00 +01:00
ganfra
820bc644b6 Bubble: introduce CornersRadius 2022-01-31 19:18:42 +01:00
Benoit Marty
3a040aaee2 Update vector/build.gradle 2022-01-31 17:17:37 +01:00
Benoit Marty
31d976ac71 Merge pull request #5108 from vector-im/bmarty-patch-1
With the link...
2022-01-31 17:13:27 +01:00
Adam Brown
fd2aaa8c2b prioritizing the maven central build repository as it appears that dependabot is only using the first 2 defined maven repositories for the build scans 2022-01-31 15:29:31 +00:00
dependabot[bot]
3caf81d07e Bump ucrop from 2.2.7 to 2.2.8-native
Bumps [ucrop](https://github.com/Yalantis/uCrop) from 2.2.7 to 2.2.8-native.
- [Release notes](https://github.com/Yalantis/uCrop/releases)
- [Commits](https://github.com/Yalantis/uCrop/compare/2.2.7...2.2.8-native)

---
updated-dependencies:
- dependency-name: com.github.yalantis:ucrop
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-31 15:22:21 +00:00
Benoit Marty
cd1693548a With the link... 2022-01-31 16:03:21 +01:00
Benoit Marty
d3d8ea1f2d Merge pull request #5107 from vector-im/bmarty-patch-1
Give more visibility to the sanity test nighly by adding a badge to the README
2022-01-31 16:02:01 +01:00
Benoit Marty
78f09e25ae Give more visibility to the sanity test nighly by adding a badge to the README 2022-01-31 15:50:16 +01:00
ariskotsomitopoulos
15fe9edfbd Fix conflicts 2022-01-31 16:21:46 +02:00
Adam Brown
986d9f92e9 Merge pull request #5009 from vector-im/feature/adm/storing-use-case
Storing and tracking the onboarding messaging use case
2022-01-31 14:15:39 +00:00
ariskotsomitopoulos
d509b3324c Merge branch 'develop' into feature/aris/threads 2022-01-31 15:58:12 +02:00
Benoit Marty
7dad08654b Revert "Downgrade the version to 1.3.17 to prepare a corrective release."
This reverts commit 2efa67e587.
2022-01-31 14:53:43 +01:00
Benoit Marty
af83073965 Merge branch 'release/1.3.17' into develop 2022-01-31 14:52:28 +01:00
ariskotsomitopoulos
f07c23fdda Formating & remove unused comments 2022-01-31 14:52:09 +02:00
ariskotsomitopoulos
ec9b6aa993 Fix error in unit test 2022-01-31 14:50:57 +02:00
Onuray Sahin
b3288c5acc Fix translation errors. 2022-01-31 15:49:18 +03:00
Onuray Sahin
a584cb876b Fix translation errors. 2022-01-31 15:26:54 +03:00
ariskotsomitopoulos
26eaa843b3 ktlint format 2022-01-31 13:56:17 +02:00
ariskotsomitopoulos
14e56b8f7d MessageComposerViewModel format 2022-01-31 13:53:49 +02:00
ariskotsomitopoulos
d91f3d2de6 Enhance SlashCommandNotSupportedInThreads 2022-01-31 13:42:20 +02:00
ariskotsomitopoulos
cb3501ea17 Lazy load notSupportedThreadsCommands to improve performance 2022-01-31 13:08:15 +02:00
ariskotsomitopoulos
3253a252fb Introduce ThreadsService by splitting TimelineService 2022-01-31 12:58:19 +02:00
ariskotsomitopoulos
5ff5f762d4 Revert the use of coerceIn 2022-01-31 12:24:28 +02:00
ariskotsomitopoulos
32a982c287 Improve coerceIn format 2022-01-31 12:19:12 +02:00
ariskotsomitopoulos
cdd36ce034 Fix IndexOutOfBound crashes while clicking permalinks 2022-01-31 11:56:26 +02:00
ganfra
fd99d6d7d8 Bubbles: start fixing RTL 2022-01-28 19:53:07 +01:00
ganfra
c425701c20 Bubbles: handle ripple effect 2022-01-28 17:55:32 +01:00
ariskotsomitopoulos
b1067e9a58 - ktlint format
- Update a text resource
2022-01-28 16:37:59 +02:00
ganfra
35674ad401 Bubbles: handle location sharing (need updates) 2022-01-28 15:10:13 +01:00
ganfra
b79a5fd4f4 Bubble: move overlay view to MessageBubbleView (and fix corner radius) 2022-01-28 15:09:43 +01:00
ariskotsomitopoulos
1d6d8102b3 Further improve thread summary after forward scrolling 2022-01-28 14:11:03 +02:00
NIkita Fedrunov
f56991f077 code review and lint fixes 2022-01-28 11:02:10 +01:00
David Langley
e9ae76da9c CallTileTimelineItem.CallStatus.MISSED renders both missed and rejected calls. Logic should be !answered. 2022-01-28 08:57:19 +00:00
ganfra
c14eb050b1 Merge branch 'develop' into feature/fga/message_bubbles 2022-01-27 18:59:10 +01:00
ganfra
ed9adf8367 Bubbles: fix paddings 2022-01-27 18:18:39 +01:00
ganfra
881b063d45 Bubbles: remove emote from bubble (keep right/left alignment) 2022-01-27 18:18:29 +01:00
ganfra
4de421d663 Timeline html rendering: better reply and pill 2022-01-27 18:17:56 +01:00
ganfra
a9fe21e583 Timeline html rendering: handle code tags 2022-01-27 18:17:23 +01:00
ariskotsomitopoulos
78dfac52a7 ktlint format 2022-01-27 18:39:43 +02:00
ariskotsomitopoulos
bac6d271ca Merge develop into this branch 2022-01-27 18:13:05 +02:00
ariskotsomitopoulos
b2a2fe2710 Merge branch 'develop' into feature/aris/threads
# Conflicts:
#	matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt
#	tools/check/forbidden_strings_in_code.txt
#	vector/build.gradle
#	vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
#	vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt
#	vector/src/main/java/im/vector/app/features/command/Command.kt
#	vector/src/main/java/im/vector/app/features/command/CommandParser.kt
#	vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt
#	vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
#	vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
#	vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
#	vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt
#	vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
#	vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
#	vector/src/main/java/im/vector/app/features/navigation/Navigator.kt
#	vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt
#	vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
#	vector/src/main/res/layout/fragment_timeline.xml
#	vector/src/main/res/xml/vector_settings_labs.xml
2022-01-27 17:11:26 +02:00
Benoit Marty
4aff1ab017 AGP 7.1.0 2022-01-27 16:08:47 +01:00
ariskotsomitopoulos
b83872d5f0 When show all threads developer mode option is enabled, prevent reply in thread to those events 2022-01-27 16:38:14 +02:00
ariskotsomitopoulos
554ece724e - Remove counter from thread notifications
- Fix red dot on user mentioning
2022-01-27 14:55:34 +02:00
ariskotsomitopoulos
f53b711e0d When thread disabled add thread replies within threads ( to the users with threads enabled ) 2022-01-27 13:49:03 +02:00
ariskotsomitopoulos
358a7d0ec4 Handle latest thread message & root thread edition to update thread summary and thread list appropriately 2022-01-27 13:22:04 +02:00
Adam Brown
cbdeb54fdb using md5 hash of the userId to normalise the usage as a key 2022-01-26 18:43:09 +00:00
ariskotsomitopoulos
92d082c26a Improve thread message deletion
Fix thread summary after isLimited events
2022-01-26 14:07:07 +02:00
Adam Brown
bc373917b5 making the onboarding store a userId scoped generic store instead 2022-01-26 09:35:13 +00:00
Adam Brown
73b80b1c7d providing the onboarding store via session extension
- avoids needing to know about the user id for each read/write
2022-01-26 09:35:13 +00:00
Adam Brown
b91b9cb973 lifting analytics extension to the common package 2022-01-26 09:35:13 +00:00
Adam Brown
f44ccd739e storing an retrieving the use case selection by user
- will enable multi account support in the future
2022-01-26 09:35:13 +00:00
Adam Brown
c4ac03949c persisting the use case at the point of session creation
- this ensures we have a unique session or account id to store the selection against in case we support multi account in the future
2022-01-26 09:35:13 +00:00
Adam Brown
0a7c421faa making use of the analytics tracker instead of the vector analytics for the smaller tracking subset 2022-01-26 09:35:13 +00:00
Adam Brown
6c1cbccff3 updating the identity with the use case selection as part of the onboarding flow 2022-01-26 09:35:13 +00:00
Adam Brown
515c8ce7c2 adding ability to update the user properties (identity) via the vector analytics 2022-01-26 09:35:13 +00:00
Adam Brown
84e23e1911 storing the user usecase selection and clearinng on signout 2022-01-26 09:35:13 +00:00
ariskotsomitopoulos
c19b52cded Enhance thread summary
Fix deleted root thread messages when show deleted messages are enabled/disabled
2022-01-25 18:21:42 +02:00
ariskotsomitopoulos
b1b27bdd0e Enhance edit to support new threads fallback 2022-01-25 14:12:13 +02:00
ganfra
8f0e1039aa Bubbles: make round style algorithm more accurate 2022-01-24 19:31:51 +01:00
ganfra
608d8a5d54 Bubbles: change colors 2022-01-24 19:31:04 +01:00
ariskotsomitopoulos
fe88e81d4a - Refactor thread awareness (handle decrypted rooms, images, stickers etc)
- Enable/disable threads functionality
- New fallback thread implementation
2022-01-24 16:55:15 +02:00
fedrunov
b6eb27f8a1 qr scanner fragments merged into one 2022-01-24 15:27:22 +01:00
ganfra
1580269857 Bubbles: move settings to timeline section 2022-01-21 16:01:54 +01:00
ganfra
713f6f7a59 Timeline: Add spacing to quote 2022-01-21 16:01:30 +01:00
ganfra
8c4dff4db9 Bubbles: change again url preview 2022-01-21 16:00:58 +01:00
ariskotsomitopoulos
e0630ceac0 Fix mentions UI within threads 2022-01-20 13:02:35 +02:00
ariskotsomitopoulos
35ee72aac0 Add typealias for TimelineEvent 2022-01-20 00:50:44 +02:00
ariskotsomitopoulos
38f193fbd5 Add LightweightSettingsStorage in sdk
Enable thread awareness when threads are disabled
Enhance enable/disable thread messages to app & sdk
Add Shared PReferences to sdk
2022-01-19 18:52:02 +02:00
ganfra
2d9454c5b6 Bubbles: first iteration on url preview 2022-01-19 16:19:47 +01:00
ganfra
ac0c2624f0 Bubbles: update sticker handling 2022-01-19 11:49:33 +01:00
ariskotsomitopoulos
8cc96e27bc - Add threads to lab settings
- Disable thread awareness due to the new fallback mechanism
2022-01-19 12:28:00 +02:00
ganfra
5ee4984ec8 Bubbles: handle images and make small refactoring 2022-01-18 19:27:12 +01:00
ganfra
a9e7c45074 Fix url preview sizing 2022-01-18 19:26:23 +01:00
ariskotsomitopoulos
4cff3938e7 - Hide read receipts from thread timeline
- Enhance FetchThreadTimelineTask
2022-01-18 16:05:41 +02:00
ariskotsomitopoulos
707397cb9d cleanup 2022-01-18 15:28:44 +02:00
ariskotsomitopoulos
10599aa728 ktlint format 2022-01-18 13:11:52 +02:00
ariskotsomitopoulos
636474b748 Merge branch 'develop' into feature/aris/threads
# Conflicts:
#	matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt
#	matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
#	vector/src/main/java/im/vector/app/features/command/Command.kt
#	vector/src/main/java/im/vector/app/features/command/CommandParser.kt
#	vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
2022-01-18 12:41:40 +02:00
ganfra
7ff8483592 Merge branch 'develop' into feature/fga/message_bubbles 2022-01-17 19:19:29 +01:00
ariskotsomitopoulos
5e23947419 Enhance filtering to support threads 2022-01-17 19:22:22 +02:00
ariskotsomitopoulos
81a1dfd66d PR Remarks 2022-01-17 17:28:40 +02:00
ariskotsomitopoulos
f6067977fe Refactor ThreadMessagingTest 2022-01-17 14:27:30 +02:00
ariskotsomitopoulos
b343739a71 Enhance decrypted thread summary to return poll questions 2022-01-17 14:27:17 +02:00
ariskotsomitopoulos
5b786e5749 Remove duplicate RetryTestRule 2022-01-17 14:26:39 +02:00
ganfra
1108ef9fbe Bubbles: make it works for file, voice and polls. Also add parity for "modern" layout. 2022-01-14 19:19:23 +01:00
ariskotsomitopoulos
3a3cce85f8 Add encryption shield
Change thread list filtering UI tick to radio buttons
2022-01-14 18:42:57 +02:00
ariskotsomitopoulos
ff87f07f65 Add empty screen UI on empty thread list 2022-01-14 15:11:20 +02:00
ariskotsomitopoulos
53b82dfa3f Fix permalink handling for threads regarding timeline changes 2022-01-14 13:02:08 +02:00
ganfra
d06df45e85 Merge branch 'develop' into feature/fga/message_bubbles 2022-01-13 13:17:23 +01:00
ganfra
5ac155285b Bubbles: some clean up 2022-01-13 13:14:37 +01:00
ganfra
baee076e41 Bubbles: fix types using wrong layout 2022-01-13 12:33:58 +01:00
ganfra
b9cc795996 Bubbles : fix background colors 2022-01-13 12:33:36 +01:00
ganfra
37af93fba4 Bubbles: fix avatar/name visibility in modern layout 2022-01-12 19:22:14 +01:00
ganfra
32e72f54b3 Bubbles: add quick settings (temporary) 2022-01-12 19:01:13 +01:00
ganfra
f7df0b891e Bubbles: fix recycling issue 2022-01-12 18:45:40 +01:00
ariskotsomitopoulos
53fecef2d4 Fix compilation error on TimelineFragment 2022-01-12 18:47:34 +02:00
ariskotsomitopoulos
b89054685f Fix migration from 21 to 22 2022-01-12 18:40:33 +02:00
ariskotsomitopoulos
c049351130 Fix kltint errors 2022-01-12 18:30:43 +02:00
ariskotsomitopoulos
9d48ecea2f Merge branch 'develop' into feature/aris/threads
# Conflicts:
#	.github/workflows/integration.yml
#	matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
#	vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
#	vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt
2022-01-12 18:20:50 +02:00
ariskotsomitopoulos
4560d748d3 Display encrypted messages in thread summary and in thread list 2022-01-11 17:52:14 +02:00
ganfra
af542a8243 Bubbles: start adding "theming" mechanism 2022-01-11 15:38:58 +01:00
ariskotsomitopoulos
1e2fb88783 - fix lint error 2022-01-11 16:30:29 +02:00
ariskotsomitopoulos
753e3e7519 - fix ktlint format
- Update Threads toolbar UI
2022-01-11 15:31:21 +02:00
ariskotsomitopoulos
3bd21d04d8 Merge branch 'develop' into feature/aris/threads
# Conflicts:
#	matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt
#	tools/check/forbidden_strings_in_code.txt
2022-01-11 14:09:49 +02:00
ganfra
f7c9b36cef Bubbles: continue exploration 2022-01-11 11:57:35 +01:00
ariskotsomitopoulos
37ec3fdf84 Refactor threads to support the new timeline implementation 2022-01-11 12:13:53 +02:00
ariskotsomitopoulos
1b41a72e72 Fix Quote from within a thread 2022-01-10 14:14:11 +02:00
ariskotsomitopoulos
6503412928 Merge branch 'develop' into feature/aris/threads
# Conflicts:
#	matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
#	vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
2022-01-10 13:26:57 +02:00
ariskotsomitopoulos
e541636802 Make TimelineSettings aware of rootThreadEventId and welcome a new Thread mode for the timeline creation 2022-01-10 11:20:31 +02:00
ariskotsomitopoulos
50e51cbe29 Merge branch 'develop' into feature/aris/threads 2022-01-07 16:36:48 +02:00
ariskotsomitopoulos
ae81f61958 fix integration test 2022-01-07 16:28:58 +02:00
ariskotsomitopoulos
ac5caccdf9 Merge branch 'develop' into feature/aris/threads
# Conflicts:
#	matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
#	matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
#	matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt
#	matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
#	matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt
#	matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
#	tools/check/forbidden_strings_in_code.txt
#	vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
#	vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
#	vector/src/main/res/menu/menu_timeline.xml
2022-01-07 13:29:43 +02:00
ganfra
ad63d3de1c Bubbles: still R&D. Not sure how to handle every event types. 2022-01-06 19:07:28 +01:00
ganfra
bde1df0322 Bubbles: continue R&D on UI 2022-01-05 11:00:12 +01:00
ganfra
e540b26112 Merge develop into feature/fga/message_bubbles 2022-01-04 15:13:42 +01:00
ariskotsomitopoulos
f1f1d59576 Github actions test 2022-01-04 00:49:39 +02:00
ariskotsomitopoulos
91bda140e9 Github actions test 2022-01-04 00:46:23 +02:00
ariskotsomitopoulos
ddfdf180c2 Github actions test 2022-01-04 00:45:57 +02:00
ariskotsomitopoulos
84c537315c Github actions test 2022-01-04 00:38:50 +02:00
ariskotsomitopoulos
ef2c32e2c9 Github actions test 2022-01-04 00:32:39 +02:00
ariskotsomitopoulos
948f75b215 Github actions test 2022-01-04 00:30:51 +02:00
ariskotsomitopoulos
da8ec4debf Github actions test 2022-01-04 00:25:52 +02:00
ariskotsomitopoulos
42002b80ed Github actions test 2022-01-04 00:23:34 +02:00
ariskotsomitopoulos
925c1671a6 Add more integrations tests for threads 2022-01-03 21:09:36 +02:00
ariskotsomitopoulos
3ef960c4c3 Update copyright 2022-01-03 19:45:48 +02:00
ariskotsomitopoulos
929cc29f77 Update copyright 2022-01-03 19:18:51 +02:00
ariskotsomitopoulos
1b2ce33f7a Github actions test 2022-01-03 18:24:43 +02:00
ariskotsomitopoulos
70d1c15b89 Github actions test 2022-01-03 18:21:52 +02:00
ariskotsomitopoulos
e7dfdce057 Github actions test 2022-01-03 18:15:41 +02:00
ariskotsomitopoulos
b67199eb07 Github actions test 2022-01-03 18:12:16 +02:00
ariskotsomitopoulos
e482ef4262 First local thread integration test 2022-01-03 16:51:12 +02:00
ariskotsomitopoulos
aadbf69f3a Github actions improvement test 2022-01-03 13:13:46 +02:00
ariskotsomitopoulos
c14420378b Github actions improvement test 2022-01-03 13:06:13 +02:00
ariskotsomitopoulos
5e282533b6 Github actions improvement test 2022-01-03 13:02:05 +02:00
ariskotsomitopoulos
1127a26928 Github actions improvement test 2022-01-03 12:58:52 +02:00
ariskotsomitopoulos
bb85e9c0c2 Github actions improvement test 2022-01-03 12:57:54 +02:00
ariskotsomitopoulos
c8e4fad5c8 Github actions improvement test 2022-01-03 12:57:19 +02:00
ariskotsomitopoulos
540687cc4b Github actions improvement test 2022-01-03 12:54:21 +02:00
ariskotsomitopoulos
4d6d9181ab Github actions improvement test 2022-01-03 12:52:56 +02:00
ariskotsomitopoulos
7e3a074f8b Github actions improvement test 2022-01-03 12:45:05 +02:00
ariskotsomitopoulos
ae2dbb808f Github actions improvement test 2022-01-03 12:41:44 +02:00
ariskotsomitopoulos
f7a2088009 Github actions improvement test 2022-01-03 12:39:33 +02:00
ariskotsomitopoulos
b4d5d13205 Github actions improvement test 2022-01-03 12:37:22 +02:00
ariskotsomitopoulos
4ef9d089e7 Github actions improvement test 2022-01-03 12:28:51 +02:00
ariskotsomitopoulos
683fcc7f3e Github actions improvement test 2022-01-03 12:26:58 +02:00
ariskotsomitopoulos
5edc0506ce Github actions improvement test 2022-01-03 12:23:00 +02:00
ariskotsomitopoulos
c2183800d3 Github actions improvement test 2022-01-03 12:14:14 +02:00
ariskotsomitopoulos
f9e03aa99e Remove unused code 2022-01-03 11:33:38 +02:00
ariskotsomitopoulos
694b8de034 Merge branch 'develop' into feature/aris/threads
# Conflicts:
#	library/ui-styles/src/main/res/values/dimens.xml
2022-01-03 11:08:22 +02:00
ariskotsomitopoulos
0d9bc188d7 Fix code quality issues 2021-12-26 00:48:11 +02:00
ariskotsomitopoulos
0e30f4e817 Fix code quality issues 2021-12-25 23:35:40 +02:00
ariskotsomitopoulos
9ef4e1e83f Fix code quality issues 2021-12-25 13:42:53 +02:00
ariskotsomitopoulos
d3e9e19779 Fix code quality issues 2021-12-25 13:11:42 +02:00
ariskotsomitopoulos
581f71e89d Remove unused code 2021-12-23 17:22:27 +02:00
ariskotsomitopoulos
f06397023a Add support when there no threads messages to init timeline. Init as the normal one and hide them on the app side. That is also helpful to work to load all the threads when there is no server support 2021-12-23 17:19:36 +02:00
ariskotsomitopoulos
dcabaa0dab Merge branch 'feature/aris/threads' into feature/aris/threads_api_support 2021-12-21 20:16:17 +02:00
ariskotsomitopoulos
d7546db26f Fix code quality issues 2021-12-21 20:09:25 +02:00
ariskotsomitopoulos
5a7d12a9a5 Enhance RoomEventFilter with MSC3440 2021-12-21 20:04:50 +02:00
ariskotsomitopoulos
7048080ee0 Merge branch 'develop' into feature/aris/threads 2021-12-21 13:24:03 +02:00
ariskotsomitopoulos
ed48eb38c9 Apply ktlinFormat 2021-12-21 13:23:17 +02:00
ariskotsomitopoulos
cc7e3ea78c Improve init thread query 2021-12-17 01:25:50 +02:00
ariskotsomitopoulos
5723465106 Fix local notification badge number 2021-12-17 01:23:09 +02:00
ariskotsomitopoulos
a60f6e996a Enhance thread awareness to support stickers 2021-12-17 00:46:47 +02:00
ariskotsomitopoulos
a187e0ec33 Enhance thread awareness to recognise the type of messages that are not able to be send as a reply such as images, videos, audios, stickers 2021-12-16 22:03:42 +02:00
ganfra
9a5934dd33 Bubbles: R&D try to find the best way to provide dynamic layout 2021-12-16 20:57:05 +01:00
ariskotsomitopoulos
dca5bea744 Merge branch 'feature/aris/fix_immutable_model_crash' into feature/aris/threads 2021-12-16 20:56:10 +02:00
ariskotsomitopoulos
f769d84443 Merge branch 'develop' into feature/aris/threads 2021-12-16 20:55:52 +02:00
ariskotsomitopoulos
638d56c707 Fix update from develop/prod to threads 2021-12-16 17:10:29 +02:00
ariskotsomitopoulos
bc6e89b503 Disable user typing from thread timeline 2021-12-15 18:49:22 +02:00
ariskotsomitopoulos
3acdccb339 Disable polls from within threads but allow users to vote if the poll is a root thread message 2021-12-15 16:31:58 +02:00
ariskotsomitopoulos
20357ce5c4 - Fix remaining conflicts with develop
- Disable thread awareness when threads are enabled
2021-12-15 14:38:08 +02:00
ariskotsomitopoulos
cd95fc41e4 Merge branch 'develop' into feature/aris/threads
# Conflicts:
#	library/ui-styles/src/main/res/values/dimens.xml
#	matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt
#	matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
#	matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt
#	matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
#	vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
#	vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt
#	vector/src/main/java/im/vector/app/features/command/Command.kt
#	vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt
#	vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
#	vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
#	vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewEvents.kt
#	vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
#	vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt
#	vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
#	vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
#	vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt
#	vector/src/main/java/im/vector/app/features/navigation/Navigator.kt
#	vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt
#	vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt
#	vector/src/main/res/layout/fragment_room_detail.xml
2021-12-14 20:18:54 +02:00
ariskotsomitopoulos
6a33c41091 Fix stickers in unencrypted rooms 2021-12-14 17:45:07 +02:00
ariskotsomitopoulos
2aa24f0a0d Fix threads sort order, newest first 2021-12-14 16:30:59 +02:00
ariskotsomitopoulos
5ceed4096e Fix threads sort order, newest first 2021-12-14 15:44:38 +02:00
ariskotsomitopoulos
d56281dca7 - Enhance local notification to work with read receipt & the latest chunk
- Local notification mentioning system
- Fix/Improve thread list filtering
2021-12-14 13:35:08 +02:00
ariskotsomitopoulos
5c015a7444 Support stickers in threads 2021-12-10 20:15:39 +02:00
ariskotsomitopoulos
57ef0b59ab Disable local echo for normal messages while there is a duplication 2021-12-09 20:29:13 +02:00
ariskotsomitopoulos
b1d4031a76 Add/Fix local echo to threads timeline 2021-12-09 16:33:11 +02:00
ariskotsomitopoulos
c40a686cff Implement LOCAL thread notifications that work only on real time. 2021-12-03 18:15:25 +00:00
ariskotsomitopoulos
d1bb96cec0 Threads notification badge UI 2021-12-03 11:30:30 +00:00
ariskotsomitopoulos
0241d66f8e Enhance search functionality to support threads 2021-12-01 12:57:53 +00:00
ariskotsomitopoulos
e7b8b90b0a Highlight the whole message along with the thread summary 2021-11-30 16:05:45 +00:00
ariskotsomitopoulos
53ca86dc6c Permalink handling for thread events 2021-11-29 18:08:16 +00:00
ariskotsomitopoulos
2a83e93265 Delete root message UI 2021-11-29 15:00:27 +02:00
ariskotsomitopoulos
c4967a2871 Handle chunks merging with thread summary
Add animation to fragment transition with offset for recyclerview initialization
Support threads on deleted events
2021-11-25 17:59:28 +02:00
ariskotsomitopoulos
afc69c77bd Add local filtering in thread list 2021-11-24 18:23:33 +02:00
ariskotsomitopoulos
e2bf3e7097 Add navigation to thread from the thread list
Add thread creation from photos, files, images, audios, replies etc
Add slide animation to view threads from thread list
Add click listener on room summary to handle cases like ( there is an image and the user upon click should view the image instead of navigating to the thread, so he can click the thread summary )
2021-11-23 22:22:58 +02:00
ariskotsomitopoulos
5e5ce614ef Add date in view all threads
UI Improvements on threads summary
Add View In Room bottom sheet action from within thread timeline root message
2021-11-23 17:09:58 +02:00
ariskotsomitopoulos
722f367690 View all threads screen implementation & UI
Add user friendly message thread summary on the SDK side
Fix not encrypted rooms thread summaries
2021-11-23 13:34:24 +02:00
ariskotsomitopoulos
586b3d8caa - Add specific toolbar for threads
- Renamed RoomDetailFragment to TimelineFragment while it should be reused from threads
 - View root thread message in room functionality
2021-11-22 14:02:17 +02:00
ariskotsomitopoulos
3de0f7bf37 Add sending file to thread support
** Important while this feature depends on local echo, should be added local echo support in threads to work 100%
2021-11-18 15:48:17 +02:00
ariskotsomitopoulos
3d9350091e Add Replies support from within a thread 2021-11-17 13:09:27 +02:00
ariskotsomitopoulos
4160688f83 Supporting command in threads 2021-11-16 14:59:30 +02:00
ariskotsomitopoulos
8c539426e6 - Thread Summary along with optimization
- Create new thread & reply to thread
2021-11-15 19:17:13 +02:00
ariskotsomitopoulos
ecc9b59ad1 Reply In Thread, create a new thread timeline 2021-11-08 20:46:37 +02:00
ariskotsomitopoulos
cb0fefa74d Add changelog file 2021-11-04 09:33:32 +02:00
ariskotsomitopoulos
a2a2315f9c Make room thread detail text composer visible 2021-10-21 16:53:20 +03:00
ariskotsomitopoulos
cb6376670b Add room avatar to threads activities 2021-10-21 12:25:43 +03:00
ariskotsomitopoulos
ab87937e5b Threads init commit 2021-10-20 18:39:59 +03:00
345 changed files with 9347 additions and 2534 deletions

View File

@@ -1,6 +1,6 @@
name: Release checklist
description: Checklist for each release. This template is only for the core team.
title: "[Release] Element Android v"
title: "[Release] Element Android v"
labels: [🚀 Release]
assignees:
- bmarty
@@ -10,7 +10,7 @@ body:
id: checklist
attributes:
label: Release checklist
description: For the template example, we are releasing the version 1.1.10. Replace 1.1.10 with the version in the issue body.
description: For the template example, we are releasing the version 1.2.3. Replace 1.2.3 with the version in the issue body.
placeholder: |
If you are reading this, you have deleted the content of the release template: undo the deletion or start again.
value: |
@@ -22,35 +22,41 @@ body:
### Do the release
- [ ] Create release with gitflow, branch name `release/1.1.10`
- [ ] Create release with gitflow, branch name `release/1.2.3`
- [ ] Check the crashes from the PlayStore
- [ ] Check the rageshake with the current dev version: https://github.com/matrix-org/element-android-rageshakes/labels/1.1.10-dev
- [ ] Check the rageshake with the current dev version: https://github.com/matrix-org/element-android-rageshakes/labels/1.2.3-dev
- [ ] Run the integration test, and especially `UiAllScreensSanityTest.allScreensTest()`
- [ ] Create an account on matrix.org
- [ ] Run towncrier: `towncrier --version v1.1.10 --draft` (remove `--draft` do write the file CHANGES.md)
- [ ] Create an account on matrix.org and do some smoke tests that the sanity test does not cover like: 1-1 call, 1-1 video call, Jitsi call for instance
- [ ] Run towncrier: `towncrier --version v1.2.3 --draft` (remove `--draft` do write the file CHANGES.md)
- [ ] Check that the folder `changelog.d` is empty. It can happen that some remaining files stay here
- [ ] Check the file CHANGES.md consistency. It's possible to reorder items (most important changes first) or change their section if relevant. Also an opportunity to fix some typo, or rewrite things
- [ ] Add file for fastlane under ./fastlane/metadata/android/en-US/changelogs
- [ ] Push the branch and start a draft PR (will not be merged), to check that the CI is happy with all the changes.
- [ ] Finish release with gitflow, delete the draft PR
- [ ] Push `main` and the new tag `v1.1.10` to origin
- [ ] (optional) Push the branch and start a draft PR (will not be merged), to check that the CI is happy with all the changes.
- [ ] Finish release with gitflow, delete the draft PR (if created)
- [ ] Push `main` and the new tag `v1.2.3` to origin
- [ ] Checkout `develop`
- [ ] Increase version in `./vector/build.gradle`
- [ ] Increase version (versionPatch + 2) in `./vector/build.gradle`
- [ ] Change the value of SDK_VERSION in the file `./matrix-sdk-android/build.gradle`
- [ ] Commit and push `develop`
- [ ] Wait for [Buildkite](https://buildkite.com/matrix-dot-org/element-android/builds?branch=main) to build the `main` branch.
- [ ] Run the script `~/scripts/releaseElement.sh`. It will download the APKs from Buildkite check them and sign them.
- [ ] Install the APK on your phone to check that the upgrade went well (no init sync, etc.)
- [ ] Create the release on gitHub [from the tag](https://github.com/vector-im/element-android/tags), copy paste the block from the file CHANGES.md
- [ ] Add the 4 signed APKs to the GitHub release
- [ ] Ping the Android Internal room
### Once tested and validated internally
- [ ] Create a new beta release on the GooglePlay console and upload the 4 signed Apks.
- [ ] Check that the version codes are correct
- [ ] Copy the fastlane change to the GooglePlay console in the section en-GB.
- [ ] Push to beta release to 100% of the users
- [ ] Create the release on gitHub [from the tag](https://github.com/vector-im/element-android/tags), copy paste the block from the file CHANGES.md
- [ ] Add the 4 signed APKs to the GitHub release
- [ ] Ping the Android Internal room
- [ ] Add an entry in the internal diary
- [ ] Notify the F-Droid team so that they can schedule the publication on F-Droid
### Once Live on PlayStore
- [ ] Ping the Android public room and update its topic
- [ ] Add an entry in the internal diary
### After at least 2 days
@@ -62,6 +68,8 @@ body:
### Android SDK2
The SDK2 and the sample app are released only when Element has been pushed to production.
- [ ] Checkout the `main` branch on Element Android project
#### On the SDK2 project

View File

@@ -180,6 +180,7 @@ jobs:
body="$(cat ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml | grep "<testsuite" | sed "s@.*tests=\(.*\)time=.*@\1@")"
echo "::set-output name=permalink::passed=$body"
- name: Find Comment
if: github.event_name == 'pull_request'
uses: peter-evans/find-comment@v1
id: fc
with:
@@ -187,6 +188,7 @@ jobs:
comment-author: 'github-actions[bot]'
body-includes: Integration Tests Results
- name: Publish results to PR
if: github.event_name == 'pull_request'
uses: peter-evans/create-or-update-comment@v1
with:
comment-id: ${{ steps.fc.outputs.comment-id }}

View File

@@ -54,7 +54,7 @@ jobs:
echo "::set-output name=body::$body"
fi
- name: Find Comment
if: always()
if: always() && github.event_name == 'pull_request'
uses: peter-evans/find-comment@v1
id: fc
with:
@@ -62,7 +62,7 @@ jobs:
comment-author: 'github-actions[bot]'
body-includes: Ktlint Results
- name: Add comment if needed
if: always() && steps.ktlint-results.outputs.add_comment == 'true'
if: always() && github.event_name == 'pull_request' && steps.ktlint-results.outputs.add_comment == 'true'
uses: peter-evans/create-or-update-comment@v1
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
@@ -73,7 +73,7 @@ jobs:
${{ steps.ktlint-results.outputs.body }}
edit-mode: replace
- name: Delete comment if needed
if: always() && steps.fc.outputs.comment-id != '' && steps.ktlint-results.outputs.add_comment == 'false'
if: always() && github.event_name == 'pull_request' && steps.fc.outputs.comment-id != '' && steps.ktlint-results.outputs.add_comment == 'false'
uses: actions/github-script@v3
with:
script: |

View File

@@ -202,3 +202,53 @@ jobs:
env:
PROJECT_ID: "PN_kwDOAM0swc3m-g"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
move_ftue_issues:
name: Z-FTUE to Mobile FTUE board
runs-on: ubuntu-latest
# Skip in forks
if: >
github.repository == 'vector-im/element-android' &&
contains(github.event.issue.labels.*.name, 'Z-FTUE')
steps:
- uses: octokit/graphql-action@v2.x
with:
headers: '{"GraphQL-Features": "projects_next_graphql"}'
query: |
mutation add_to_project($projectid:ID!,$contentid:ID!) {
addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) {
projectNextItem {
id
}
}
}
projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.issue.node_id }}
env:
PROJECT_ID: "PN_kwDOAM0swc4AAqVx"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
move_WTF_issues:
name: Z-WTF to WTF board
runs-on: ubuntu-latest
# Skip in forks
if: >
github.repository == 'vector-im/element-android' &&
contains(github.event.issue.labels.*.name, 'Z-WTF')
steps:
- uses: octokit/graphql-action@v2.x
with:
headers: '{"GraphQL-Features": "projects_next_graphql"}'
query: |
mutation add_to_project($projectid:ID!,$contentid:ID!) {
addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) {
projectNextItem {
id
}
}
}
projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.issue.node_id }}
env:
PROJECT_ID: "PN_kwDOAM0swc4AArk0"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}

View File

@@ -1,3 +1,35 @@
Changes in Element v1.4.0 (2022-02-09)
======================================
Features ✨
----------
- Initial implementation of thread messages ([#4746](https://github.com/vector-im/element-android/issues/4746))
- Support message bubbles in timeline. ([#4937](https://github.com/vector-im/element-android/issues/4937))
- Support generic location pin ([#5146](https://github.com/vector-im/element-android/issues/5146))
- Retrieve map style url from .well-known ([#5175](https://github.com/vector-im/element-android/issues/5175))
Bugfixes 🐛
----------
- Fixes non sans-serif font weights being ignored ([#3907](https://github.com/vector-im/element-android/issues/3907))
- Fixing missing/intermittent notifications on the google play variant when wifi is enabled ([#5038](https://github.com/vector-im/element-android/issues/5038))
- Fixes call statuses in the timeline for missed/rejected calls and connected calls. ([#5088](https://github.com/vector-im/element-android/issues/5088))
- Fix fallback permalink when threads are disabled ([#5128](https://github.com/vector-im/element-android/issues/5128))
- Analytics: aligns use case identifying with iOS implementation ([#5142](https://github.com/vector-im/element-android/issues/5142))
- Fix location rendering in timeline if map cannot be loaded ([#5143](https://github.com/vector-im/element-android/issues/5143))
Other changes
-------------
- "Invite users to space" dialog now closed when user choose invite method ([#4295](https://github.com/vector-im/element-android/issues/4295))
- Changed layout for space card and room card used at "explore room" screen and space/room invite dialogs ([#4304](https://github.com/vector-im/element-android/issues/4304))
- Removed spaces restricted search hint dialogs ([#4315](https://github.com/vector-im/element-android/issues/4315))
- Remove Search from room options if not available ([#4641](https://github.com/vector-im/element-android/issues/4641))
- Qr code scanning fragments merged into one ([#4873](https://github.com/vector-im/element-android/issues/4873))
- Fix CI/CD errors after merges for quality and integration tests ([#5118](https://github.com/vector-im/element-android/issues/5118))
- Added automation for the Z-FTUE label to add issues to the FTUE Project Board ([#5120](https://github.com/vector-im/element-android/issues/5120))
- Added automation for WTF labels to move to WTF project board ([#5148](https://github.com/vector-im/element-android/issues/5148))
- Update WTF automation to fix it ([#5173](https://github.com/vector-im/element-android/issues/5173))
Changes in Element v1.3.18 (2022-02-03)
=======================================

View File

@@ -14,7 +14,8 @@ It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-androi
[<img src="resources/img/google-play-badge.png" alt="Get it on Google Play" height="60">](https://play.google.com/store/apps/details?id=im.vector.app)
[<img src="resources/img/f-droid-badge.png" alt="Get it on F-Droid" height="60">](https://f-droid.org/app/im.vector.app)
Nightly build: [![Buildkite](https://badge.buildkite.com/ad0065c1b70f557cd3b1d3d68f9c2154010f83c4d6f71706a9.svg?branch=develop)](https://buildkite.com/matrix-dot-org/element-android/builds?branch=develop)
Nightly build: [![Buildkite](https://badge.buildkite.com/ad0065c1b70f557cd3b1d3d68f9c2154010f83c4d6f71706a9.svg?branch=develop)](https://buildkite.com/matrix-dot-org/element-android/builds?branch=develop) Nighly sanity test status: [![allScreensTest](https://github.com/vector-im/element-android/actions/workflows/sanity_test.yml/badge.svg)](https://github.com/vector-im/element-android/actions/workflows/sanity_test.yml)
# New Android SDK

View File

@@ -36,6 +36,12 @@ allprojects {
apply plugin: "org.jlleitschuh.gradle.ktlint"
repositories {
mavenCentral {
content {
groups.mavenCentral.regex.each { includeGroupByRegex it }
groups.mavenCentral.group.each { includeGroup it }
}
}
maven {
url 'https://jitpack.io'
content {
@@ -59,12 +65,6 @@ allprojects {
groups.google.group.each { includeGroup it }
}
}
mavenCentral {
content {
groups.mavenCentral.regex.each { includeGroupByRegex it }
groups.mavenCentral.group.each { includeGroup it }
}
}
//noinspection JcenterRepositoryObsolete
jcenter {
content {
@@ -144,6 +144,11 @@ project(":diff-match-patch") {
}
}
// Global configurations across all modules
ext {
isThreadingEnabled = true
}
//project(":matrix-sdk-android") {
// sonarqube {
// properties {

View File

@@ -0,0 +1,2 @@
Hlavní změny v této verzi: Odeslání vlastní polohy do libovolné místnosti. Možnost úpravy hlasování.
Úplný seznam změn: https://github.com/vector-im/element-android/releases/tag/v1.3.16

View File

@@ -0,0 +1,2 @@
Hauptänderungen: Du kannst ab sofort deinen Standort an deine Räume senden und Abstimmungen bearbeiten.
Alle Änderungen: https://github.com/vector-im/element-android/releases/tag/v1.3.16

View File

@@ -0,0 +1,2 @@
Main changes in this version: Initial implementation of thread messages. Message bubbles.
Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.4.0

View File

@@ -0,0 +1,2 @@
Põhilised muutused selles versioonis: oma asukoha saatmine jututuppa ja küsitluste muutmise võimalus.
Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases/tag/v1.3.16

View File

@@ -0,0 +1,2 @@
تغییرات عمده در این نگارشنخستین تغییر در صفحه‌های راه‌اندازی شامل وارد شدن به تجزیه‌ها. پشتیبانی از رویدادهایی با ریاضیات افزوده در آزمایشگاه‌ها.
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.13

View File

@@ -0,0 +1,2 @@
تغییرات عمده در این نگارشنخستین تغییر در صفحه‌های راه‌اندازی شامل وارد شدن به تجزیه‌ها. پشتیبانی از رویدادهایی با ریاضیات افزوده در آزمایشگاه‌ها.
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.14

View File

@@ -0,0 +1,2 @@
تغییرات عمده در این نگارشنخستین تغییر در صفحه‌های راه‌اندازی شامل وارد شدن به تجزیه‌ها. پشتیبانی از رویدادهایی با ریاضیات افزوده در آزمایشگاه‌ها.
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.15

View File

@@ -0,0 +1,2 @@
تغییرات عمده در این نگارش: فرستادن مکانتان به هر اتاقی. ویرایش نظرسنجی.
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.16

View File

@@ -0,0 +1,2 @@
Principaux changements pour cette version : Premier changement dans lécran de bienvenue, y compris ladhésion aux données danalyses. Support des événements avec opération mathématiques ajoutées dans les labs.
Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.3.13

View File

@@ -0,0 +1,2 @@
Principaux changements pour cette version : Premier changement dans lécran de bienvenue, y compris ladhésion aux données danalyses. Support des événements avec opération mathématiques ajoutées dans les labs.
Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.3.14

View File

@@ -0,0 +1,2 @@
Principaux changements pour cette version : Premier changement dans lécran de bienvenue, y compris ladhésion aux données danalyses. Support des événements avec opération mathématiques ajoutées dans les labs.
Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.3.15

View File

@@ -0,0 +1,2 @@
Principaux changements pour cette version : envoi de votre position dans nimporte quelle salon. Édition des sondage.
Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.3.16

View File

@@ -0,0 +1,2 @@
Fő változás ebben a verzióban: földrajzi helyzet küldése bármely szobába. Szavazás szerkesztése
Teljes változásnapló: https://github.com/vector-im/element-android/releases/tag/v1.3.16

View File

@@ -1,2 +1,2 @@
Perubahan utama dalam versi ini: Perubahan pertama di layar permulaan, termasuk analitik opt-in. Dukungan untuk Peristiwa dengan Matematika ditambahkan di Uji Coba.
Changelog lanjutan:
Changelog lanjutan: https://github.com/vector-im/element-android/releases/tag/v1.3.13

View File

@@ -0,0 +1,2 @@
Perubahan utama dalam versi ini: Kirim lokasi Anda ke ruangan apa saja. Edit poll.
Changelog lanjutan: https://github.com/vector-im/element-android/releases/tag/v1.3.16

View File

@@ -0,0 +1,2 @@
Modifiche principali in questa versione: invia la tua posizione in qualsiasi stanza. Modifica sondaggi.
Cronologia completa: https://github.com/vector-im/element-android/releases/tag/v1.3.16

View File

@@ -0,0 +1,2 @@
Principais mudanças nesta versão: envie sua localização para qualquer sala. Editar sondagem.
Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.3.16

View File

@@ -0,0 +1,2 @@
Основные изменения в этой версии: отправьте свое местоположение в любую комнату. Редактирование опроса.
Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.3.16

View File

@@ -0,0 +1,2 @@
Hlavné zmeny v tejto verzii: odoslanie polohy do ľubovoľnej miestnosti. Úprava ankety.
Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.3.16

View File

@@ -0,0 +1,2 @@
Ndryshimet kryesore në këtë version: Ndryshimi i parë në skenat e mirëseardhjes, përfshi zgjedhje për pjesëmarrje në Analiza. Në laboratorë u shtua mbulim për Akte me Formula Matematikore.
Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.3.14

View File

@@ -0,0 +1,2 @@
Ndryshimet kryesore në këtë version: Ndryshimi i parë në skenat e mirëseardhjes, përfshi zgjedhje për pjesëmarrje në Analiza. Në laboratorë u shtua mbulim për Akte me Formula Matematikore.
Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.3.15

View File

@@ -0,0 +1,2 @@
Ndryshimet kryesore në këtë version: dërgojeni vendndodhjen tuaj te cilado dhomë. Përpunoni pyetësor.
Regjistër i plotës ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.3.16

View File

@@ -0,0 +1,2 @@
Huvudsakliga ändringar i den här versionen: Första ändringen på introduktionsskärmar, inklusive opt-in för statistik. Stöd för händelser med matte tillagd i experiment.
Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.3.13

View File

@@ -0,0 +1,2 @@
Huvudsakliga ändringar i den här versionen: Första ändringen på introduktionsskärmar, inklusive opt-in för statistik. Stöd för händelser med matte tillagd i experiment.
Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.3.14

View File

@@ -0,0 +1,2 @@
Huvudsakliga ändringar i den här versionen: Första ändringen på introduktionsskärmar, inklusive opt-in för statistik. Stöd för händelser med matte tillagd i experiment.
Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.3.15

View File

@@ -0,0 +1,2 @@
Huvudsakliga ändringar i den här versionen: skicka din plats till vilket rum som helst. Redigera omröstningar.
Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.3.16

View File

@@ -0,0 +1,2 @@
Основні зміни у цій версії: надсилання свого місцеперебування у будь-яку кімнату. Редагування опитувань.
Вичерпний перелік змін: https://github.com/vector-im/element-android/releases/tag/v1.3.16

View File

@@ -0,0 +1,2 @@
此版本中的主要變動:將您的位置傳送給任何聊天室。編輯投票。
完整的變更紀錄https://github.com/vector-im/element-android/releases/tag/v1.3.16

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromXDelta="-100%p" android:toXDelta="0"
android:duration="@android:integer/config_mediumAnimTime"/>
</set>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:startOffset="250"
android:fromXDelta="100%p" android:toXDelta="0"
android:duration="@android:integer/config_mediumAnimTime"/>
</set>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:startOffset="250"
android:fromXDelta="0" android:toXDelta="-100%p"
android:duration="@android:integer/config_mediumAnimTime"/>
</set>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromXDelta="0" android:toXDelta="100%p"
android:duration="@android:integer/config_mediumAnimTime"/>
</set>

View File

@@ -2,9 +2,6 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Tint color is provided by the theme -->
<solid android:color="@android:color/black" />
<size
android:width="240dp"
android:height="44dp" />
<corners
android:bottomLeftRadius="12dp"
android:bottomRightRadius="12dp"

View File

@@ -2,16 +2,14 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<corners android:radius="8dp" />
<solid android:color="?vctr_room_active_widgets_banner_bg" />
<shape android:shape="oval">
<solid android:color="?vctr_system" />
</shape>
</item>
<item android:id="@android:id/progress">
<clip>
<shape>
<corners android:radius="8dp" />
<shape android:shape="oval">
<solid android:color="@color/vctr_notice_secondary_alpha12" />
</shape>
</clip>

View File

@@ -1,6 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="is_rtl">true</bool>
<dimen name="menu_item_ripple_size">28dp</dimen>
</resources>

View File

@@ -4,6 +4,4 @@
<!-- Created to detect what has to be implemented (especially in the settings) -->
<bool name="false_not_implemented">false</bool>
<bool name="is_rtl">false</bool>
</resources>

View File

@@ -137,4 +137,5 @@
<attr name="vctr_presence_indicator_offline" format="color" />
<color name="vctr_presence_indicator_offline_light">@color/palette_gray_100</color>
<color name="vctr_presence_indicator_offline_dark">@color/palette_gray_450</color>
</resources>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Timeline bubble background colors -->
<attr name="vctr_message_bubble_inbound" format="color" />
<color name="vctr_message_bubble_inbound_light">#E8EDF4</color>
<color name="vctr_message_bubble_inbound_dark">#21262C</color>
<attr name="vctr_message_bubble_outbound" format="color" />
<color name="vctr_message_bubble_outbound_light">#E7F8F3</color>
<color name="vctr_message_bubble_outbound_dark">#133A34</color>
</resources>

View File

@@ -15,6 +15,8 @@
<dimen name="item_decoration_left_margin">72dp</dimen>
<dimen name="item_event_message_state_size">16dp</dimen>
<dimen name="item_event_message_media_button_size">32dp</dimen>
<dimen name="chat_avatar_size">40dp</dimen>
<dimen name="member_list_avatar_size">60dp</dimen>
@@ -42,12 +44,23 @@
<!-- Preview Url -->
<dimen name="preview_url_view_corner_radius">8dp</dimen>
<dimen name="preview_url_view_image_max_height">160dp</dimen>
<dimen name="menu_item_icon_size">24dp</dimen>
<dimen name="menu_item_size">48dp</dimen>
<dimen name="menu_item_ripple_size">48dp</dimen>
<!-- Composer -->
<dimen name="composer_min_height">56dp</dimen>
<dimen name="composer_attachment_size">52dp</dimen>
<dimen name="composer_attachment_margin">1dp</dimen>
<dimen name="chat_bubble_margin_start">28dp</dimen>
<dimen name="chat_bubble_margin_end">62dp</dimen>
<dimen name="chat_bubble_fixed_size">300dp</dimen>
<dimen name="chat_bubble_corner_radius">12dp</dimen>
<!-- Onboarding -->
<item name="ftue_auth_gutter_start_percent" format="float" type="dimen">0.05</item>
<item name="ftue_auth_gutter_end_percent" format="float" type="dimen">0.95</item>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MessageBubble">
<attr name="incoming_style" format="boolean" />
<attr name="show_time_overlay" format="boolean" />
<attr name="is_first" format="boolean" />
<attr name="is_last" format="boolean" />
</declare-styleable>
</resources>

View File

@@ -6,6 +6,7 @@
<style name="Widget.Vector.ProgressBar.Horizontal.File">
<item name="android:indeterminateOnly">false</item>
<item name="android:progressDrawable">@drawable/file_progress_bar</item>
<item name="android:progressBackgroundTint">?android:colorBackground</item>
<item name="android:minHeight">10dp</item>
<item name="android:maxHeight">40dp</item>
</style>

View File

@@ -4,12 +4,23 @@
<style name="TimelineContentStubBaseParams">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_marginStart">8dp</item>
<item name="android:layout_marginLeft">8dp</item>
<item name="android:layout_marginEnd">8dp</item>
<item name="android:layout_marginRight">8dp</item>
<item name="android:layout_marginBottom">4dp</item>
<item name="android:layout_marginTop">4dp</item>
</style>
<style name="TimelineContentStubContainerParams">
<item name="android:paddingStart">8dp</item>
<item name="android:paddingEnd">8dp</item>
<item name="android:paddingTop">4dp</item>
<item name="android:paddingBottom">4dp</item>
</style>
<style name="TimelineContentMediaPillStyle">
<item name="android:paddingStart">8dp</item>
<item name="android:paddingEnd">8dp</item>
<item name="android:paddingTop">6dp</item>
<item name="android:paddingBottom">6dp</item>
<item name="minHeight">48dp</item>
<item name="android:background">@drawable/bg_media_pill</item>
<item name="android:backgroundTint">?vctr_content_quinary</item>
</style>
</resources>

View File

@@ -31,6 +31,8 @@
<item name="vctr_waiting_background_color">@color/vctr_waiting_background_color_dark</item>
<item name="vctr_chat_effect_snow_background">@color/vctr_chat_effect_snow_background_dark</item>
<item name="vctr_toolbar_background">@color/element_system_dark</item>
<item name="vctr_message_bubble_inbound">@color/vctr_message_bubble_inbound_dark</item>
<item name="vctr_message_bubble_outbound">@color/vctr_message_bubble_outbound_dark</item>
<!-- room message colors -->
<item name="vctr_notice_secondary">#61708B</item>
@@ -105,9 +107,6 @@
<!-- disable the overscroll because setOverscrollHeader/Footer don't always work -->
<item name="android:overScrollMode">never</item>
<!-- fonts -->
<item name="android:typeface">sans</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
<item name="pf_lock_screen">@style/PinCodeScreenStyle</item>

View File

@@ -31,6 +31,8 @@
<item name="vctr_waiting_background_color">@color/vctr_waiting_background_color_light</item>
<item name="vctr_chat_effect_snow_background">@color/vctr_chat_effect_snow_background_light</item>
<item name="vctr_toolbar_background">@color/element_background_light</item>
<item name="vctr_message_bubble_inbound">@color/vctr_message_bubble_inbound_light</item>
<item name="vctr_message_bubble_outbound">@color/vctr_message_bubble_outbound_light</item>
<!-- room message colors -->
<item name="vctr_notice_secondary">#61708B</item>
@@ -105,9 +107,6 @@
<!-- disable the overscroll because setOverscrollHeader/Footer don't always work -->
<item name="android:overScrollMode">never</item>
<!-- fonts -->
<item name="android:typeface">sans</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
<item name="pf_lock_screen">@style/PinCodeScreenStyle</item>

View File

@@ -32,6 +32,8 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional
typealias ThreadRootEvent = TimelineEvent
class FlowRoom(private val room: Room) {
fun liveRoomSummary(): Flow<Optional<RoomSummary>> {
@@ -98,6 +100,20 @@ class FlowRoom(private val room: Room) {
fun liveNotificationState(): Flow<RoomNotificationState> {
return room.getLiveRoomNotificationState().asFlow()
}
fun liveThreadList(): Flow<List<ThreadRootEvent>> {
return room.getAllThreadsLive().asFlow()
.startWith(room.coroutineDispatchers.io) {
room.getAllThreads()
}
}
fun liveLocalUnreadThreadList(): Flow<List<ThreadRootEvent>> {
return room.getMarkedThreadNotificationsLive().asFlow()
.startWith(room.coroutineDispatchers.io) {
room.getMarkedThreadNotifications()
}
}
}
fun Room.flow(): FlowRoom {

View File

@@ -31,13 +31,15 @@ android {
// that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true'
buildConfigField "String", "SDK_VERSION", "\"1.3.18\""
buildConfigField "String", "SDK_VERSION", "\"1.3.19\""
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
resValue "string", "git_sdk_revision", "\"${gitRevision()}\""
resValue "string", "git_sdk_revision_unix_date", "\"${gitRevisionUnixDate()}\""
resValue "string", "git_sdk_revision_date", "\"${gitRevisionDate()}\""
// Indicates whether or not threading support is enabled
buildConfigField "Boolean", "THREADING_ENABLED", "${isThreadingEnabled}"
defaultConfig {
consumerProguardFiles 'proguard-rules.pro'
}
@@ -139,6 +141,9 @@ dependencies {
kapt 'dk.ilios:realmfieldnameshelper:2.0.0'
// Shared Preferences
implementation libs.androidx.preferenceKtx
// Work
implementation libs.androidx.work

View File

@@ -157,14 +157,20 @@ class CommonTestHelper(context: Context) {
/**
* Will send nb of messages provided by count parameter but waits every 10 messages to avoid gap in sync
*/
private fun sendTextMessagesBatched(timeline: Timeline, room: Room, message: String, count: Int, timeout: Long): List<TimelineEvent> {
private fun sendTextMessagesBatched(timeline: Timeline, room: Room, message: String, count: Int, timeout: Long, rootThreadEventId: String? = null): List<TimelineEvent> {
val sentEvents = ArrayList<TimelineEvent>(count)
(1 until count + 1)
.map { "$message #$it" }
.chunked(10)
.forEach { batchedMessages ->
batchedMessages.forEach { formattedMessage ->
room.sendTextMessage(formattedMessage)
if (rootThreadEventId != null) {
room.replyInThread(
rootThreadEventId = rootThreadEventId,
replyInThreadText = formattedMessage)
} else {
room.sendTextMessage(formattedMessage)
}
}
waitWithLatch(timeout) { latch ->
val timelineListener = object : Timeline.Listener {
@@ -196,6 +202,27 @@ class CommonTestHelper(context: Context) {
return sentEvents
}
/**
* Reply in a thread
* @param room the room where to send the messages
* @param message the message to send
* @param numberOfMessages the number of time the message will be sent
*/
fun replyInThreadMessage(
room: Room,
message: String,
numberOfMessages: Int,
rootThreadEventId: String,
timeout: Long = TestConstants.timeOutMillis): List<TimelineEvent> {
val timeline = room.createTimeline(null, TimelineSettings(10))
timeline.start()
val sentEvents = sendTextMessagesBatched(timeline, room, message, numberOfMessages, timeout, rootThreadEventId)
timeline.dispose()
// Check that all events has been created
assertEquals("Message number do not match $sentEvents", numberOfMessages.toLong(), sentEvents.size.toLong())
return sentEvents
}
// PRIVATE METHODS *****************************************************************************
/**

View File

@@ -0,0 +1,339 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.session.room.threads
import org.amshove.kluent.shouldBe
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldBeFalse
import org.amshove.kluent.shouldBeNull
import org.amshove.kluent.shouldBeTrue
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId
import org.matrix.android.sdk.api.session.events.model.isTextMessage
import org.matrix.android.sdk.api.session.events.model.isThread
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper
import java.util.concurrent.CountDownLatch
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
class ThreadMessagingTest : InstrumentedTest {
@Test
fun reply_in_thread_should_create_a_thread() {
val commonTestHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
val aliceSession = cryptoTestData.firstSession
val aliceRoomId = cryptoTestData.roomId
val aliceRoom = aliceSession.getRoom(aliceRoomId)!!
// Let's send a message in the normal timeline
val textMessage = "This is a normal timeline message"
val sentMessages = commonTestHelper.sendTextMessage(
room = aliceRoom,
message = textMessage,
nbOfMessages = 1)
val initMessage = sentMessages.first()
initMessage.root.isThread().shouldBeFalse()
initMessage.root.isTextMessage().shouldBeTrue()
initMessage.root.getRootThreadEventId().shouldBeNull()
initMessage.root.threadDetails?.isRootThread?.shouldBeFalse()
// Let's reply in timeline to that message
val repliesInThread = commonTestHelper.replyInThreadMessage(
room = aliceRoom,
message = "Reply In the above thread",
numberOfMessages = 1,
rootThreadEventId = initMessage.root.eventId.orEmpty())
val replyInThread = repliesInThread.first()
replyInThread.root.isThread().shouldBeTrue()
replyInThread.root.isTextMessage().shouldBeTrue()
replyInThread.root.getRootThreadEventId().shouldBeEqualTo(initMessage.root.eventId)
// The init normal message should now be a root thread event
val timeline = aliceRoom.createTimeline(null, TimelineSettings(30))
timeline.start()
aliceSession.startSync(true)
run {
val lock = CountDownLatch(1)
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
val initMessageThreadDetails = snapshot.firstOrNull {
it.root.eventId == initMessage.root.eventId
}?.root?.threadDetails
initMessageThreadDetails?.isRootThread?.shouldBeTrue()
initMessageThreadDetails?.numberOfThreads?.shouldBe(1)
true
}
timeline.addListener(eventsListener)
commonTestHelper.await(lock, 600_000)
}
aliceSession.stopSync()
}
@Test
fun reply_in_thread_should_create_a_thread_from_other_user() {
val commonTestHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
val aliceSession = cryptoTestData.firstSession
val aliceRoomId = cryptoTestData.roomId
val aliceRoom = aliceSession.getRoom(aliceRoomId)!!
// Let's send a message in the normal timeline
val textMessage = "This is a normal timeline message"
val sentMessages = commonTestHelper.sendTextMessage(
room = aliceRoom,
message = textMessage,
nbOfMessages = 1)
val initMessage = sentMessages.first()
initMessage.root.isThread().shouldBeFalse()
initMessage.root.isTextMessage().shouldBeTrue()
initMessage.root.getRootThreadEventId().shouldBeNull()
initMessage.root.threadDetails?.isRootThread?.shouldBeFalse()
// Let's reply in timeline to that message from another user
val bobSession = cryptoTestData.secondSession!!
val bobRoomId = cryptoTestData.roomId
val bobRoom = bobSession.getRoom(bobRoomId)!!
val repliesInThread = commonTestHelper.replyInThreadMessage(
room = bobRoom,
message = "Reply In the above thread",
numberOfMessages = 1,
rootThreadEventId = initMessage.root.eventId.orEmpty())
val replyInThread = repliesInThread.first()
replyInThread.root.isThread().shouldBeTrue()
replyInThread.root.isTextMessage().shouldBeTrue()
replyInThread.root.getRootThreadEventId().shouldBeEqualTo(initMessage.root.eventId)
// The init normal message should now be a root thread event
val timeline = aliceRoom.createTimeline(null, TimelineSettings(30))
timeline.start()
aliceSession.startSync(true)
run {
val lock = CountDownLatch(1)
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
val initMessageThreadDetails = snapshot.firstOrNull { it.root.eventId == initMessage.root.eventId }?.root?.threadDetails
initMessageThreadDetails?.isRootThread?.shouldBeTrue()
initMessageThreadDetails?.numberOfThreads?.shouldBe(1)
true
}
timeline.addListener(eventsListener)
commonTestHelper.await(lock, 600_000)
}
aliceSession.stopSync()
bobSession.startSync(true)
run {
val lock = CountDownLatch(1)
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
val initMessageThreadDetails = snapshot.firstOrNull { it.root.eventId == initMessage.root.eventId }?.root?.threadDetails
initMessageThreadDetails?.isRootThread?.shouldBeTrue()
initMessageThreadDetails?.numberOfThreads?.shouldBe(1)
true
}
timeline.addListener(eventsListener)
commonTestHelper.await(lock, 600_000)
}
bobSession.stopSync()
}
@Test
fun reply_in_thread_to_timeline_message_multiple_times() {
val commonTestHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
val aliceSession = cryptoTestData.firstSession
val aliceRoomId = cryptoTestData.roomId
val aliceRoom = aliceSession.getRoom(aliceRoomId)!!
// Let's send 5 messages in the normal timeline
val textMessage = "This is a normal timeline message"
val sentMessages = commonTestHelper.sendTextMessage(
room = aliceRoom,
message = textMessage,
nbOfMessages = 5)
sentMessages.forEach {
it.root.isThread().shouldBeFalse()
it.root.isTextMessage().shouldBeTrue()
it.root.getRootThreadEventId().shouldBeNull()
it.root.threadDetails?.isRootThread?.shouldBeFalse()
}
// let's start the thread from the second message
val selectedInitMessage = sentMessages[1]
// Let's reply 40 times in the timeline to the second message
val repliesInThread = commonTestHelper.replyInThreadMessage(
room = aliceRoom,
message = "Reply In the above thread",
numberOfMessages = 40,
rootThreadEventId = selectedInitMessage.root.eventId.orEmpty())
repliesInThread.forEach {
it.root.isThread().shouldBeTrue()
it.root.isTextMessage().shouldBeTrue()
it.root.getRootThreadEventId()?.shouldBeEqualTo(selectedInitMessage.root.eventId.orEmpty()) ?: assert(false)
}
// The init normal message should now be a root thread event
val timeline = aliceRoom.createTimeline(null, TimelineSettings(30))
timeline.start()
aliceSession.startSync(true)
run {
val lock = CountDownLatch(1)
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
val initMessageThreadDetails = snapshot.firstOrNull { it.root.eventId == selectedInitMessage.root.eventId }?.root?.threadDetails
// Selected init message should be the thread root
initMessageThreadDetails?.isRootThread?.shouldBeTrue()
// All threads should be 40
initMessageThreadDetails?.numberOfThreads?.shouldBeEqualTo(40)
true
}
// Because we sent more than 30 messages we should paginate a bit more
timeline.paginate(Timeline.Direction.BACKWARDS, 50)
timeline.addListener(eventsListener)
commonTestHelper.await(lock, 600_000)
}
aliceSession.stopSync()
}
@Test
fun thread_summary_advanced_validation_after_multiple_messages_in_multiple_threads() {
val commonTestHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
val aliceSession = cryptoTestData.firstSession
val aliceRoomId = cryptoTestData.roomId
val aliceRoom = aliceSession.getRoom(aliceRoomId)!!
// Let's send 5 messages in the normal timeline
val textMessage = "This is a normal timeline message"
val sentMessages = commonTestHelper.sendTextMessage(
room = aliceRoom,
message = textMessage,
nbOfMessages = 5)
sentMessages.forEach {
it.root.isThread().shouldBeFalse()
it.root.isTextMessage().shouldBeTrue()
it.root.getRootThreadEventId().shouldBeNull()
it.root.threadDetails?.isRootThread?.shouldBeFalse()
}
// let's start the thread from the second message
val firstMessage = sentMessages[0]
val secondMessage = sentMessages[1]
// Alice will reply in thread to the second message 35 times
val aliceThreadRepliesInSecondMessage = commonTestHelper.replyInThreadMessage(
room = aliceRoom,
message = "Alice reply In the above second thread message",
numberOfMessages = 35,
rootThreadEventId = secondMessage.root.eventId.orEmpty())
// Let's reply in timeline to that message from another user
val bobSession = cryptoTestData.secondSession!!
val bobRoomId = cryptoTestData.roomId
val bobRoom = bobSession.getRoom(bobRoomId)!!
// Bob will reply in thread to the first message 35 times
val bobThreadRepliesInFirstMessage = commonTestHelper.replyInThreadMessage(
room = bobRoom,
message = "Bob reply In the above first thread message",
numberOfMessages = 42,
rootThreadEventId = firstMessage.root.eventId.orEmpty())
// Bob will also reply in second thread 5 times
val bobThreadRepliesInSecondMessage = commonTestHelper.replyInThreadMessage(
room = bobRoom,
message = "Another Bob reply In the above second thread message",
numberOfMessages = 20,
rootThreadEventId = secondMessage.root.eventId.orEmpty())
aliceThreadRepliesInSecondMessage.forEach {
it.root.isThread().shouldBeTrue()
it.root.isTextMessage().shouldBeTrue()
it.root.getRootThreadEventId()?.shouldBeEqualTo(secondMessage.root.eventId.orEmpty()) ?: assert(false)
}
bobThreadRepliesInFirstMessage.forEach {
it.root.isThread().shouldBeTrue()
it.root.isTextMessage().shouldBeTrue()
it.root.getRootThreadEventId()?.shouldBeEqualTo(firstMessage.root.eventId.orEmpty()) ?: assert(false)
}
bobThreadRepliesInSecondMessage.forEach {
it.root.isThread().shouldBeTrue()
it.root.isTextMessage().shouldBeTrue()
it.root.getRootThreadEventId()?.shouldBeEqualTo(secondMessage.root.eventId.orEmpty()) ?: assert(false)
}
// The init normal message should now be a root thread event
val timeline = aliceRoom.createTimeline(null, TimelineSettings(30))
timeline.start()
aliceSession.startSync(true)
run {
val lock = CountDownLatch(1)
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
val firstMessageThreadDetails = snapshot.firstOrNull { it.root.eventId == firstMessage.root.eventId }?.root?.threadDetails
val secondMessageThreadDetails = snapshot.firstOrNull { it.root.eventId == secondMessage.root.eventId }?.root?.threadDetails
// first & second message should be the thread root
firstMessageThreadDetails?.isRootThread?.shouldBeTrue()
secondMessageThreadDetails?.isRootThread?.shouldBeTrue()
// First thread message should contain 42
firstMessageThreadDetails?.numberOfThreads shouldBeEqualTo 42
// Second thread message should contain 35+20
secondMessageThreadDetails?.numberOfThreads shouldBeEqualTo 55
true
}
// Because we sent more than 30 messages we should paginate a bit more
timeline.paginate(Timeline.Direction.BACKWARDS, 50)
timeline.paginate(Timeline.Direction.BACKWARDS, 50)
timeline.addListener(eventsListener)
commonTestHelper.await(lock, 600_000)
}
aliceSession.stopSync()
}
}

View File

@@ -25,9 +25,14 @@ import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import org.matrix.android.sdk.api.session.room.model.relation.shouldRenderInThread
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.threads.ThreadDetails
import org.matrix.android.sdk.api.util.ContentUtils
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
@@ -98,6 +103,9 @@ data class Event(
@Transient
var sendStateDetails: String? = null
@Transient
var threadDetails: ThreadDetails? = null
fun sendStateError(): MatrixError? {
return sendStateDetails?.let {
val matrixErrorAdapter = MoshiProvider.providesMoshi().adapter(MatrixError::class.java)
@@ -123,6 +131,7 @@ data class Event(
it.mCryptoErrorReason = mCryptoErrorReason
it.sendState = sendState
it.ageLocalTs = ageLocalTs
it.threadDetails = threadDetails
}
}
@@ -185,6 +194,51 @@ data class Event(
return contentMap?.let { JSONObject(adapter.toJson(it)).toString(4) }
}
/**
* Returns a user friendly content depending on the message type.
* It can be used especially for message summaries.
* It will return a decrypted text message or an empty string otherwise.
*/
fun getDecryptedTextSummary(): String? {
if (isRedacted()) return "Message Deleted"
val text = getDecryptedValue() ?: return null
return when {
isReplyRenderedInThread() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text)
isFileMessage() -> "sent a file."
isAudioMessage() -> "sent an audio file."
isImageMessage() -> "sent an image."
isVideoMessage() -> "sent a video."
isSticker() -> "sent a sticker"
isPoll() -> getPollQuestion() ?: "created a poll."
else -> text
}
}
private fun Event.isQuote(): Boolean {
if (isReplyRenderedInThread()) return false
return getDecryptedValue("formatted_body")?.contains("<blockquote>") ?: false
}
/**
* Determines whether or not current event has mentioned the user
*/
fun isUserMentioned(userId: String): Boolean {
return getDecryptedValue("formatted_body")?.contains(userId) ?: false
}
/**
* Decrypt the message, or return the pure payload value if there is no encryption
*/
private fun getDecryptedValue(key: String = "body"): String? {
return if (isEncrypted()) {
@Suppress("UNCHECKED_CAST")
val decryptedContent = mxDecryptionResult?.payload?.get("content") as? JsonDict
decryptedContent?.get(key) as? String
} else {
content?.get(key) as? String
}
}
/**
* Tells if the event is redacted
*/
@@ -217,7 +271,7 @@ data class Event(
if (mCryptoError != other.mCryptoError) return false
if (mCryptoErrorReason != other.mCryptoErrorReason) return false
if (sendState != other.sendState) return false
if (threadDetails != other.threadDetails) return false
return true
}
@@ -236,6 +290,8 @@ data class Event(
result = 31 * result + (mCryptoError?.hashCode() ?: 0)
result = 31 * result + (mCryptoErrorReason?.hashCode() ?: 0)
result = 31 * result + sendState.hashCode()
result = 31 * result + threadDetails.hashCode()
return result
}
}
@@ -243,70 +299,101 @@ data class Event(
fun Event.isTextMessage(): Boolean {
return getClearType() == EventType.MESSAGE &&
when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_EMOTE,
MessageType.MSGTYPE_NOTICE -> true
else -> false
}
MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_EMOTE,
MessageType.MSGTYPE_NOTICE -> true
else -> false
}
}
fun Event.isImageMessage(): Boolean {
return getClearType() == EventType.MESSAGE &&
when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
MessageType.MSGTYPE_IMAGE -> true
else -> false
}
MessageType.MSGTYPE_IMAGE -> true
else -> false
}
}
fun Event.isVideoMessage(): Boolean {
return getClearType() == EventType.MESSAGE &&
when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
MessageType.MSGTYPE_VIDEO -> true
else -> false
}
MessageType.MSGTYPE_VIDEO -> true
else -> false
}
}
fun Event.isAudioMessage(): Boolean {
return getClearType() == EventType.MESSAGE &&
when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
MessageType.MSGTYPE_AUDIO -> true
else -> false
}
MessageType.MSGTYPE_AUDIO -> true
else -> false
}
}
fun Event.isFileMessage(): Boolean {
return getClearType() == EventType.MESSAGE &&
when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
MessageType.MSGTYPE_FILE -> true
else -> false
}
MessageType.MSGTYPE_FILE -> true
else -> false
}
}
fun Event.isAttachmentMessage(): Boolean {
return getClearType() == EventType.MESSAGE &&
when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
MessageType.MSGTYPE_IMAGE,
MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_VIDEO,
MessageType.MSGTYPE_FILE -> true
else -> false
}
MessageType.MSGTYPE_IMAGE,
MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_VIDEO,
MessageType.MSGTYPE_FILE -> true
else -> false
}
}
fun Event.isPoll(): Boolean = getClearType() == EventType.POLL_START || getClearType() == EventType.POLL_END
fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER
fun Event.getRelationContent(): RelationDefaultContent? {
return if (isEncrypted()) {
content.toModel<EncryptedEventContent>()?.relatesTo
} else {
content.toModel<MessageContent>()?.relatesTo
content.toModel<MessageContent>()?.relatesTo ?: run {
// Special case to handle stickers, while there is only a local msgtype for stickers
if (getClearType() == EventType.STICKER) {
getClearContent().toModel<MessageStickerContent>()?.relatesTo
} else {
null
}
}
}
}
/**
* Returns the poll question or null otherwise
*/
fun Event.getPollQuestion(): String? =
getPollContent()?.pollCreationInfo?.question?.question
/**
* Returns the relation content for a specific type or null otherwise
*/
fun Event.getRelationContentForType(type: String): RelationDefaultContent? =
getRelationContent()?.takeIf { it.type == type }
fun Event.isReply(): Boolean {
return getRelationContent()?.inReplyTo?.eventId != null
}
fun Event.isReplyRenderedInThread(): Boolean {
return isReply() && getRelationContent()?.inReplyTo?.shouldRenderInThread() == true
}
fun Event.isThread(): Boolean = getRelationContentForType(RelationType.IO_THREAD)?.eventId != null
fun Event.getRootThreadEventId(): String? = getRelationContentForType(RelationType.IO_THREAD)?.eventId
fun Event.isEdition(): Boolean {
return getRelationContent()?.takeIf { it.type == RelationType.REPLACE }?.eventId != null
return getRelationContentForType(RelationType.REPLACE)?.eventId != null
}
fun Event.getPresenceContent(): PresenceContent? {
@@ -315,3 +402,7 @@ fun Event.getPresenceContent(): PresenceContent? {
fun Event.isInvitation(): Boolean = type == EventType.STATE_ROOM_MEMBER &&
content?.toModel<RoomMemberContent>()?.membership == Membership.INVITE
fun Event.getPollContent(): MessagePollContent? {
return content.toModel<MessagePollContent>()
}

View File

@@ -28,9 +28,9 @@ object RelationType {
/** Lets you define an event which references an existing event.*/
const val REFERENCE = "m.reference"
/** Lets you define an thread event that belongs to another existing event.*/
// const val THREAD = "m.thread" // m.thread is not yet released in the backend
const val THREAD = "io.element.thread" // io.element.thread will be replaced by m.thread when it is released
/** Lets you define an event which is a thread reply to an existing event.*/
const val THREAD = "m.thread"
const val IO_THREAD = "io.element.thread"
/** Lets you define an event which adds a response to an existing event.*/
const val RESPONSE = "org.matrix.response"

View File

@@ -47,5 +47,9 @@ data class PreviewUrlData(
// Value of field "og:description"
val description: String?,
// Value of field "og:image"
val mxcUrl: String?
val mxcUrl: String?,
// Value of field "og:image:width"
val imageWidth: Int?,
// Value of field "og:image:height"
val imageHeight: Int?
)

View File

@@ -32,6 +32,7 @@ import org.matrix.android.sdk.api.session.room.send.DraftService
import org.matrix.android.sdk.api.session.room.send.SendService
import org.matrix.android.sdk.api.session.room.state.StateService
import org.matrix.android.sdk.api.session.room.tags.TagsService
import org.matrix.android.sdk.api.session.room.threads.ThreadsService
import org.matrix.android.sdk.api.session.room.timeline.TimelineService
import org.matrix.android.sdk.api.session.room.typing.TypingService
import org.matrix.android.sdk.api.session.room.uploads.UploadsService
@@ -45,6 +46,7 @@ import org.matrix.android.sdk.api.util.Optional
*/
interface Room :
TimelineService,
ThreadsService,
SendService,
DraftService,
ReadService,

View File

@@ -64,4 +64,12 @@ data class MessageLocationContent(
) : MessageContent {
fun getBestGeoUri() = locationInfo?.geoUri ?: geoUri
/**
* @return true if the location asset is a user location, not a generic one.
*/
fun isSelfLocation(): Boolean {
// Should behave like m.self if locationAsset is null
return locationAsset?.type == null || locationAsset.type == LocationAssetType.SELF
}
}

View File

@@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.room.model.relation
import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.message.PollType
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.Cancelable
@@ -45,6 +46,9 @@ import org.matrix.android.sdk.api.util.Optional
* m.reference - lets you define an event which references an existing event.
* When aggregated, currently doesn't do anything special, but in future could bundle chains of references (i.e. threads).
* These are primarily intended for handling replies (and in future threads).
*
* m.thread - lets you define an event which is a thread reply to an existing event.
* When aggregated, returns the most thread event
*/
interface RelationService {
@@ -62,8 +66,8 @@ interface RelationService {
* @param targetEventId the id of the event being reacted
* @param reaction the reaction (preferably emoji)
*/
fun undoReaction(targetEventId: String,
reaction: String): Cancelable
suspend fun undoReaction(targetEventId: String,
reaction: String): Cancelable
/**
* Edit a poll.
@@ -118,10 +122,15 @@ interface RelationService {
* @param eventReplied the event referenced by the reply
* @param replyText the reply text
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
* @param showInThread If true, relation will be added to the reply in order to be visible from within threads
* @param rootThreadEventId If show in thread is true then we need the rootThreadEventId to generate the relation
*/
fun replyToMessage(eventReplied: TimelineEvent,
replyText: CharSequence,
autoMarkdown: Boolean = false): Cancelable?
autoMarkdown: Boolean = false,
showInThread: Boolean = false,
rootThreadEventId: String? = null
): Cancelable?
/**
* Get the current EventAnnotationsSummary
@@ -136,4 +145,31 @@ interface RelationService {
* @return the LiveData of EventAnnotationsSummary
*/
fun getEventAnnotationsSummaryLive(eventId: String): LiveData<Optional<EventAnnotationsSummary>>
/**
* Creates a thread reply for an existing timeline event
* The replyInThreadText can be a Spannable and contains special spans (MatrixItemSpan) that will be translated
* by the sdk into pills.
* @param rootThreadEventId the root thread eventId
* @param replyInThreadText the reply text
* @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE
* @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
* @param eventReplied the event referenced by the reply within a thread
*/
fun replyInThread(rootThreadEventId: String,
replyInThreadText: CharSequence,
msgType: String = MessageType.MSGTYPE_TEXT,
autoMarkdown: Boolean = false,
formattedText: String? = null,
eventReplied: TimelineEvent? = null): Cancelable?
/**
* Get all the thread replies for the specified rootThreadEventId
* The return list will contain the original root thread event and all the thread replies to that event
* Note: We will use a large limit value in order to avoid using pagination until it would be 100% ready
* from the backend
* @param rootThreadEventId the root thread eventId
*/
suspend fun fetchThreadTimeline(rootThreadEventId: String): Boolean
}

View File

@@ -21,5 +21,8 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ReplyToContent(
@Json(name = "event_id") val eventId: String? = null
@Json(name = "event_id") val eventId: String? = null,
@Json(name = "render_in") val renderIn: List<String>? = null
)
fun ReplyToContent.shouldRenderInThread(): Boolean = renderIn?.contains("m.thread") == true

View File

@@ -64,7 +64,7 @@ interface SendService {
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
* @return a [Cancelable]
*/
fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean): Cancelable
fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean, rootThreadEventId: String? = null): Cancelable
/**
* Method to send a media asynchronously.
@@ -72,11 +72,13 @@ interface SendService {
* @param compressBeforeSending set to true to compress images before sending them
* @param roomIds set of roomIds to where the media will be sent. The current roomId will be add to this set if not present.
* It can be useful to send media to multiple room. It's safe to include the current roomId in this set
* @param rootThreadEventId when this param is not null, the Media will be sent in this specific thread
* @return a [Cancelable]
*/
fun sendMedia(attachment: ContentAttachmentData,
compressBeforeSending: Boolean,
roomIds: Set<String>): Cancelable
roomIds: Set<String>,
rootThreadEventId: String? = null): Cancelable
/**
* Method to send a list of media asynchronously.
@@ -84,11 +86,13 @@ interface SendService {
* @param compressBeforeSending set to true to compress images before sending them
* @param roomIds set of roomIds to where the media will be sent. The current roomId will be add to this set if not present.
* It can be useful to send media to multiple room. It's safe to include the current roomId in this set
* @param rootThreadEventId when this param is not null, all the Media will be sent in this specific thread
* @return a [Cancelable]
*/
fun sendMedias(attachments: List<ContentAttachmentData>,
compressBeforeSending: Boolean,
roomIds: Set<String>): Cancelable
roomIds: Set<String>,
rootThreadEventId: String? = null): Cancelable
/**
* Send a poll to the room.

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.room.threads
import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
/**
* This interface defines methods to interact with threads related features.
* It's implemented at the room level within the main timeline.
*/
interface ThreadsService {
/**
* Returns a [LiveData] list of all the thread root TimelineEvents that exists at the room level
*/
fun getAllThreadsLive(): LiveData<List<TimelineEvent>>
/**
* Returns a list of all the thread root TimelineEvents that exists at the room level
*/
fun getAllThreads(): List<TimelineEvent>
/**
* Returns a [LiveData] list of all the marked unread threads that exists at the room level
*/
fun getMarkedThreadNotificationsLive(): LiveData<List<TimelineEvent>>
/**
* Returns a list of all the marked unread threads that exists at the room level
*/
fun getMarkedThreadNotifications(): List<TimelineEvent>
/**
* Returns whether or not the current user is participating in the thread
* @param rootThreadEventId the eventId of the current thread
*/
fun isUserParticipatingInThread(rootThreadEventId: String): Boolean
/**
* Enhance the provided root thread TimelineEvent [List] by adding the latest
* message edition for that thread
* @return the enhanced [List] with edited updates
*/
fun mapEventsWithEdition(threads: List<TimelineEvent>): List<TimelineEvent>
/**
* Marks the current thread as read in local DB.
* note: read receipts within threads are not yet supported with the API
* @param rootThreadEventId the root eventId of the current thread
*/
suspend fun markThreadAsRead(rootThreadEventId: String)
}

View File

@@ -43,7 +43,7 @@ interface Timeline {
/**
* This must be called before any other method after creating the timeline. It ensures the underlying database is open
*/
fun start()
fun start(rootThreadEventId: String? = null)
/**
* This must be called when you don't need the timeline. It ensures the underlying database get closed.

View File

@@ -22,7 +22,9 @@ import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.getRelationContent
import org.matrix.android.sdk.api.session.events.model.isEdition
import org.matrix.android.sdk.api.session.events.model.isPoll
import org.matrix.android.sdk.api.session.events.model.isReply
import org.matrix.android.sdk.api.session.events.model.isSticker
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
@@ -149,6 +151,13 @@ fun TimelineEvent.isEdition(): Boolean {
return root.isEdition()
}
fun TimelineEvent.isPoll(): Boolean =
root.isPoll()
fun TimelineEvent.isSticker(): Boolean {
return root.isSticker()
}
/**
* Get the latest message body, after a possible edition, stripping the reply prefix if necessary
*/

View File

@@ -27,5 +27,14 @@ data class TimelineSettings(
/**
* If true, will build read receipts for each event.
*/
val buildReadReceipts: Boolean = true
)
val buildReadReceipts: Boolean = true,
/**
* The root thread eventId if this is a thread timeline, or null if this is NOT a thread timeline
*/
val rootThreadEventId: String? = null) {
/**
* Returns true if this is a thread timeline or false otherwise
*/
fun isThreadTimeline() = rootThreadEventId != null
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.threads
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
/**
* This class contains all the details needed for threads.
* Is is mainly used from within an Event.
*/
data class ThreadDetails(
val isRootThread: Boolean = false,
val numberOfThreads: Int = 0,
val threadSummarySenderInfo: SenderInfo? = null,
val threadSummaryLatestTextMessage: String? = null,
val lastMessageTimestamp: Long? = null,
var threadNotificationState: ThreadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE,
val isThread: Boolean = false,
val lastRootThreadEdition: String? = null
)

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.threads
/**
* This class defines the state of a thread notification badge
*/
data class ThreadNotificationBadgeState(
val numberOfLocalUnreadThreads: Int = 0,
val isUserMentioned: Boolean = false
)

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.threads
/**
* This class defines the state of a thread notification
*/
enum class ThreadNotificationState {
// There are no new message
NO_NEW_MESSAGE,
// There is at least one new message
NEW_MESSAGE,
// The is at least one new message that should be highlighted
// ex. "Hello @aris.kotsomitopoulos"
NEW_HIGHLIGHTED_MESSAGE;
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.threads
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
/**
* This class contains a thread TimelineEvent along with a boolean that
* determines if the current user has participated in that event
*/
data class ThreadTimelineEvent(
val timelineEvent: TimelineEvent,
val isParticipating: Boolean
)

View File

@@ -35,7 +35,7 @@ internal class MXOutboundSessionInfo(
val sessionLifetime = System.currentTimeMillis() - creationTime
if (useCount >= rotationPeriodMsgs || sessionLifetime >= rotationPeriodMs) {
Timber.v("## needsRotation() : Rotating megolm session after " + useCount + ", " + sessionLifetime + "ms")
Timber.v("## needsRotation() : Rotating megolm session after $useCount, ${sessionLifetime}ms")
needsRotation = true
}

View File

@@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
import org.matrix.android.sdk.api.session.room.model.VersioningState
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent
import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields
@@ -56,7 +57,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
) : RealmMigration {
companion object {
const val SESSION_STORE_SCHEMA_VERSION = 22L
const val SESSION_STORE_SCHEMA_VERSION = 24L
}
/**
@@ -91,6 +92,8 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion <= 19) migrateTo20(realm)
if (oldVersion <= 20) migrateTo21(realm)
if (oldVersion <= 21) migrateTo22(realm)
if (oldVersion <= 22) migrateTo23(realm)
if (oldVersion <= 23) migrateTo24(realm)
}
private fun migrateTo1(realm: DynamicRealm) {
@@ -462,4 +465,28 @@ internal class RealmSessionStoreMigration @Inject constructor(
realm.deleteAll()
}
}
private fun migrateTo23(realm: DynamicRealm) {
Timber.d("Step 22 -> 23")
val eventEntity = realm.schema.get("TimelineEventEntity") ?: return
realm.schema.get("EventEntity")
?.addField(EventEntityFields.IS_ROOT_THREAD, Boolean::class.java, FieldAttribute.INDEXED)
?.addField(EventEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
?.addField(EventEntityFields.NUMBER_OF_THREADS, Int::class.java)
?.addField(EventEntityFields.THREAD_NOTIFICATION_STATE_STR, String::class.java)
?.transform {
it.setString(EventEntityFields.THREAD_NOTIFICATION_STATE_STR, ThreadNotificationState.NO_NEW_MESSAGE.name)
}
?.addRealmObjectField(EventEntityFields.THREAD_SUMMARY_LATEST_MESSAGE.`$`, eventEntity)
}
private fun migrateTo24(realm: DynamicRealm) {
Timber.d("Step 23 -> 24")
realm.schema.get("PreviewUrlCacheEntity")
?.addField(PreviewUrlCacheEntityFields.IMAGE_WIDTH, Int::class.java)
?.setNullable(PreviewUrlCacheEntityFields.IMAGE_WIDTH, true)
?.addField(PreviewUrlCacheEntityFields.IMAGE_HEIGHT, Int::class.java)
?.setNullable(PreviewUrlCacheEntityFields.IMAGE_HEIGHT, true)
}
}

View File

@@ -34,6 +34,7 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.query.find
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.whereRoomId
import org.matrix.android.sdk.internal.extensions.assertIsManaged
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
import timber.log.Timber
@@ -81,7 +82,7 @@ internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity,
internal fun ChunkEntity.addTimelineEvent(roomId: String,
eventEntity: EventEntity,
direction: PaginationDirection,
roomMemberContentsByUser: Map<String, RoomMemberContent?>) {
roomMemberContentsByUser: Map<String, RoomMemberContent?>? = null) {
val eventId = eventEntity.eventId
if (timelineEvents.find(eventId) != null) {
return
@@ -101,7 +102,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
?.also { it.cleanUp(eventEntity.sender) }
this.readReceipts = readReceiptsSummaryEntity
this.displayIndex = displayIndex
val roomMemberContent = roomMemberContentsByUser[senderId]
val roomMemberContent = roomMemberContentsByUser?.get(senderId)
this.senderAvatar = roomMemberContent?.avatarUrl
this.senderName = roomMemberContent?.displayName
isUniqueDisplayName = if (roomMemberContent?.displayName != null) {
@@ -157,9 +158,21 @@ private fun ChunkEntity.addTimelineEventFromMerge(realm: Realm, timelineEventEnt
this.senderName = timelineEventEntity.senderName
this.isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName
}
handleThreadSummary(realm, eventId, copied)
timelineEvents.add(copied)
}
/**
* Upon copy of the timeline events we should update the latestMessage TimelineEventEntity with the new one
*/
private fun handleThreadSummary(realm: Realm, oldEventId: String, newTimelineEventEntity: TimelineEventEntity) {
EventEntity
.whereRoomId(realm, newTimelineEventEntity.roomId)
.equalTo(EventEntityFields.IS_ROOT_THREAD, true)
.equalTo(EventEntityFields.THREAD_SUMMARY_LATEST_MESSAGE.EVENT_ID, oldEventId)
.findFirst()?.threadSummaryLatestMessage = newTimelineEventEntity
}
private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventEntity, senderId: String): ReadReceiptsSummaryEntity {
val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventEntity.eventId).findFirst()
?: realm.createObject<ReadReceiptsSummaryEntity>(eventEntity.eventId).apply {

View File

@@ -0,0 +1,321 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.database.helper
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.Sort
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.ChunkEntity
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.query.find
import org.matrix.android.sdk.internal.database.query.findIncludingEvent
import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.whereRoomId
private typealias ThreadSummary = Pair<Int, TimelineEventEntity>?
/**
* Finds the root thread event and update it with the latest message summary along with the number
* of threads included. If there is no root thread event no action is done
*/
internal fun Map<String, EventEntity>.updateThreadSummaryIfNeeded(
roomId: String,
realm: Realm, currentUserId: String,
chunkEntity: ChunkEntity? = null,
shouldUpdateNotifications: Boolean = true) {
for ((rootThreadEventId, eventEntity) in this) {
eventEntity.threadSummaryInThread(eventEntity.realm, rootThreadEventId, chunkEntity)?.let { threadSummary ->
val numberOfMessages = threadSummary.first
val latestEventInThread = threadSummary.second
// If this is a thread message, find its root event if exists
val rootThreadEvent = if (eventEntity.isThread()) eventEntity.findRootThreadEvent() else eventEntity
rootThreadEvent?.markEventAsRoot(
threadsCounted = numberOfMessages,
latestMessageTimelineEventEntity = latestEventInThread
)
}
}
if (shouldUpdateNotifications) {
updateNotificationsNew(roomId, realm, currentUserId)
}
}
/**
* Finds the root event of the the current thread event message.
* Returns the EventEntity or null if the root event do not exist
*/
internal fun EventEntity.findRootThreadEvent(): EventEntity? =
rootThreadEventId?.let {
EventEntity
.where(realm, it)
.findFirst()
}
/**
* Mark or update the current event a root thread event
*/
internal fun EventEntity.markEventAsRoot(
threadsCounted: Int,
latestMessageTimelineEventEntity: TimelineEventEntity?) {
isRootThread = true
numberOfThreads = threadsCounted
threadSummaryLatestMessage = latestMessageTimelineEventEntity
}
/**
* Count the number of threads for the provided root thread eventId, and finds the latest event message
* @param rootThreadEventId The root eventId that will find the number of threads
* @return A ThreadSummary containing the counted threads and the latest event message
*/
internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: String, chunkEntity: ChunkEntity?): ThreadSummary {
// Number of messages
val messages = TimelineEventEntity
.whereRoomId(realm, roomId = roomId)
.equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId)
.count()
.toInt()
if (messages <= 0) return null
// Find latest thread event, we know it exists
var chunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) ?: chunkEntity ?: return null
var result: TimelineEventEntity? = null
// Iterate the chunk until we find our latest event
while (result == null) {
result = findLatestSortedChunkEvent(chunk, rootThreadEventId)
chunk = ChunkEntity.find(realm, roomId, nextToken = chunk.prevToken) ?: break
}
if (result == null && chunkEntity != null) {
// Find latest event from our current chunk
result = findLatestSortedChunkEvent(chunkEntity, rootThreadEventId)
} else if (result != null && chunkEntity != null) {
val currentChunkLatestEvent = findLatestSortedChunkEvent(chunkEntity, rootThreadEventId)
result = findMostRecentEvent(result, currentChunkLatestEvent)
}
result ?: return null
return ThreadSummary(messages, result)
}
/**
* Lets compare them in case user is moving forward in the timeline and we cannot know the
* exact chunk sequence while currentChunk is not yet committed in the DB
*/
private fun findMostRecentEvent(result: TimelineEventEntity, currentChunkLatestEvent: TimelineEventEntity?): TimelineEventEntity {
currentChunkLatestEvent ?: return result
val currentChunkEventTimestamp = currentChunkLatestEvent.root?.originServerTs ?: return result
val resultTimestamp = result.root?.originServerTs ?: return result
if (currentChunkEventTimestamp > resultTimestamp) {
return currentChunkLatestEvent
}
return result
}
/**
* Find the latest event of the current chunk
*/
private fun findLatestSortedChunkEvent(chunk: ChunkEntity, rootThreadEventId: String): TimelineEventEntity? =
chunk.timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)?.firstOrNull {
it.root?.rootThreadEventId == rootThreadEventId
}
/**
* Find all TimelineEventEntity that are root threads for the specified room
* @param roomId The room that all stored root threads will be returned
*/
internal fun TimelineEventEntity.Companion.findAllThreadsForRoomId(realm: Realm, roomId: String): RealmQuery<TimelineEventEntity> =
TimelineEventEntity
.whereRoomId(realm, roomId = roomId)
.equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD, true)
.sort("${TimelineEventEntityFields.ROOT.THREAD_SUMMARY_LATEST_MESSAGE}.${TimelineEventEntityFields.ROOT.ORIGIN_SERVER_TS}", Sort.DESCENDING)
/**
* Map each root thread TimelineEvent with the equivalent decrypted text edition/replacement
*/
internal fun List<TimelineEvent>.mapEventsWithEdition(realm: Realm, roomId: String): List<TimelineEvent> =
this.map {
EventAnnotationsSummaryEntity
.where(realm, roomId, eventId = it.eventId)
.findFirst()
?.editSummary
?.editions
?.lastOrNull()
?.eventId
?.let { editedEventId ->
TimelineEventEntity.where(realm, roomId, eventId = editedEventId).findFirst()?.let { editedEvent ->
it.root.threadDetails = it.root.threadDetails?.copy(lastRootThreadEdition = editedEvent.root?.asDomain()?.getDecryptedTextSummary()
?: "(edited)")
it
} ?: it
} ?: it
}
/**
* Returns a list of all the marked unread threads that exists for the specified room
* @param roomId The roomId that the user is currently in
*/
internal fun TimelineEventEntity.Companion.findAllLocalThreadNotificationsForRoomId(realm: Realm, roomId: String): RealmQuery<TimelineEventEntity> =
TimelineEventEntity
.whereRoomId(realm, roomId = roomId)
.equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD, true)
.beginGroup()
.equalTo(TimelineEventEntityFields.ROOT.THREAD_NOTIFICATION_STATE_STR, ThreadNotificationState.NEW_MESSAGE.name)
.or()
.equalTo(TimelineEventEntityFields.ROOT.THREAD_NOTIFICATION_STATE_STR, ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE.name)
.endGroup()
/**
* Returns whether or not the given user is participating in a current thread
* @param roomId the room that the thread exists
* @param rootThreadEventId the thread that the search will be done
* @param senderId the user that will try to find participation
*/
internal fun TimelineEventEntity.Companion.isUserParticipatingInThread(realm: Realm, roomId: String, rootThreadEventId: String, senderId: String): Boolean =
TimelineEventEntity
.whereRoomId(realm, roomId = roomId)
.equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId)
.equalTo(TimelineEventEntityFields.ROOT.SENDER, senderId)
.findFirst()
?.let { true }
?: false
/**
* Returns whether or not the given user is mentioned in a current thread
* @param roomId the room that the thread exists
* @param rootThreadEventId the thread that the search will be done
* @param userId the user that will try to find if there is a mention
*/
internal fun TimelineEventEntity.Companion.isUserMentionedInThread(realm: Realm, roomId: String, rootThreadEventId: String, userId: String): Boolean =
TimelineEventEntity
.whereRoomId(realm, roomId = roomId)
.equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId)
.equalTo(TimelineEventEntityFields.ROOT.SENDER, userId)
.findAll()
.firstOrNull { isUserMentioned(userId, it) }
?.let { true }
?: false
/**
* Find the read receipt for the current user
*/
internal fun findMyReadReceipt(realm: Realm, roomId: String, userId: String): String? =
ReadReceiptEntity.where(realm, roomId = roomId, userId = userId)
.findFirst()
?.eventId
/**
* Returns whether or not the user is mentioned in the event
*/
internal fun isUserMentioned(currentUserId: String, timelineEventEntity: TimelineEventEntity?): Boolean {
return timelineEventEntity?.root?.asDomain()?.isUserMentioned(currentUserId) == true
}
/**
* Update badge notifications. Count the number of new thread events after the latest
* read receipt and aggregate. This function will find and notify new thread events
* that the user is either mentioned, or the user had participated in.
* Important: If the root thread event is not fetched notification will not work
* Important: It will work only with the latest chunk, while read marker will be changed
* immediately so we should not display wrong notifications
*/
internal fun updateNotificationsNew(roomId: String, realm: Realm, currentUserId: String) {
val readReceipt = findMyReadReceipt(realm, roomId, currentUserId) ?: return
val readReceiptChunk = ChunkEntity
.findIncludingEvent(realm, readReceipt) ?: return
val readReceiptChunkTimelineEvents = readReceiptChunk
.timelineEvents
.where()
.equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
.findAll() ?: return
val readReceiptChunkPosition = readReceiptChunkTimelineEvents.indexOfFirst { it.eventId == readReceipt }
if (readReceiptChunkPosition == -1) return
if (readReceiptChunkPosition < readReceiptChunkTimelineEvents.lastIndex) {
// If the read receipt is found inside the chunk
val threadEventsAfterReadReceipt = readReceiptChunkTimelineEvents
.slice(readReceiptChunkPosition..readReceiptChunkTimelineEvents.lastIndex)
.filter { it.root?.isThread() == true }
// In order for the below code to work for old events, we should save the previous read receipt
// and then continue with the chunk search for that read receipt
/*
val newThreadEventsList = arrayListOf<TimelineEventEntity>()
newThreadEventsList.addAll(threadEventsAfterReadReceipt)
// got from latest chunk all new threads, lets move to the others
var nextChunk = ChunkEntity
.find(realm = realm, roomId = roomId, nextToken = readReceiptChunk.nextToken)
.takeIf { readReceiptChunk.nextToken != null }
while (nextChunk != null) {
newThreadEventsList.addAll(nextChunk.timelineEvents
.filter { it.root?.isThread() == true })
nextChunk = ChunkEntity
.find(realm = realm, roomId = roomId, nextToken = nextChunk.nextToken)
.takeIf { readReceiptChunk.nextToken != null }
}*/
// Find if the user is mentioned in those events
val userMentionsList = threadEventsAfterReadReceipt
.filter {
isUserMentioned(currentUserId = currentUserId, it)
}.map {
it.root?.rootThreadEventId
}
// Find the root events in the new thread events
val rootThreads = threadEventsAfterReadReceipt.distinctBy { it.root?.rootThreadEventId }.mapNotNull { it.root?.rootThreadEventId }
// Update root thread events only if the user have participated in
rootThreads.forEach { eventId ->
val isUserParticipating = TimelineEventEntity.isUserParticipatingInThread(
realm = realm,
roomId = roomId,
rootThreadEventId = eventId,
senderId = currentUserId)
val rootThreadEventEntity = EventEntity.where(realm, eventId).findFirst()
if (isUserParticipating) {
rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_MESSAGE
}
if (userMentionsList.contains(eventId)) {
rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE
}
}
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.database.lightweight
import android.content.Context
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import javax.inject.Inject
/**
* The purpose of this class is to provide an alternative and lightweight way to store settings/data
* on the sdi without using the database. This should be used just for sdk/user preferences and
* not for large data sets
*/
class LightweightSettingsStorage @Inject constructor(context: Context) {
private val sdkDefaultPrefs = PreferenceManager.getDefaultSharedPreferences(context.applicationContext)
fun setThreadMessagesEnabled(enabled: Boolean) {
sdkDefaultPrefs.edit {
putBoolean(MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED, enabled)
}
}
fun areThreadMessagesEnabled(): Boolean {
return sdkDefaultPrefs.getBoolean(MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED, false)
}
companion object {
const val MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED = "MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED"
}
}

View File

@@ -21,7 +21,11 @@ import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.UnsignedData
import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
import org.matrix.android.sdk.api.session.threads.ThreadDetails
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.di.MoshiProvider
@@ -51,6 +55,10 @@ internal object EventMapper {
}
eventEntity.decryptionErrorReason = event.mCryptoErrorReason
eventEntity.decryptionErrorCode = event.mCryptoError?.name
eventEntity.isRootThread = event.threadDetails?.isRootThread ?: false
eventEntity.rootThreadEventId = event.getRootThreadEventId()
eventEntity.numberOfThreads = event.threadDetails?.numberOfThreads ?: 0
eventEntity.threadNotificationState = event.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE
return eventEntity
}
@@ -93,6 +101,23 @@ internal object EventMapper {
MXCryptoError.ErrorType.valueOf(errorCode)
}
it.mCryptoErrorReason = eventEntity.decryptionErrorReason
it.threadDetails = ThreadDetails(
isRootThread = eventEntity.isRootThread,
isThread = if (it.threadDetails?.isThread == true) true else eventEntity.isThread(),
numberOfThreads = eventEntity.numberOfThreads,
threadSummarySenderInfo = eventEntity.threadSummaryLatestMessage?.let { timelineEventEntity ->
SenderInfo(
userId = timelineEventEntity.root?.sender ?: "",
displayName = timelineEventEntity.senderName,
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
avatarUrl = timelineEventEntity.senderAvatar
)
},
threadNotificationState = eventEntity.threadNotificationState,
threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedTextSummary(),
lastMessageTimestamp = eventEntity.threadSummaryLatestMessage?.root?.originServerTs
)
}
}
}
@@ -101,9 +126,15 @@ internal fun EventEntity.asDomain(castJsonNumbers: Boolean = false): Event {
return EventMapper.map(this, castJsonNumbers)
}
internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long?): EventEntity {
internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long?, contentToInject: String? = null): EventEntity {
return EventMapper.map(this, roomId).apply {
this.sendState = sendState
this.ageLocalTs = ageLocalTs
contentToInject?.let {
this.content = it
if (this.type == EventType.STICKER) {
this.type = EventType.MESSAGE
}
}
}
}

View File

@@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.database.model
import io.realm.RealmObject
import io.realm.annotations.Index
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
import org.matrix.android.sdk.internal.di.MoshiProvider
@@ -40,7 +40,12 @@ internal open class EventEntity(@Index var eventId: String = "",
var unsignedData: String? = null,
var redacts: String? = null,
var decryptionResultJson: String? = null,
var ageLocalTs: Long? = null
var ageLocalTs: Long? = null,
// Thread related, no need to create a new Entity for performance
@Index var isRootThread: Boolean = false,
@Index var rootThreadEventId: String? = null,
var numberOfThreads: Int = 0,
var threadSummaryLatestMessage: TimelineEventEntity? = null
) : RealmObject() {
private var sendStateStr: String = SendState.UNKNOWN.name
@@ -53,6 +58,15 @@ internal open class EventEntity(@Index var eventId: String = "",
sendStateStr = value.name
}
private var threadNotificationStateStr: String = ThreadNotificationState.NO_NEW_MESSAGE.name
var threadNotificationState: ThreadNotificationState
get() {
return ThreadNotificationState.valueOf(threadNotificationStateStr)
}
set(value) {
threadNotificationStateStr = value.name
}
var decryptionErrorCode: String? = null
set(value) {
if (value != field) field = value
@@ -65,10 +79,10 @@ internal open class EventEntity(@Index var eventId: String = "",
companion object
fun setDecryptionResult(result: MXEventDecryptionResult, clearEvent: JsonDict? = null) {
fun setDecryptionResult(result: MXEventDecryptionResult) {
assertIsManaged()
val decryptionResult = OlmDecryptionResult(
payload = clearEvent ?: result.clearEvent,
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
@@ -84,4 +98,6 @@ internal open class EventEntity(@Index var eventId: String = "",
.findFirst()
?.canBeProcessed = true
}
fun isThread(): Boolean = rootThreadEventId != null
}

View File

@@ -28,7 +28,8 @@ internal open class PreviewUrlCacheEntity(
var title: String? = null,
var description: String? = null,
var mxcUrl: String? = null,
var imageWidth: Int? = null,
var imageHeight: Int? = null,
var lastUpdatedTimestamp: Long = 0L
) : RealmObject() {

View File

@@ -49,6 +49,11 @@ internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQu
.equalTo(EventEntityFields.EVENT_ID, eventId)
}
internal fun EventEntity.Companion.whereRoomId(realm: Realm, roomId: String): RealmQuery<EventEntity> {
return realm.where<EventEntity>()
.equalTo(EventEntityFields.ROOM_ID, roomId)
}
internal fun EventEntity.Companion.where(realm: Realm, eventIds: List<String>): RealmQuery<EventEntity> {
return realm.where<EventEntity>()
.`in`(EventEntityFields.EVENT_ID, eventIds.toTypedArray())
@@ -85,3 +90,8 @@ internal fun RealmList<EventEntity>.find(eventId: String): EventEntity? {
internal fun RealmList<EventEntity>.fastContains(eventId: String): Boolean {
return this.find(eventId) != null
}
internal fun EventEntity.Companion.whereRootThreadEventId(realm: Realm, rootThreadEventId: String): RealmQuery<EventEntity> {
return realm.where<EventEntity>()
.equalTo(EventEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId)
}

View File

@@ -34,27 +34,29 @@ internal fun isEventRead(realmConfiguration: RealmConfiguration,
if (LocalEcho.isLocalEchoId(eventId)) {
return true
}
// If we don't know if the event has been read, we assume it's not
var isEventRead = false
Realm.getInstance(realmConfiguration).use { realm ->
val latestEvent = TimelineEventEntity.latestEvent(realm, roomId, true)
// If latest event is from you we are sure the event is read
if (latestEvent?.root?.sender == userId) {
return true
}
return Realm.getInstance(realmConfiguration).use { realm ->
val eventToCheck = TimelineEventEntity.where(realm, roomId, eventId).findFirst()
isEventRead = when {
eventToCheck == null -> false
eventToCheck.root?.sender == userId -> true
else -> {
val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() ?: return@use
val readReceiptEvent = TimelineEventEntity.where(realm, roomId, readReceipt.eventId).findFirst() ?: return@use
readReceiptEvent.isMoreRecentThan(eventToCheck)
}
when {
// The event doesn't exist locally, let's assume it hasn't been read
eventToCheck == null -> false
eventToCheck.root?.sender == userId -> true
// If new event exists and the latest event is from ourselves we can infer the event is read
latestEventIsFromSelf(realm, roomId, userId) -> true
eventToCheck.isBeforeLatestReadReceipt(realm, roomId, userId) -> true
else -> false
}
}
return isEventRead
}
private fun latestEventIsFromSelf(realm: Realm, roomId: String, userId: String) = TimelineEventEntity.latestEvent(realm, roomId, true)
?.root?.sender == userId
private fun TimelineEventEntity.isBeforeLatestReadReceipt(realm: Realm, roomId: String, userId: String): Boolean {
return ReadReceiptEntity.where(realm, roomId, userId).findFirst()?.let { readReceipt ->
val readReceiptEvent = TimelineEventEntity.where(realm, roomId, readReceipt.eventId).findFirst()
readReceiptEvent?.isMoreRecentThan(this)
} ?: false
}
/**

View File

@@ -59,6 +59,7 @@ internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm,
filters: TimelineEventFilters = TimelineEventFilters()): TimelineEventEntity? {
val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: return null
val sendingTimelineEvents = roomEntity.sendingTimelineEvents.where().filterEvents(filters)
val liveEvents = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)?.timelineEvents?.where()?.filterEvents(filters)
val query = if (includesSending && sendingTimelineEvents.findAll().isNotEmpty()) {
sendingTimelineEvents
@@ -100,6 +101,7 @@ internal fun RealmQuery<TimelineEventEntity>.filterEvents(filters: TimelineEvent
if (filters.filterRedacted) {
not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED)
}
return this
}

View File

@@ -66,7 +66,7 @@ internal class ThumbnailExtractor @Inject constructor(
thumbnail.recycle()
outputStream.reset()
} ?: run {
Timber.e("Cannot extract video thumbnail at %s", attachment.queryUri.toString())
Timber.e("Cannot extract video thumbnail at ${attachment.queryUri}")
}
} catch (e: Exception) {
Timber.e(e, "Cannot extract video thumbnail")

View File

@@ -48,6 +48,16 @@ data class RoomEventFilter(
* a wildcard to match any sequence of characters.
*/
@Json(name = "types") val types: List<String>? = null,
/**
* A list of relation types which must be exist pointing to the event being filtered.
* If this list is absent then no filtering is done on relation types.
*/
@Json(name = "relation_types") val relationTypes: List<String>? = null,
/**
* A list of senders of relations which must exist pointing to the event being filtered.
* If this list is absent then no filtering is done on relation types.
*/
@Json(name = "relation_senders") val relationSenders: List<String>? = null,
/**
* A list of room IDs to include. If this list is absent then all rooms are included.
*/

View File

@@ -48,8 +48,8 @@ internal class DefaultGetPreviewUrlTask @Inject constructor(
override suspend fun execute(params: GetPreviewUrlTask.Params): PreviewUrlData {
return when (params.cacheStrategy) {
CacheStrategy.NoCache -> doRequest(params.url, params.timestamp)
is CacheStrategy.TtlCache -> doRequestWithCache(
CacheStrategy.NoCache -> doRequest(params.url, params.timestamp)
is CacheStrategy.TtlCache -> doRequestWithCache(
params.url,
params.timestamp,
params.cacheStrategy.validityDurationInMillis,
@@ -77,7 +77,9 @@ internal class DefaultGetPreviewUrlTask @Inject constructor(
siteName = (get("og:site_name") as? String)?.unescapeHtml(),
title = (get("og:title") as? String)?.unescapeHtml(),
description = (get("og:description") as? String)?.unescapeHtml(),
mxcUrl = get("og:image") as? String
mxcUrl = get("og:image") as? String,
imageHeight = (get("og:image:height") as? Double)?.toInt(),
imageWidth = (get("og:image:width") as? Double)?.toInt(),
)
}
@@ -114,7 +116,8 @@ internal class DefaultGetPreviewUrlTask @Inject constructor(
previewUrlCacheEntity.title = data.title
previewUrlCacheEntity.description = data.description
previewUrlCacheEntity.mxcUrl = data.mxcUrl
previewUrlCacheEntity.imageHeight = data.imageHeight
previewUrlCacheEntity.imageWidth = data.imageWidth
previewUrlCacheEntity.lastUpdatedTimestamp = Date().time
}

View File

@@ -27,5 +27,7 @@ internal fun PreviewUrlCacheEntity.toDomain() = PreviewUrlData(
siteName = siteName,
title = title,
description = description,
mxcUrl = mxcUrl
mxcUrl = mxcUrl,
imageWidth = imageWidth,
imageHeight = imageHeight
)

View File

@@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.room.send.DraftService
import org.matrix.android.sdk.api.session.room.send.SendService
import org.matrix.android.sdk.api.session.room.state.StateService
import org.matrix.android.sdk.api.session.room.tags.TagsService
import org.matrix.android.sdk.api.session.room.threads.ThreadsService
import org.matrix.android.sdk.api.session.room.timeline.TimelineService
import org.matrix.android.sdk.api.session.room.typing.TypingService
import org.matrix.android.sdk.api.session.room.uploads.UploadsService
@@ -54,6 +55,7 @@ import java.security.InvalidParameterException
internal class DefaultRoom(override val roomId: String,
private val roomSummaryDataSource: RoomSummaryDataSource,
private val timelineService: TimelineService,
private val threadsService: ThreadsService,
private val sendService: SendService,
private val draftService: DraftService,
private val stateService: StateService,
@@ -77,6 +79,7 @@ internal class DefaultRoom(override val roomId: String,
) :
Room,
TimelineService by timelineService,
ThreadsService by threadsService,
SendService by sendService,
DraftService by draftService,
StateService by stateService,

View File

@@ -44,6 +44,7 @@ import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
import org.matrix.android.sdk.internal.crypto.verification.toState
import org.matrix.android.sdk.internal.database.helper.findRootThreadEvent
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
import org.matrix.android.sdk.internal.database.mapper.EventMapper
import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity
@@ -332,6 +333,29 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
)
}
}
if (!isLocalEcho) {
val replaceEvent = TimelineEventEntity.where(realm, roomId, eventId).findFirst()
handleThreadSummaryEdition(editedEvent, replaceEvent, existingSummary?.editions)
}
}
/**
* Check if the edition is on the latest thread event, and update it accordingly
*/
private fun handleThreadSummaryEdition(editedEvent: EventEntity?,
replaceEvent: TimelineEventEntity?,
editions: List<EditionOfEvent>?) {
replaceEvent ?: return
editedEvent ?: return
editedEvent.findRootThreadEvent()?.apply {
val threadSummaryEventId = threadSummaryLatestMessage?.eventId
if (editedEvent.eventId == threadSummaryEventId || editions?.any { it.eventId == threadSummaryEventId } == true) {
// The edition is for the latest event or for any event replaced, this is to handle multiple
// edits of the same latest event
threadSummaryLatestMessage = replaceEvent
}
}
}
private fun handleResponse(realm: Realm,

View File

@@ -226,7 +226,8 @@ internal interface RoomAPI {
suspend fun getRelations(@Path("roomId") roomId: String,
@Path("eventId") eventId: String,
@Path("relationType") relationType: String,
@Path("eventType") eventType: String
@Path("eventType") eventType: String,
@Query("limit") limit: Int? = null
): RelationsResponse
/**

View File

@@ -35,6 +35,7 @@ import org.matrix.android.sdk.internal.session.room.state.DefaultStateService
import org.matrix.android.sdk.internal.session.room.state.SendStateTask
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
import org.matrix.android.sdk.internal.session.room.tags.DefaultTagsService
import org.matrix.android.sdk.internal.session.room.threads.DefaultThreadsService
import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimelineService
import org.matrix.android.sdk.internal.session.room.typing.DefaultTypingService
import org.matrix.android.sdk.internal.session.room.uploads.DefaultUploadsService
@@ -50,6 +51,7 @@ internal interface RoomFactory {
internal class DefaultRoomFactory @Inject constructor(private val cryptoService: CryptoService,
private val roomSummaryDataSource: RoomSummaryDataSource,
private val timelineServiceFactory: DefaultTimelineService.Factory,
private val threadsServiceFactory: DefaultThreadsService.Factory,
private val sendServiceFactory: DefaultSendService.Factory,
private val draftServiceFactory: DefaultDraftService.Factory,
private val stateServiceFactory: DefaultStateService.Factory,
@@ -76,6 +78,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
roomId = roomId,
roomSummaryDataSource = roomSummaryDataSource,
timelineService = timelineServiceFactory.create(roomId),
threadsService = threadsServiceFactory.create(roomId),
sendService = sendServiceFactory.create(roomId),
draftService = draftServiceFactory.create(roomId),
stateService = stateServiceFactory.create(roomId),

View File

@@ -77,6 +77,8 @@ import org.matrix.android.sdk.internal.session.room.relation.DefaultUpdateQuickR
import org.matrix.android.sdk.internal.session.room.relation.FetchEditHistoryTask
import org.matrix.android.sdk.internal.session.room.relation.FindReactionEventForUndoTask
import org.matrix.android.sdk.internal.session.room.relation.UpdateQuickReactionTask
import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
import org.matrix.android.sdk.internal.session.room.reporting.DefaultReportContentTask
import org.matrix.android.sdk.internal.session.room.reporting.ReportContentTask
import org.matrix.android.sdk.internal.session.room.state.DefaultSendStateTask
@@ -289,4 +291,7 @@ internal abstract class RoomModule {
@Binds
abstract fun bindGetRoomSummaryTask(task: DefaultGetRoomSummaryTask): GetRoomSummaryTask
@Binds
abstract fun bindFetchThreadTimelineTask(task: DefaultFetchThreadTimelineTask): FetchThreadTimelineTask
}

View File

@@ -83,7 +83,9 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr
// }
val modified = unsignedData.copy(redactedEvent = redactionEvent)
eventToPrune.content = ContentMapper.map(emptyMap())
// I Commented the line below, it should not be empty while we lose all the previous info about
// the redacted event
// eventToPrune.content = ContentMapper.map(emptyMap())
eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified)
eventToPrune.decryptionResultJson = null
eventToPrune.decryptionErrorCode = null

View File

@@ -21,7 +21,6 @@ import com.zhuinden.monarchy.Monarchy
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
import org.matrix.android.sdk.api.session.room.model.message.PollType
@@ -38,10 +37,9 @@ import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEnt
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.util.fetchCopyMap
import timber.log.Timber
@@ -53,10 +51,10 @@ internal class DefaultRelationService @AssistedInject constructor(
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
private val fetchEditHistoryTask: FetchEditHistoryTask,
private val fetchThreadTimelineTask: FetchThreadTimelineTask,
private val timelineEventMapper: TimelineEventMapper,
@SessionDatabase private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor) :
RelationService {
@SessionDatabase private val monarchy: Monarchy
) : RelationService {
@AssistedFactory
interface Factory {
@@ -78,39 +76,31 @@ internal class DefaultRelationService @AssistedInject constructor(
.none { it.addedByMe && it.key == reaction }) {
val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction)
.also { saveLocalEcho(it) }
return eventSenderProcessor.postEvent(event, false /* reaction are not encrypted*/)
eventSenderProcessor.postEvent(event, false /* reaction are not encrypted*/)
} else {
Timber.w("Reaction already added")
NoOpCancellable
}
}
override fun undoReaction(targetEventId: String, reaction: String): Cancelable {
override suspend fun undoReaction(targetEventId: String, reaction: String): Cancelable {
val params = FindReactionEventForUndoTask.Params(
roomId,
targetEventId,
reaction
)
// TODO We should avoid using MatrixCallback internally
val callback = object : MatrixCallback<FindReactionEventForUndoTask.Result> {
override fun onSuccess(data: FindReactionEventForUndoTask.Result) {
if (data.redactEventId == null) {
Timber.w("Cannot find reaction to undo (not yet synced?)")
// TODO?
}
data.redactEventId?.let { toRedact ->
val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null)
.also { saveLocalEcho(it) }
eventSenderProcessor.postRedaction(redactEvent, null)
}
}
val data = findReactionEventForUndoTask.executeRetry(params, Int.MAX_VALUE)
return if (data.redactEventId == null) {
Timber.w("Cannot find reaction to undo (not yet synced?)")
// TODO?
NoOpCancellable
} else {
val redactEvent = eventFactory.createRedactEvent(roomId, data.redactEventId, null)
.also { saveLocalEcho(it) }
eventSenderProcessor.postRedaction(redactEvent, null)
}
return findReactionEventForUndoTask
.configureWith(params) {
this.retryCount = Int.MAX_VALUE
this.callback = callback
}
.executeBy(taskExecutor)
}
override fun editPoll(targetEvent: TimelineEvent,
@@ -139,8 +129,20 @@ internal class DefaultRelationService @AssistedInject constructor(
return fetchEditHistoryTask.execute(FetchEditHistoryTask.Params(roomId, eventId))
}
override fun replyToMessage(eventReplied: TimelineEvent, replyText: CharSequence, autoMarkdown: Boolean): Cancelable? {
val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText, autoMarkdown)
override fun replyToMessage(
eventReplied: TimelineEvent,
replyText: CharSequence,
autoMarkdown: Boolean,
showInThread: Boolean,
rootThreadEventId: String?
): Cancelable? {
val event = eventFactory.createReplyTextEvent(
roomId = roomId,
eventReplied = eventReplied,
replyText = replyText,
autoMarkdown = autoMarkdown,
rootThreadEventId = rootThreadEventId,
showInThread = showInThread)
?.also { saveLocalEcho(it) }
?: return null
@@ -166,6 +168,47 @@ internal class DefaultRelationService @AssistedInject constructor(
}
}
override fun replyInThread(
rootThreadEventId: String,
replyInThreadText: CharSequence,
msgType: String,
autoMarkdown: Boolean,
formattedText: String?,
eventReplied: TimelineEvent?): Cancelable? {
val event = if (eventReplied != null) {
// Reply within a thread
eventFactory.createReplyTextEvent(
roomId = roomId,
eventReplied = eventReplied,
replyText = replyInThreadText,
autoMarkdown = autoMarkdown,
rootThreadEventId = rootThreadEventId,
showInThread = false
)
?.also {
saveLocalEcho(it)
}
?: return null
} else {
// Normal thread reply
eventFactory.createThreadTextEvent(
rootThreadEventId = rootThreadEventId,
roomId = roomId,
text = replyInThreadText,
msgType = msgType,
autoMarkdown = autoMarkdown,
formattedText = formattedText)
.also {
saveLocalEcho(it)
}
}
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
}
override suspend fun fetchThreadTimeline(rootThreadEventId: String): Boolean {
return fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(roomId, rootThreadEventId))
}
/**
* Saves the event in database as a local echo.
* SendState is set to UNSENT and it's added to a the sendingTimelineEvents list of the room.

View File

@@ -97,7 +97,13 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor:
val roomId = replyToEdit.roomId
if (replyToEdit.root.sendState.hasFailed()) {
// We create a new in memory event for the EventSenderProcessor but we keep the eventId of the failed event.
val editedEvent = eventFactory.createReplyTextEvent(roomId, originalTimelineEvent, newBodyText, false)?.copy(
val editedEvent = eventFactory.createReplyTextEvent(
roomId = roomId,
eventReplied = originalTimelineEvent,
replyText = newBodyText,
autoMarkdown = false,
showInThread = false
)?.copy(
eventId = replyToEdit.eventId
) ?: return NoOpCancellable
updateFailedEchoWithEvent(roomId, replyToEdit.eventId, editedEvent)

View File

@@ -0,0 +1,207 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.relation.threads
import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
import org.matrix.android.sdk.internal.database.helper.updateThreadSummaryIfNeeded
import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.mapper.toEntity
import org.matrix.android.sdk.internal.database.model.ChunkEntity
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventInsertType
import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntity
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.getOrNull
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction
import timber.log.Timber
import javax.inject.Inject
internal interface FetchThreadTimelineTask : Task<FetchThreadTimelineTask.Params, Boolean> {
data class Params(
val roomId: String,
val rootThreadEventId: String
)
}
internal class DefaultFetchThreadTimelineTask @Inject constructor(
private val roomAPI: RoomAPI,
private val globalErrorReceiver: GlobalErrorReceiver,
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
@SessionDatabase private val monarchy: Monarchy,
@UserId private val userId: String,
private val cryptoService: DefaultCryptoService
) : FetchThreadTimelineTask {
override suspend fun execute(params: FetchThreadTimelineTask.Params): Boolean {
val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId)
val response = executeRequest(globalErrorReceiver) {
roomAPI.getRelations(
roomId = params.roomId,
eventId = params.rootThreadEventId,
relationType = RelationType.IO_THREAD,
eventType = if (isRoomEncrypted) EventType.ENCRYPTED else EventType.MESSAGE,
limit = 2000
)
}
val threadList = response.chunks + listOfNotNull(response.originalEvent)
return storeNewEventsIfNeeded(threadList, params.roomId)
}
/**
* Store new events if they are not already received, and returns weather or not,
* a timeline update should be made
* @param threadList is the list containing the thread replies
* @param roomId the roomId of the the thread
* @return
*/
private suspend fun storeNewEventsIfNeeded(threadList: List<Event>, roomId: String): Boolean {
var eventsSkipped = 0
monarchy
.awaitTransaction { realm ->
val chunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)
val optimizedThreadSummaryMap = hashMapOf<String, EventEntity>()
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
for (event in threadList.reversed()) {
if (event.eventId == null || event.senderId == null || event.type == null) {
eventsSkipped++
continue
}
if (EventEntity.where(realm, event.eventId).findFirst() != null) {
// Skip if event already exists
eventsSkipped++
continue
}
if (event.isEncrypted()) {
// Decrypt events that will be stored
decryptIfNeeded(event, roomId)
}
handleReaction(realm, event, roomId)
val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
// Sender info
roomMemberContentsByUser.getOrPut(event.senderId) {
// If we don't have any new state on this user, get it from db
val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root
rootStateEvent?.asDomain()?.getFixedRoomMemberContent()
}
chunk?.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
eventEntity.rootThreadEventId?.let {
// This is a thread event
optimizedThreadSummaryMap[it] = eventEntity
} ?: run {
// This is a normal event or a root thread one
optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity
}
}
optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(
roomId = roomId,
realm = realm,
currentUserId = userId,
shouldUpdateNotifications = false
)
}
Timber.i("----> size: ${threadList.size} | skipped: $eventsSkipped | threads: ${threadList.map { it.eventId }}")
return eventsSkipped == threadList.size
}
/**
* Invoke the event decryption mechanism for a specific event
*/
private fun decryptIfNeeded(event: Event, roomId: String) {
try {
// Event from sync does not have roomId, so add it to the event first
val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "")
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
} catch (e: MXCryptoError) {
if (e is MXCryptoError.Base) {
event.mCryptoError = e.errorType
event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
}
}
}
private fun handleReaction(realm: Realm,
event: Event,
roomId: String) {
val unsignedData = event.unsignedData ?: return
val relatedEventId = event.eventId ?: return
unsignedData.relations?.annotations?.chunk?.forEach { relationChunk ->
if (relationChunk.type == EventType.REACTION) {
val reaction = relationChunk.key
Timber.i("----> Annotation found in ${event.eventId} ${relationChunk.key} ")
val eventSummary = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, relatedEventId)
var sum = eventSummary.reactionsSummary.find { it.key == reaction }
if (sum == null) {
sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java)
sum.key = reaction
sum.firstTimestamp = event.originServerTs ?: 0
Timber.v("Adding synced reaction $reaction")
sum.count = 1
// reactionEventId not included in the /relations API
// sum.sourceEvents.add(reactionEventId)
eventSummary.reactionsSummary.add(sum)
} else {
sum.count += 1
}
}
}
}
}

View File

@@ -98,8 +98,14 @@ internal class DefaultSendService @AssistedInject constructor(
.let { sendEvent(it) }
}
override fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean): Cancelable {
return localEchoEventFactory.createQuotedTextEvent(roomId, quotedEvent, text, autoMarkdown)
override fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean, rootThreadEventId: String?): Cancelable {
return localEchoEventFactory.createQuotedTextEvent(
roomId = roomId,
quotedEvent = quotedEvent,
text = text,
autoMarkdown = autoMarkdown,
rootThreadEventId = rootThreadEventId
)
.also { createLocalEcho(it) }
.let { sendEvent(it) }
}
@@ -254,22 +260,37 @@ internal class DefaultSendService @AssistedInject constructor(
override fun sendMedias(attachments: List<ContentAttachmentData>,
compressBeforeSending: Boolean,
roomIds: Set<String>): Cancelable {
roomIds: Set<String>,
rootThreadEventId: String?
): Cancelable {
return attachments.mapTo(CancelableBag()) {
sendMedia(it, compressBeforeSending, roomIds)
sendMedia(
attachment = it,
compressBeforeSending = compressBeforeSending,
roomIds = roomIds,
rootThreadEventId = rootThreadEventId)
}
}
override fun sendMedia(attachment: ContentAttachmentData,
compressBeforeSending: Boolean,
roomIds: Set<String>): Cancelable {
roomIds: Set<String>,
rootThreadEventId: String?
): Cancelable {
// Ensure that the event will not be send in a thread if we are a different flow.
// Like sending files to multiple rooms
val rootThreadId = if (roomIds.isNotEmpty()) null else rootThreadEventId
// Create an event with the media file path
// Ensure current roomId is included in the set
val allRoomIds = (roomIds + roomId).toList()
// Create local echo for each room
val allLocalEchoes = allRoomIds.map {
localEchoEventFactory.createMediaEvent(it, attachment).also { event ->
localEchoEventFactory.createMediaEvent(
roomId = it,
attachment = attachment,
rootThreadEventId = rootThreadId).also { event ->
createLocalEcho(event)
}
}

View File

@@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.session.events.model.LocalEcho
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.UnsignedData
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.AudioInfo
import org.matrix.android.sdk.api.session.room.model.message.AudioWaveformInfo
import org.matrix.android.sdk.api.session.room.model.message.FileInfo
@@ -45,6 +46,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
@@ -292,13 +294,16 @@ internal class LocalEchoEventFactory @Inject constructor(
))
}
fun createMediaEvent(roomId: String, attachment: ContentAttachmentData): Event {
fun createMediaEvent(roomId: String,
attachment: ContentAttachmentData,
rootThreadEventId: String?
): Event {
return when (attachment.type) {
ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment)
ContentAttachmentData.Type.VIDEO -> createVideoEvent(roomId, attachment)
ContentAttachmentData.Type.AUDIO -> createAudioEvent(roomId, attachment, isVoiceMessage = false)
ContentAttachmentData.Type.VOICE_MESSAGE -> createAudioEvent(roomId, attachment, isVoiceMessage = true)
ContentAttachmentData.Type.FILE -> createFileEvent(roomId, attachment)
ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment, rootThreadEventId)
ContentAttachmentData.Type.VIDEO -> createVideoEvent(roomId, attachment, rootThreadEventId)
ContentAttachmentData.Type.AUDIO -> createAudioEvent(roomId, attachment, isVoiceMessage = false, rootThreadEventId = rootThreadEventId)
ContentAttachmentData.Type.VOICE_MESSAGE -> createAudioEvent(roomId, attachment, isVoiceMessage = true, rootThreadEventId = rootThreadEventId)
ContentAttachmentData.Type.FILE -> createFileEvent(roomId, attachment, rootThreadEventId)
}
}
@@ -321,7 +326,7 @@ internal class LocalEchoEventFactory @Inject constructor(
unsignedData = UnsignedData(age = null, transactionId = localId))
}
private fun createImageEvent(roomId: String, attachment: ContentAttachmentData): Event {
private fun createImageEvent(roomId: String, attachment: ContentAttachmentData, rootThreadEventId: String?): Event {
var width = attachment.width
var height = attachment.height
@@ -345,12 +350,19 @@ internal class LocalEchoEventFactory @Inject constructor(
height = height?.toInt() ?: 0,
size = attachment.size
),
url = attachment.queryUri.toString()
url = attachment.queryUri.toString(),
relatesTo = rootThreadEventId?.let {
RelationDefaultContent(
type = RelationType.IO_THREAD,
eventId = it,
inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
)
}
)
return createMessageEvent(roomId, content)
}
private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData): Event {
private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData, rootThreadEventId: String?): Event {
val mediaDataRetriever = MediaMetadataRetriever()
mediaDataRetriever.setDataSource(context, attachment.queryUri)
@@ -381,12 +393,23 @@ internal class LocalEchoEventFactory @Inject constructor(
thumbnailUrl = attachment.queryUri.toString(),
thumbnailInfo = thumbnailInfo
),
url = attachment.queryUri.toString()
url = attachment.queryUri.toString(),
relatesTo = rootThreadEventId?.let {
RelationDefaultContent(
type = RelationType.IO_THREAD,
eventId = it,
inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
)
}
)
return createMessageEvent(roomId, content)
}
private fun createAudioEvent(roomId: String, attachment: ContentAttachmentData, isVoiceMessage: Boolean): Event {
private fun createAudioEvent(roomId: String,
attachment: ContentAttachmentData,
isVoiceMessage: Boolean,
rootThreadEventId: String?
): Event {
val content = MessageAudioContent(
msgType = MessageType.MSGTYPE_AUDIO,
body = attachment.name ?: "audio",
@@ -400,12 +423,19 @@ internal class LocalEchoEventFactory @Inject constructor(
duration = attachment.duration?.toInt(),
waveform = waveformSanitizer.sanitize(attachment.waveform)
),
voiceMessageIndicator = if (!isVoiceMessage) null else emptyMap()
voiceMessageIndicator = if (!isVoiceMessage) null else emptyMap(),
relatesTo = rootThreadEventId?.let {
RelationDefaultContent(
type = RelationType.IO_THREAD,
eventId = it,
inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
)
}
)
return createMessageEvent(roomId, content)
}
private fun createFileEvent(roomId: String, attachment: ContentAttachmentData): Event {
private fun createFileEvent(roomId: String, attachment: ContentAttachmentData, rootThreadEventId: String?): Event {
val content = MessageFileContent(
msgType = MessageType.MSGTYPE_FILE,
body = attachment.name ?: "file",
@@ -413,7 +443,14 @@ internal class LocalEchoEventFactory @Inject constructor(
mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() },
size = attachment.size
),
url = attachment.queryUri.toString()
url = attachment.queryUri.toString(),
relatesTo = rootThreadEventId?.let {
RelationDefaultContent(
type = RelationType.IO_THREAD,
eventId = it,
inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
)
}
)
return createMessageEvent(roomId, content)
}
@@ -423,6 +460,7 @@ internal class LocalEchoEventFactory @Inject constructor(
}
fun createEvent(roomId: String, type: String, content: Content?): Event {
val newContent = enhanceStickerIfNeeded(type, content) ?: content
val localId = LocalEcho.createLocalEchoId()
return Event(
roomId = roomId,
@@ -430,19 +468,65 @@ internal class LocalEchoEventFactory @Inject constructor(
senderId = userId,
eventId = localId,
type = type,
content = content,
content = newContent,
unsignedData = UnsignedData(age = null, transactionId = localId)
)
}
/**
* Enhance sticker to support threads fallback if needed
*/
private fun enhanceStickerIfNeeded(type: String, content: Content?): Content? {
var newContent: Content? = null
if (type == EventType.STICKER) {
val isThread = (content.toModel<MessageStickerContent>())?.relatesTo?.type == RelationType.IO_THREAD
val rootThreadEventId = (content.toModel<MessageStickerContent>())?.relatesTo?.eventId
if (isThread && rootThreadEventId != null) {
val newRelationalDefaultContent = (content.toModel<MessageStickerContent>())?.relatesTo?.copy(
inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId))
)
newContent = (content.toModel<MessageStickerContent>())?.copy(
relatesTo = newRelationalDefaultContent
).toContent()
}
}
return newContent
}
/**
* Creates a thread event related to the already existing root event
*/
fun createThreadTextEvent(
rootThreadEventId: String,
roomId: String,
text: CharSequence,
msgType: String,
autoMarkdown: Boolean,
formattedText: String?): Event {
val content = formattedText?.let { TextContent(text.toString(), it) } ?: createTextContent(text, autoMarkdown)
return createEvent(
roomId,
EventType.MESSAGE,
content.toThreadTextContent(
rootThreadEventId = rootThreadEventId,
latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId),
msgType = msgType)
.toContent())
}
private fun dummyOriginServerTs(): Long {
return System.currentTimeMillis()
}
/**
* Creates a reply to a regular timeline Event or a thread Event if needed
*/
fun createReplyTextEvent(roomId: String,
eventReplied: TimelineEvent,
replyText: CharSequence,
autoMarkdown: Boolean): Event? {
autoMarkdown: Boolean,
rootThreadEventId: String? = null,
showInThread: Boolean): Event? {
// Fallbacks and event representation
// TODO Add error/warning logs when any of this is null
val permalink = permalinkFactory.createPermalink(eventReplied.root, false) ?: return null
@@ -473,11 +557,33 @@ internal class LocalEchoEventFactory @Inject constructor(
format = MessageFormat.FORMAT_MATRIX_HTML,
body = replyFallback,
formattedBody = replyFormatted,
relatesTo = RelationDefaultContent(null, null, ReplyToContent(eventId))
)
relatesTo = generateReplyRelationContent(
eventId = eventId,
rootThreadEventId = rootThreadEventId,
showAsReply = showInThread))
return createMessageEvent(roomId, content)
}
/**
* Generates the appropriate relatesTo object for a reply event.
* It can either be a regular reply or a reply within a thread
* "m.relates_to": {
* "rel_type": "m.thread",
* "event_id": "$thread_root",
* "m.in_reply_to": {
* "event_id": "$event_target",
* "render_in": ["m.thread"]
* }
* }
*/
private fun generateReplyRelationContent(eventId: String, rootThreadEventId: String? = null, showAsReply: Boolean): RelationDefaultContent =
rootThreadEventId?.let {
RelationDefaultContent(
type = RelationType.IO_THREAD,
eventId = it,
inReplyTo = ReplyToContent(eventId = eventId, renderIn = if (showAsReply) arrayListOf("m.thread") else null))
} ?: RelationDefaultContent(null, null, ReplyToContent(eventId = eventId))
private fun buildFormattedReply(permalink: String, userLink: String, userId: String, bodyFormatted: String, newBodyFormatted: String): String {
return REPLY_PATTERN.format(
permalink,
@@ -488,6 +594,7 @@ internal class LocalEchoEventFactory @Inject constructor(
newBodyFormatted
)
}
private fun buildReplyFallback(body: TextContent, originalSenderId: String?, newBodyText: String): String {
return buildString {
append("> <")
@@ -593,11 +700,28 @@ internal class LocalEchoEventFactory @Inject constructor(
quotedEvent: TimelineEvent,
text: String,
autoMarkdown: Boolean,
rootThreadEventId: String?
): Event {
val messageContent = quotedEvent.getLastMessageContent()
val textMsg = messageContent?.body
val quoteText = legacyRiotQuoteText(textMsg, text)
return createFormattedTextEvent(roomId, markdownParser.parse(quoteText, force = true, advanced = autoMarkdown), MessageType.MSGTYPE_TEXT)
return if (rootThreadEventId != null) {
createMessageEvent(
roomId,
markdownParser
.parse(quoteText, force = true, advanced = autoMarkdown)
.toThreadTextContent(
rootThreadEventId = rootThreadEventId,
latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId),
msgType = MessageType.MSGTYPE_TEXT)
)
} else {
createFormattedTextEvent(
roomId,
markdownParser.parse(quoteText, force = true, advanced = autoMarkdown),
MessageType.MSGTYPE_TEXT)
}
}
private fun legacyRiotQuoteText(quotedText: String?, myText: String): String {
@@ -631,6 +755,7 @@ internal class LocalEchoEventFactory @Inject constructor(
// </mx-reply>
// No whitespace because currently breaks temporary formatted text to Span
const val REPLY_PATTERN = """<mx-reply><blockquote><a href="%s">In reply to</a> <a href="%s">%s</a><br />%s</blockquote></mx-reply>%s"""
const val QUOTE_PATTERN = """<blockquote><p>%s</p></blockquote><p>%s</p>"""
// This is used to replace inner mx-reply tags
val MX_REPLY_REGEX = "<mx-reply>.*</mx-reply>".toRegex()

View File

@@ -138,7 +138,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
}
}
fun deleteFailedEchoAsync(roomId: String, eventId: String?) {
fun deleteFailedEchoAsync(roomId: String, eventId: String?) {
monarchy.runTransactionSync { realm ->
TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId ?: "").findFirst()?.deleteFromRealm()
EventEntity.where(realm, eventId = eventId ?: "").findFirst()?.deleteFromRealm()
@@ -215,4 +215,13 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
}
}
}
/**
* Returns the latest known thread event message, or the rootThreadEventId if no other event found
*/
fun getLatestThreadEvent(rootThreadEventId: String): String {
return realmSessionProvider.withRealm { realm ->
EventEntity.where(realm, eventId = rootThreadEventId).findFirst()?.threadSummaryLatestMessage?.eventId
} ?: rootThreadEventId
}
}

View File

@@ -16,9 +16,12 @@
package org.matrix.android.sdk.internal.session.room.send
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent
import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromHtmlReply
import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromReply
@@ -41,6 +44,29 @@ fun TextContent.toMessageTextContent(msgType: String = MessageType.MSGTYPE_TEXT)
)
}
/**
* Transform a TextContent to a thread message content. It will also add the inReplyTo
* latestThreadEventId in order for the clients without threads enabled to render it appropriately
* If latest event not found, we pass rootThreadEventId
*/
fun TextContent.toThreadTextContent(
rootThreadEventId: String,
latestThreadEventId: String,
msgType: String = MessageType.MSGTYPE_TEXT): MessageTextContent {
return MessageTextContent(
msgType = msgType,
format = MessageFormat.FORMAT_MATRIX_HTML.takeIf { formattedText != null },
body = text,
relatesTo = RelationDefaultContent(
type = RelationType.IO_THREAD,
eventId = rootThreadEventId,
inReplyTo = ReplyToContent(
eventId = latestThreadEventId
)),
formattedBody = formattedText
)
}
fun TextContent.removeInReplyFallbacks(): TextContent {
return copy(
text = extractUsefulTextFromReply(this.text),

View File

@@ -0,0 +1,103 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.threads
import androidx.lifecycle.LiveData
import com.zhuinden.monarchy.Monarchy
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.realm.Realm
import org.matrix.android.sdk.api.session.room.threads.ThreadsService
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
import org.matrix.android.sdk.internal.database.helper.findAllLocalThreadNotificationsForRoomId
import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId
import org.matrix.android.sdk.internal.database.helper.isUserParticipatingInThread
import org.matrix.android.sdk.internal.database.helper.mapEventsWithEdition
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.util.awaitTransaction
internal class DefaultThreadsService @AssistedInject constructor(
@Assisted private val roomId: String,
@UserId private val userId: String,
@SessionDatabase private val monarchy: Monarchy,
private val timelineEventMapper: TimelineEventMapper,
) : ThreadsService {
@AssistedFactory
interface Factory {
fun create(roomId: String): DefaultThreadsService
}
override fun getMarkedThreadNotificationsLive(): LiveData<List<TimelineEvent>> {
return monarchy.findAllMappedWithChanges(
{ TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) },
{ timelineEventMapper.map(it) }
)
}
override fun getMarkedThreadNotifications(): List<TimelineEvent> {
return monarchy.fetchAllMappedSync(
{ TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) },
{ timelineEventMapper.map(it) }
)
}
override fun getAllThreadsLive(): LiveData<List<TimelineEvent>> {
return monarchy.findAllMappedWithChanges(
{ TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
{ timelineEventMapper.map(it) }
)
}
override fun getAllThreads(): List<TimelineEvent> {
return monarchy.fetchAllMappedSync(
{ TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
{ timelineEventMapper.map(it) }
)
}
override fun isUserParticipatingInThread(rootThreadEventId: String): Boolean {
return Realm.getInstance(monarchy.realmConfiguration).use {
TimelineEventEntity.isUserParticipatingInThread(
realm = it,
roomId = roomId,
rootThreadEventId = rootThreadEventId,
senderId = userId)
}
}
override fun mapEventsWithEdition(threads: List<TimelineEvent>): List<TimelineEvent> {
return Realm.getInstance(monarchy.realmConfiguration).use {
threads.mapEventsWithEdition(it, roomId)
}
}
override suspend fun markThreadAsRead(rootThreadEventId: String) {
monarchy.awaitTransaction {
EventEntity.where(
realm = it,
eventId = rootThreadEventId).findFirst()?.threadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE
}
}
}

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