Compare commits

..

220 Commits

Author SHA1 Message Date
d1e9844469 Use CS API to send relation
fix scroll on login view when hs field is hidden
2019-05-20 16:03:10 +02:00
2581bf69e0 Merge pull request #139 from vector-im/feature/undo_reaction
Undo Reaction
2019-05-20 14:28:36 +02:00
2da4823e33 Fix / crash on logout
cannot access deleted object from delete
2019-05-20 13:58:45 +02:00
70c4b7528d Fix doc 2019-05-20 12:49:35 +02:00
5dfc0b3c0e Toggle Quick React (agree/disagree like/dislike) 2019-05-20 12:43:02 +02:00
44d1d063e9 Fix / theme update after rebase was displaying emojis grayed out 2019-05-20 10:16:00 +02:00
71e50b1bb9 Fix / Missing inject after rebase 2019-05-20 10:16:00 +02:00
64c307077f Refactoring / PruneWorker should be a task not a worker 2019-05-20 10:16:00 +02:00
71e364b42f Fix / Hide reactions on redacted message 2019-05-20 10:16:00 +02:00
054d339b48 Fix / Reaction stays highlighted when undone
When undoing my reaction, the reactji stays selected as if i have done the reaction
2019-05-20 10:16:00 +02:00
e3b9031e71 Fix / day separator flicker when adding reaction
When adding a reaction, the tmp local echo force the display of a new 'day separator' at the bottom if there was no new message for this day yet (then disappears just after -flicker-)
2019-05-20 10:16:00 +02:00
3fa9d7a1d4 Fix formatting 2019-05-20 10:16:00 +02:00
6eafa3c43d Undo Reaction 2019-05-20 10:16:00 +02:00
207579c59f Fix / margin end-right missing for compatibility 2019-05-20 10:14:40 +02:00
2780ca30a8 Merge branch 'feature/invites' into develop 2019-05-17 17:38:09 +02:00
41c54029b5 Merge pull request #138 from vector-im/feature/send_reaction-phase1
Send reaction view quick react and picker
2019-05-17 09:05:58 +02:00
f9142fedfd Increment reaction when reaction pill clicked in timeline 2019-05-16 18:33:32 +02:00
ef26519993 Send reaction view quick react and picker
+ fix / Error when to many reactions in cells (more than placeholders -8-)
+ fix / DefaultTimeline quick map access was not shifted when items inserted at given index
2019-05-16 16:35:54 +02:00
e27367e3f2 Merge pull request #137 from vector-im/feature/aggregations_relations
Feature/aggregations relations
2019-05-16 16:22:47 +02:00
dc9db9a438 Fix / was using wrong emoji for agree 2019-05-16 10:56:37 +02:00
6fe455dac0 Support AddedByMe in reaction + context menu reflect agree/like state 2019-05-16 10:49:52 +02:00
a9a1fe2aa0 Fix / hide keyboard when showing context action sheet
+ fix reaction colors
2019-05-16 09:33:49 +02:00
d68b447874 Relations Model + Aggregation + Reactions Summary 2019-05-16 09:21:10 +02:00
11e3a5def8 Moved reactions view to a stub 2019-05-16 09:20:24 +02:00
6aae943e77 Merge pull request #135 from vector-im/feature/reaction_timeline_ux
Reactions: Display existing reactions below the message
2019-05-15 11:24:00 +02:00
8d0322c0c3 Hot fix LL not working 2019-05-15 11:11:38 +02:00
f60a5f568f Invitation : start polishing but theme has to be rework. Waiting for Nad inputs. 2019-05-14 11:55:48 +02:00
00fd866cc8 Reactions: Display existing reactions below the message
- Reaction Button Bellow the message
- Upgrade to constraint layout 2.0.0 beta (for Constraint Helpers / Flow)
- Added tap on member name action
- Cleaning
2019-05-14 11:07:53 +02:00
8929898397 Merge pull request #134 from vector-im/feature/refactor_timeline_layouts
Refactoring / Use view ViewStub to avoid layout xml duplication
2019-05-13 16:59:13 +02:00
73d5110d67 Code review / Renamed base item class name 2019-05-13 16:29:33 +02:00
0c559976d6 Default avatar style so small 2019-05-13 09:38:52 +02:00
540989f38a Fix / ripple effect broken after merge 2019-05-10 18:43:06 +02:00
608bbdd4ee Refactoring / Use view ViewStub to avoid layout xml duplication 2019-05-10 18:31:56 +02:00
9a5f96f80b Merge pull request #132 from vector-im/feature/reactions_chooser
Feature/reactions chooser
2019-05-10 16:59:14 +02:00
6bf1deb99b Code Review 2019-05-10 12:14:40 +02:00
cfca4927e2 Cleaning 2019-05-10 09:28:52 +02:00
93cb7b8ce6 Cleaning 2019-05-09 18:43:10 +02:00
dcc430f91b Refactoring / revert reaction module to package in main module 2019-05-09 18:26:32 +02:00
64216f74ae fix / code quality 2019-05-09 09:15:30 +02:00
8fd15f4082 Debounce click + avatar click 2019-05-08 15:49:32 +02:00
38abf31889 Fix / ripple effect after merging 2019-05-08 10:33:14 +02:00
c39cfbe2ae Invites : allow to accept or reject the invite + clean some code. Require UI polishing. 2019-05-07 19:33:58 +02:00
b00bff0af5 Icon Update / Added timestamp on context menu
+ played with avatar sizes
2019-05-07 18:03:56 +02:00
56a2a3a065 [WIP] Emoji Reactions 2019-05-07 18:01:51 +02:00
a64f509872 WIP / emoji picker 2019-05-07 17:20:48 +02:00
359cc67fab fix indent? 2019-05-07 14:13:50 +02:00
72cd409735 Invites : start handling invite/left sync 2019-05-06 19:17:30 +02:00
ceac06caf6 Merge pull request #109 from vector-im/feature/timeline_formatting 2019-05-06 15:01:08 +02:00
694df9d845 Timeline : fix some timeline rendering issues (senderName, merge item, left event). Still need to work on it. 2019-05-06 14:26:41 +02:00
360d2a3c2e Fix Buildkite file format 2019-05-06 10:18:32 +02:00
9cfc67329a Fix Buildkite file format
Fix Buildkite file format
2019-05-06 10:13:29 +02:00
75c74c25e1 Merge pull request #119 from vector-im/feature/buildkite
Buildkite - WIP
2019-05-06 10:07:57 +02:00
7dce8c29bc Buildkite generate release APK (unsigned) 2019-05-06 10:07:16 +02:00
41337d0ca4 Configure pipeline for buildkite 2019-04-30 13:58:13 +02:00
287feace12 Timeline : merged events are now handled directly within the recyclerview and do not need a LinearLayout. 2019-04-20 10:42:37 +02:00
b3e2eca43d Timeline : start to handle merging room member events. Need to get better perf. 2019-04-18 16:50:02 +02:00
0818c55b6d Merge pull request #107 from vector-im/feature/cache
Clear cache and rework Signout
2019-04-17 16:26:48 +02:00
b1b526a516 Clear cache and rework Signout 2019-04-17 15:55:38 +02:00
b6cbed1c90 Fix build after merge 2019-04-17 12:50:43 +02:00
65e2abf402 Merge pull request #106 from vector-im/feature/apis
Handle filter for sync
2019-04-17 12:35:31 +02:00
bee5da8f64 Format and avoid long lines 2019-04-17 12:35:18 +02:00
ab7b807740 Rename APIs 2019-04-17 12:06:15 +02:00
4c02721ada Create realm module to avoid losing credentials when Realm model change. 2019-04-17 12:06:15 +02:00
5dca31e6f9 Do not loop sync in case of JsonEncodingException (if HS is down) 2019-04-17 12:05:39 +02:00
d2e1aff453 Convert JsonFilter to FilterId 2019-04-17 12:05:39 +02:00
dab80466c5 Timeline : extract video thumbnail and upload it 2019-04-16 17:24:28 +02:00
2c83ba0824 Timeline : start to handle video media. Probably to amend 2019-04-12 13:46:59 +02:00
657f4d3e9c Timeline : handle file/audio message 2019-04-12 12:38:02 +02:00
9c9c09db2b Adjust colors for avatar and display names + start handling video in timeline 2019-04-11 19:19:52 +02:00
c38a601bcc Timeline : apply color for sender 2019-04-11 15:40:07 +02:00
dcac9aed55 Merge branch 'feature/media_attachment' into develop 2019-04-11 13:21:51 +02:00
0225fc7120 Media upload: handle local echo by pinning at the bottom... will probably be changed 2019-04-11 12:48:35 +02:00
2a2b4e7bd9 Merge pull request #104 from vector-im/feature/emote
Send emote and update room topic
2019-04-10 09:40:32 +02:00
7d872420f0 Merge pull request #103 from vector-im/feature/linkify
Linkification: import workaround done on Riot
2019-04-09 18:37:48 +02:00
0055514f90 Update room topic 2019-04-09 18:33:28 +02:00
6248cc5552 Send emote 2019-04-09 17:53:23 +02:00
347967700b Linkification: import workaround done on Riot 2019-04-09 17:33:47 +02:00
eaff5ac9f0 Merge pull request #102 from vector-im/feature/completion
Add Slash command parser and handle room member invitation
2019-04-09 16:33:20 +02:00
63964ac101 SlashCommand: add TODO 2019-04-09 15:21:17 +02:00
3b52fa4be8 Command parser: check userId format 2019-04-09 14:52:31 +02:00
81ddb8c5fb SlashCommand: handle user invitation 2019-04-09 14:44:48 +02:00
eae8f993e6 SlashCommand: implement parser 2019-04-09 14:00:30 +02:00
fab1d249f4 Autocomplete : better code 2019-04-09 10:03:06 +02:00
3f1cc466ed Autocomplete : handle click 2019-04-09 09:58:07 +02:00
c64d6b6b28 Autocomplete : handle click and better detection for / commands 2019-04-08 18:31:24 +02:00
c9658918ed Media upload : start handling progress. 2019-04-08 16:39:08 +02:00
6d3028c2d7 Autocomplete : start fetching users. Still need to adjust UI and manage selection. 2019-04-08 16:18:36 +02:00
aec7b73345 Introduce room member service 2019-04-08 15:53:02 +02:00
56563412aa Autocomplete : start integrating commands. Still need to work on it 2019-04-08 15:51:35 +02:00
a9b8c57464 Allow cleartext communication on 127.0.0.1 (Fixes #98) 2019-04-08 15:21:12 +02:00
314771cce2 Allow cleartext communication on localhost (Fixes #98) 2019-04-08 15:15:11 +02:00
bb65dc5247 Merge pull request #99 from vector-im/feature/create_room
Create Room
2019-04-08 14:16:47 +02:00
460a72e6b5 Create Room 2019-04-08 13:49:28 +02:00
748090d0f9 Avoid critical log for timeout on event thread 2019-04-05 11:07:45 +02:00
968258852f Add a log 2019-04-05 11:01:43 +02:00
260cc0dd5f Rename class from Riot* to Vector* 2019-04-05 10:40:59 +02:00
57cea677e4 Import resource from Riot 2019-04-05 10:14:45 +02:00
c47eeb9cec Send media: first working implementation. Then, need to fix local echo and handle other types than image. 2019-04-04 19:55:58 +02:00
e929019247 Merge pull request #97 from vector-im/feature/settings
Feature/settings
2019-04-04 11:39:56 +02:00
43659dffd3 Improve signout task 2019-04-04 11:10:45 +02:00
82d89825d3 Differentiate icon from Riot one 2019-04-04 11:10:31 +02:00
772670252a Make build FDroid pass 2019-04-04 11:10:12 +02:00
18591d0287 Media: start to play with uploading media 2019-04-03 23:06:17 +02:00
08dacacdda SignOut 2019-04-03 18:25:39 +02:00
3091a337c9 Launcher + Animated logo 2019-04-03 14:46:15 +02:00
73580493ea Import resource from riot 2019-04-03 14:34:52 +02:00
c188bb290f Not implemented toast or Snackbar 2019-04-03 14:08:25 +02:00
d9c8867c0b Introduce common parent for PreferenceFragment 2019-04-03 12:10:02 +02:00
87c9f6b2a0 Improve lint configuration 2019-04-03 12:04:24 +02:00
6830957d31 Import settings from Riot - not all fonctional of course 2019-04-02 18:08:43 +02:00
96a67a44ac Media: grab some code from Riot legacy 2019-04-02 15:59:36 +02:00
b9b8527b38 Improve RiotFragment 2019-04-02 12:14:16 +02:00
10520fb1bd Upgrade string from Riot 2019-04-02 11:57:12 +02:00
494d893aa3 Merge pull request #96 from vector-im/feature/vector
Rename `app` to `vector`
2019-04-02 11:46:43 +02:00
9048a1dbbe Rename app to vector 2019-04-02 11:24:54 +02:00
04b4f32e16 Sync : add log and continue when read_receipts fail 2019-04-01 17:39:27 +02:00
d110dac0a6 Merge feature/replace_paged_list into develop 2019-04-01 17:33:53 +02:00
be6a4efacb Timeline : make tests compile and pass 2019-04-01 15:18:52 +02:00
f75fe1201d Merge pull request #95 from vector-im/feature/fix_state
RageShake
2019-03-29 21:13:14 +01:00
547272b17b Please TravisCI build 2019-03-29 18:25:44 +01:00
94db36d6c4 Fix room summary not being updated when room members are loaded 2019-03-29 11:15:27 +01:00
3efcbaaea9 cleanup 2019-03-29 09:49:07 +01:00
fde09b4a94 Forbid backup 2019-03-28 17:55:38 +01:00
317503aa2b userAgent, userId and deviceId in rageshake 2019-03-28 17:49:13 +01:00
be2dad9b17 ExceptionHandler + Log in files for RageShake 2019-03-28 17:28:20 +01:00
2e2d5b9f86 Timeline : add non empty content to displayable conditions => should probably be removed later if we want to handle this case with special epoxy item. 2019-03-28 16:24:40 +01:00
a7b81a4671 Timeline : remove item animation for now. 2019-03-28 16:07:05 +01:00
86a60f7ebd Timeline : fix avatar issues / remove glide logs 2019-03-28 16:06:43 +01:00
a6366e47fe Timeline : change some database details to make it faster 2019-03-28 12:00:45 +01:00
bc467340c9 RageShake impleme + Import stuff from Riot 2019-03-28 11:53:28 +01:00
4154df7c21 Timeline : stabilize the pagedList replacement. Seems ok for phase0 2019-03-27 19:57:36 +01:00
79e273b1ca Add Doc 2019-03-27 18:03:36 +01:00
4ab3c39415 Merge pull request #18 from z3ntu/develop_api19
Hacks to get it running on API 19
2019-03-21 15:58:37 +01:00
ad243ae41f Clean code and update dependencies 2019-03-20 18:24:17 +01:00
2898eae566 Timeline : reactivate loaders and get off the main thread 2019-03-20 16:24:50 +01:00
cbd62b9e93 Rename Realm DB file 2019-03-20 14:36:47 +01:00
27374aea3f Add DebugReceiver 2019-03-20 14:12:10 +01:00
a4ef259bd2 Add FIXME 2019-03-19 18:17:40 +01:00
3f10829dcd Use correct wording for room list 2019-03-19 18:11:02 +01:00
289c820b48 Merge pull request #92 from vector-im/feature/user-agent
User agent and log of request in debug mode
2019-03-19 15:26:11 +01:00
40d4e3fe83 Log http requests, for easy debugging 2019-03-19 12:55:14 +01:00
f4170f55b7 Configure user agent 2019-03-19 12:29:45 +01:00
d8bff8201f Use gradle all distribution 2019-03-19 10:17:33 +01:00
dffcc3f405 Fix gradle issue 2019-03-18 18:53:14 +01:00
7030258de5 Add logo to badges 2019-03-18 18:49:05 +01:00
89a2aad561 Add logo to badges 2019-03-18 18:48:28 +01:00
6406c4021a Merge pull request #89 from vector-im/feature/gradle5.2.1
Upgrade gradle wrapper to 5.2.1, and pass first sonar analysis
2019-03-18 18:42:30 +01:00
ddc5a0d30b Add some badges FTW, and link to contributing.md 2019-03-18 18:42:04 +01:00
d4b1f074e3 Merge pull request #87 from Bubu/reproducible_versioncode
get timestamp for versioncode from last git commmit
2019-03-18 18:21:56 +01:00
3b68b304f5 Merge branch 'develop' into reproducible_versioncode 2019-03-18 18:21:22 +01:00
b8cceaa17a Upgrade gradle wrapper to 5.2.1, and pass first sonar analysis 2019-03-18 18:16:59 +01:00
8a7675d2e4 Merge pull request #88 from vector-im/feature/build_process
Configure CI
2019-03-18 17:26:32 +01:00
6cf91e5eca Configure sonar 2019-03-18 16:53:52 +01:00
e938866081 Fix issue in Strings 2019-03-18 16:42:19 +01:00
20108c2603 Add bug_report_url config 2019-03-18 16:32:35 +01:00
02c7deb783 Import strings from Riot and create a script to do so 2019-03-18 16:06:06 +01:00
368cf4b3f0 Create new file for RiotX string 2019-03-18 15:17:17 +01:00
c093e07c24 Deactivate test compilation by Travis for now. 2019-03-18 15:15:40 +01:00
a97142272c Fix lint issue 2019-03-18 15:08:57 +01:00
97fa94592e get timestamp for versioncode from last git commmit
This makes the versioncode for an apk reproducible. Building the same
commit/code will produce an apk with the same VC.

Signed-off-by: Marcus Hoffmann <bubu@bubu1.eu>
2019-03-16 13:56:25 +01:00
0c76178bee Markwon : update method name 2019-03-15 19:28:13 +01:00
c12bc5e02d Timeline rework : first version working for backward navigation (need more testing) 2019-03-15 19:27:56 +01:00
0db1d98f41 Fix compilation issue 2019-03-15 19:18:43 +01:00
008f6e2bdd Android version for TravisCI 2019-03-15 18:48:53 +01:00
820709d433 Timeline rework : initial commit - to amend. 2019-03-13 22:30:05 +01:00
d5838262ea Fix i18n issue 2019-03-13 18:36:57 +01:00
617f71fd51 Remove .idea files 2019-03-13 18:04:57 +01:00
1ac143a065 Travis 2019-03-13 18:02:12 +01:00
eca59114dd Setup various tools 2019-03-13 17:50:03 +01:00
a199eaa171 Setup various tools 2019-03-13 17:00:30 +01:00
26ae034ba9 Add debug_mode resValue 2019-03-13 15:11:02 +01:00
99b1c0bed4 Merge pull request #73 from tulir/patch-1
Fix room link in readme
2019-03-13 11:56:58 +01:00
388eae6a1c Merge branch 'feature/image_viewer' into develop 2019-03-12 17:26:41 +01:00
ff6d1f611a Media : exposes the view to allow animation. 2019-03-12 16:54:33 +01:00
157068634a MediaViewer : start adding simple way of seing image in full size. Will probably change in the future. 2019-03-12 08:29:49 +01:00
1d4882e596 Timeline : fix small issue, reduce animation time and reorganize a bit some files. 2019-03-05 18:31:03 +01:00
415d9e702b Fix room link 2019-03-05 13:40:05 +02:00
4c3f7171e7 Update README.md & Create LICENSE 2019-03-04 19:18:15 +01:00
a146fc43e4 Merge branch 'feature/home_state_issues' into develop 2019-03-04 17:00:50 +01:00
ef3fb561e9 Pills : finalize avatar retrieval 2019-03-04 16:52:44 +01:00
fffdf4b8c1 Pills : try to optimize memory and get better perf. Still need to rework a bit. 2019-03-01 21:44:26 +01:00
3d7562ea8e UI : fix notice avatar position 2019-02-28 18:50:46 +01:00
fd3fce6deb State issues : restore recyclerview state + fix DI issues 2019-02-28 18:50:30 +01:00
753e70775a Merge branch 'feature/html_rendering' into develop 2019-02-27 17:51:06 +01:00
2c0bc93f5a Home: getting Room interface slow (blocking Main Thread). Maybe we should get this async. 2019-02-27 17:41:06 +01:00
adc51529f2 Pills : handle avatar 2019-02-27 17:17:47 +01:00
63bf4355b9 Html : continue work on Pills. Still need to find how to handle avatar drawable. 2019-02-26 19:35:47 +01:00
41b06bca60 User : rework UserEntityUpdater (and make others RealmLiveEntityObserver process on MonarchyThread instead of Main) 2019-02-26 19:32:01 +01:00
d6f6764b0c Timeline : avoid epoxy to build on main thread first 2019-02-26 19:28:39 +01:00
46226c4efc Avatar : skip '@' when name is userId 2019-02-26 15:04:43 +01:00
f59977f884 Room state: fix sync state events duplicates with timeline 2019-02-26 14:50:53 +01:00
40f1fcab18 User : start handling users. Need to fine a good strategy to process room member events. 2019-02-25 21:55:33 +01:00
85b119bdcb Home : hide keyboard when drawer state change 2019-02-25 18:13:53 +01:00
6c7bc2b40c Timeline: get better perf 2019-02-25 18:10:37 +01:00
f06211ce4f Start playing with ChipDrawable to show Pills 2019-02-25 15:18:36 +01:00
d2db5e32fc Html : start handling reply 2019-02-22 15:43:48 +01:00
4458e28ce2 Html : introduce markown lib to handle html rendering. 2019-02-21 19:21:08 +01:00
14ac3a8ae6 Merge branch 'feature/timeline_new_events' into develop 2019-02-20 18:09:37 +01:00
ca890e1ef4 Room tag: let the tag order be anything to avoid breaking at the moment. 2019-02-20 18:09:07 +01:00
9cc2cf8360 Timeline: set empty item as fallback item 2019-02-20 16:14:12 +01:00
28c837a47f Timeline: handle call events 2019-02-20 15:47:20 +01:00
42cf45c8f3 Timeline: handle room history visibility 2019-02-20 12:06:33 +01:00
8fdce937bd Timeline: handle notice events and hide some not displayable events. 2019-02-20 11:39:25 +01:00
f36bec7176 Merge branch 'feature/emote' into develop 2019-02-19 19:37:31 +01:00
9477316b61 Sync : add timber log on sync failure 2019-02-19 19:37:16 +01:00
36d1d52880 Timeline : handle emote content 2019-02-19 19:12:58 +01:00
abb44839af Merge branch 'feature/epoxy' into develop 2019-02-19 17:49:36 +01:00
e91276bb76 Epoxy : make sures RiotEpoxyModel is used everywhere. 2019-02-19 17:48:30 +01:00
b5f40f9732 Add stetho in debug to allow some inspection 2019-02-19 17:40:38 +01:00
24ffd96b6e Epoxy : finalize replacing KotlinModels 2019-02-19 15:47:50 +01:00
c977c651a2 Epoxy : timeline messages to holder WIP 2019-02-19 11:57:17 +01:00
5c78991ae1 Rx : hides subject inside a class 2019-02-19 11:16:31 +01:00
c467f179e1 Epoxy : start using viewholder instead of kotlin model as it's more efficient 2019-02-19 10:31:05 +01:00
264265a1f7 Merge branch 'feature/quick_fixes' into develop 2019-02-18 18:14:26 +01:00
a12a9da627 Update Realm 2019-02-18 17:42:03 +01:00
c42294a21e Handle room v3 event id format 2019-02-18 17:41:35 +01:00
03437885ef Read receipts : fix issues with receipts not being sent 2019-02-18 17:41:21 +01:00
5e81fc8dc2 Temporary workarounds to avoid some crashes. 2019-02-11 13:47:47 +01:00
7a5ff282b6 Fix room list display on my account
following Ganfra's advices
2019-02-01 18:57:19 +01:00
e4069ab51b Hacks to get it running on API 19 2019-01-30 17:52:44 +01:00
1099 changed files with 80159 additions and 4605 deletions

41
.buildkite/pipeline.yml Normal file
View File

@ -0,0 +1,41 @@
# Use Docker file from https://hub.docker.com/r/runmymind/docker-android-sdk
# Last docker plugin version can be found here:
# https://github.com/buildkite-plugins/docker-buildkite-plugin/releases
# Build debug version of the RiotX application, from the develop branch and the features branches
steps:
- label: "Assemble Debug version"
agents:
# We use a medium sized instance instead of the normal small ones because
# gradle build is long
queue: "medium"
commands:
- "./gradlew clean lintGplayRelease assembleGplayDebug --stacktrace"
- "./gradlew lintFdroidRelease assembleFdroidDebug --stacktrace"
artifact_paths:
- "vector/build/outputs/apk/gplay/debug/*.apk"
- "vector/build/outputs/apk/fdroid/debug/*.apk"
branches: "develop feature/*"
plugins:
- docker#v3.1.0:
image: "runmymind/docker-android-sdk"
- label: "Build Google Play unsigned APK"
agents:
# We use a medium sized instance instead of the normal small ones because
# gradle build is long
queue: "medium"
commands:
- "./gradlew clean assembleGplayRelease --stacktrace"
artifact_paths:
- "vector/build/outputs/apk/gplay/release/*.apk"
branches: "master"
plugins:
- docker#v3.1.0:
image: "runmymind/docker-android-sdk"
# Code quality
- label: "Code quality"
command: "./tools/check/check_code_quality.sh"

10
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,10 @@
### Pull Request Checklist
<!-- Please read [CONTRIBUTING.md](https://github.com/vector-im/riotX-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->
- [ ] Changes has been tested on an Android device or Android emulator with API 16
- [ ] UI change has been tested on both light and dark themes
- [ ] Pull request is based on the develop branch
- [ ] Pull request updates [CHANGES.md](https://github.com/vector-im/riotX-android/blob/develop/CHANGES.md)
- [ ] Pull request includes screenshots or videos if containing UI changes
- [ ] Pull request includes a [sign off](https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.rst#sign-off)

2
.gitignore vendored
View File

@ -10,3 +10,5 @@
/build
/captures
.externalNativeBuild
/tmp

View File

@ -1,29 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<Objective-C-extensions>
<file>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" />
</file>
<class>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" />
</class>
<extensions>
<pair source="cpp" header="h" fileNamingConvention="NONE" />
<pair source="c" header="h" fileNamingConvention="NONE" />
</extensions>
</Objective-C-extensions>
</code_scheme>
</component>

View File

@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View File

@ -1,13 +0,0 @@
<component name="ProjectDictionaryState">
<dictionary name="ganfra">
<words>
<w>connectable</w>
<w>coroutine</w>
<w>merlins</w>
<w>moshi</w>
<w>persistor</w>
<w>synchronizer</w>
<w>untimelined</w>
</words>
</dictionary>
</component>

20
.idea/gradle.xml generated
View File

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/matrix-sdk-android" />
<option value="$PROJECT_DIR$/matrix-sdk-android-rx" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>

View File

@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="DifferentStdlibGradleVersion" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>
</component>

34
.idea/misc.xml generated
View File

@ -1,34 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="5">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
<item index="3" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="4" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
</list>
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>

60
.travis.yml Normal file
View File

@ -0,0 +1,60 @@
# FTR: Configuration on https://travis-ci.org/vector-im/riotX-android/settings
#
# - Build only if .travis.yml is present -> On
# - Limit concurrent jobs -> Off
# - Build pushed branches -> On (build the branch)
# - Build pushed pull request -> On (build the PR after auto-merge)
#
# - Auto cancel branch builds -> On
# - Auto cancel pull request builds -> On
language: android
jdk: oraclejdk8
sudo: false
notifications:
email: false
android:
components:
# Uncomment the lines below if you want to
# use the latest revision of Android SDK Tools
- tools
- platform-tools
# The BuildTools version used by your project
- build-tools-28.0.3
# The SDK version used to compile your project
- android-28
before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
cache:
directories:
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/
- $HOME/.android/build-cache
# Build with the development SDK
before_script:
# Not necessary for the moment
# - /bin/sh ./set_debug_env.sh
# Just build the project for now
script:
# Build app (assembleGplayRelease assembleFdroidRelease)
# Build Android test (assembleAndroidTest) (disabled for now)
# Code quality (lintGplayRelease lintFdroidRelease)
# Split into two steps because if a task contain Fdroid, PlayService will be disabled
- ./gradlew clean assembleGplayRelease lintGplayRelease --stacktrace
- ./gradlew clean assembleFdroidRelease lintFdroidRelease --stacktrace
# Run unitary test (Disable for now, see https://travis-ci.org/vector-im/riot-android/builds/502504370)
# - ./gradlew testGplayReleaseUnitTest --stacktrace
# Other code quality check
- ./tools/check/check_code_quality.sh
- ./tools/travis/check_pr.sh
# Check that indonesians file are identical. Due to Android issue, the resource folder must be value-in/, and Weblate export data into value-id/.
- diff ./vector/src/main/res/values-id/strings.xml ./vector/src/main/res/values-in/strings.xml

0
AUTHORS.md Normal file
View File

49
CHANGES.md Normal file
View File

@ -0,0 +1,49 @@
Changes in RiotX 0.XX (2019-XX-XX)
===================================================
Features:
- Contextual action menu for messages in room
Improvements:
-
Other changes:
-
Bugfix:
-
Translations:
-
Build:
-
=======================================================
+ TEMPLATE WHEN PREPARING A NEW RELEASE +
=======================================================
Changes in RiotX 0.XX (2019-XX-XX)
===================================================
Features:
-
Improvements:
-
Other changes:
-
Bugfix:
-
Translations:
-
Build:
-

76
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,76 @@
# Contributing code to Matrix
Please read https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.rst
Android support can be found in this [![Riot Android Matrix room #riot-android:matrix.org](https://img.shields.io/matrix/riot-android:matrix.org.svg?label=%23riot-android:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#riot-android:matrix.org) room.
Dedicated room for RiotX: [![RiotX Android Matrix room #riot-android:matrix.org](https://img.shields.io/matrix/riotx:matrix.org.svg?label=%23RiotX:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#riotx:matrix.org)
# Specific rules for Matrix Android projects
## Android Studio settings
Please set the "hard wrap" setting of Android Studio to 160 chars, this is the setting we use internally to format the source code (Menu `Settings/Editor/Code Style` then `Hard wrap at`).
## Compilation
For now, the Matrix SDK and the RiotX application are in the same project. So there is no specific thing to do, this project should compile without any special action.
## I want to help translating RiotX
If you want to fix an issue with an English string, please submit a PR.
If you want to fix an issue in other languages, or add a missing translation, or even add a new language, please use [Weblate](https://translate.riot.im/projects/riot-android/).
For the moment, Strings from Riot will be used, there is no dedicated project in Weblate for RiotX.
## I want to submit a PR to fix an issue
Please check if a corresponding issue exists. If yes, please let us know in a comment that you're working on it.
If an issue does not exist yet, it may be relevant to open a new issue and let us know that you're implementing it.
### Kotlin
This project is full Kotlin. Please do not write Java classes.
### CHANGES.md
Please add a line to the top of the file `CHANGES.md` describing your change.
### Code quality
Make sure the following commands execute without any error:
> ./tools/check/check_code_quality.sh
> ./gradlew lintGplayRelease
### Unit tests
Make sure the following commands execute without any error:
> ./gradlew testGplayReleaseUnitTest
### Tests
RiotX is currently supported on Android Jelly Bean (API 16+): please test your change on an Android device (or Android emulator) running with API 16. Many issues can happen (including crashes) on older devices.
Also, if possible, please test your change on a real device. Testing on Android emulator may not be sufficient.
### Internationalisation
When adding new string resources, please only add new entries in file `value/strings.xml`. Translations will be added later by the community of translators with a specific tool named [Weblate](https://translate.riot.im/projects/riot-android/).
Do not hesitate to use plurals when appropriate.
### Layout
When adding or editing layouts, make sure the layout will render correctly if device uses a RTL (Right To Left) language.
You can check this in the layout editor preview by selecting any RTL language (ex: Arabic).
Also please check that the colors are ok for all the current themes of RiotX. Please use `?attr` instead of `@color` to reference colors in the layout. You can check this in the layout editor preview by selecting all the main themes (`AppTheme.Status`, `AppTheme.Dark`, etc.).
### Authors
Feel free to add an entry in file AUTHORS.md
## Thanks
Thanks for contributing to Matrix projects!

176
LICENSE Normal file
View File

@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View File

@ -1 +1,20 @@
# riot-android-redesign-PoC
[![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android)
[![Weblate](https://translate.riot.im/widgets/riot-android/-/svg-badge.svg)](https://translate.riot.im/engage/riot-android/?utm_source=widget)
[![RiotX Android Matrix room #riot-android:matrix.org](https://img.shields.io/matrix/riotx:matrix.org.svg?label=%23RiotX:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#riotx:matrix.org)
[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=vector.android.riotx&metric=alert_status)](https://sonarcloud.io/dashboard?id=vector.android.riotx)
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=vector.android.riotx&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=vector.android.riotx)
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=vector.android.riotx&metric=bugs)](https://sonarcloud.io/dashboard?id=vector.android.riotx)
# RiotX Android
RiotX is an Android Matrix Client currently in development. The application is not yet available on the PlayStore.
It's based on a new Matrix SDK, written in Kotlin.
Download nightly build here: [![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)
Matrix Room: [![RiotX Android Matrix room #riot-android:matrix.org](https://img.shields.io/matrix/riotx:matrix.org.svg?label=%23RiotX:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#riotx:matrix.org)
## Contributing
Please refer to [CONTRIBUTING.md](https://github.com/vector-im/riotX-android/blob/develop/CONTRIBUTING.md) if you want to contribute the Matrix on Android projects!

View File

@ -1,105 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
kapt {
correctErrorTypes = true
}
androidExtensions {
experimental = true
}
def versionMajor = 0
def versionMinor = 1
def versionPatch = 0
def generateVersionCodeFromTimestamp() {
// It's unix timestamp divided by 10: It's incremented by one every 10 seconds.
return (System.currentTimeMillis() / 1_000 / 10).toInteger()
}
def generateVersionCodeFromVersionName() {
return versionMajor * 10000 + versionMinor * 100 + versionPatch
}
android {
compileSdkVersion 28
defaultConfig {
applicationId "im.vector.riotredesign"
minSdkVersion 16
targetSdkVersion 28
multiDexEnabled true
versionCode generateVersionCodeFromTimestamp()
versionName "${versionMajor}.${versionMinor}.${versionPatch}"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
def epoxy_version = "3.0.0"
def arrow_version = "0.8.2"
def glide_version = "4.8.0"
implementation project(":matrix-sdk-android")
implementation project(":matrix-sdk-android-rx")
implementation 'com.android.support:multidex:1.0.3'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0-alpha01'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.core:core-ktx:1.0.1'
// Paging
implementation 'androidx.paging:paging-runtime:2.0.0'
implementation 'com.jakewharton.threetenabp:threetenabp:1.1.1'
implementation 'com.jakewharton.timber:timber:4.7.1'
// rx
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
implementation 'com.jakewharton.rxrelay2:rxrelay:2.1.0'
implementation 'com.jakewharton.rxbinding2:rxbinding:2.2.0'
implementation("com.airbnb.android:epoxy:$epoxy_version")
kapt "com.airbnb.android:epoxy-processor:$epoxy_version"
implementation 'com.airbnb.android:mvrx:0.7.0'
// FP
implementation "io.arrow-kt:arrow-core:$arrow_version"
// UI
implementation "com.github.bumptech.glide:glide:$glide_version"
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
kapt "com.github.bumptech.glide:compiler:$glide_version"
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.google.android.material:material:1.1.0-alpha02'
// DI
implementation "org.koin:koin-android:$koin_version"
implementation "org.koin:koin-android-scope:$koin_version"
// TESTS
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}

View File

@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="im.vector.riotredesign">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".Riot"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Riot">
<activity
android:name=".features.MainActivity"
android:theme="@style/Theme.Riot.Splash">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".features.home.HomeActivity" />
<activity android:name=".features.login.LoginActivity" />
</application>
</manifest>

View File

@ -1,46 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.riotredesign
import android.app.Application
import android.content.Context
import androidx.multidex.MultiDex
import com.jakewharton.threetenabp.AndroidThreeTen
import im.vector.matrix.android.BuildConfig
import im.vector.riotredesign.core.di.AppModule
import org.koin.log.EmptyLogger
import org.koin.standalone.StandAloneContext.startKoin
import timber.log.Timber
class Riot : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
AndroidThreeTen.init(this)
startKoin(listOf(AppModule(this).definition), logger = EmptyLogger())
}
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
MultiDex.install(this)
}
}

View File

@ -1,74 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.riotredesign.core.epoxy
import android.view.View
import androidx.annotation.IdRes
import androidx.annotation.LayoutRes
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.OnModelVisibilityStateChangedListener
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
abstract class KotlinModel(
@LayoutRes private val layoutRes: Int
) : EpoxyModel<View>() {
private var view: View? = null
private var onBindCallback: (() -> Unit)? = null
private var onModelVisibilityStateChangedListener: OnModelVisibilityStateChangedListener<KotlinModel, View>? = null
abstract fun bind()
override fun bind(view: View) {
this.view = view
onBindCallback?.invoke()
bind()
}
override fun unbind(view: View) {
this.view = null
}
fun onBind(lambda: (() -> Unit)?): KotlinModel {
onBindCallback = lambda
return this
}
override fun onVisibilityStateChanged(visibilityState: Int, view: View) {
onModelVisibilityStateChangedListener?.onVisibilityStateChanged(this, view, visibilityState)
super.onVisibilityStateChanged(visibilityState, view)
}
fun setOnVisibilityStateChanged(listener: OnModelVisibilityStateChangedListener<KotlinModel, View>): KotlinModel {
this.onModelVisibilityStateChangedListener = listener
return this
}
override fun getDefaultLayout() = layoutRes
protected fun <V : View> bind(@IdRes id: Int) = object : ReadOnlyProperty<KotlinModel, V> {
override fun getValue(thisRef: KotlinModel, property: KProperty<*>): V {
// This is not efficient because it looks up the view by id every time (it loses
// the pattern of a "holder" to cache that look up). But it is simple to use and could
// be optimized with a map
@Suppress("UNCHECKED_CAST")
return view?.findViewById(id) as V?
?: throw IllegalStateException("View ID $id for '${property.name}' not found.")
}
}
}

View File

@ -1,41 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.riotredesign.features
import android.os.Bundle
import im.vector.matrix.android.api.Matrix
import im.vector.riotredesign.core.platform.RiotActivity
import im.vector.riotredesign.features.home.HomeActivity
import im.vector.riotredesign.features.login.LoginActivity
class MainActivity : RiotActivity() {
private val authenticator = Matrix.getInstance().authenticator()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = if (authenticator.hasActiveSessions()) {
HomeActivity.newIntent(this)
} else {
LoginActivity.newIntent(this)
}
startActivity(intent)
finish()
}
}

View File

@ -1,57 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.riotredesign.features.home
import android.widget.ImageView
import androidx.core.content.ContextCompat
import com.amulyakhare.textdrawable.TextDrawable
import com.bumptech.glide.request.RequestOptions
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.firstCharAsString
import im.vector.riotredesign.core.glide.GlideApp
object AvatarRenderer {
fun render(roomMember: RoomMember, imageView: ImageView) {
render(roomMember.avatarUrl, roomMember.displayName, imageView)
}
fun render(roomSummary: RoomSummary, imageView: ImageView) {
render(roomSummary.avatarUrl, roomSummary.displayName, imageView)
}
fun render(avatarUrl: String?, name: String?, imageView: ImageView) {
if (name.isNullOrEmpty()) {
return
}
val resolvedUrl = Matrix.getInstance().currentSession.contentUrlResolver().resolveFullSize(avatarUrl)
val avatarColor = ContextCompat.getColor(imageView.context, R.color.pale_teal)
val fallbackDrawable = TextDrawable.builder().buildRound(name.firstCharAsString().toUpperCase(), avatarColor)
GlideApp
.with(imageView)
.load(resolvedUrl)
.placeholder(fallbackDrawable)
.apply(RequestOptions.circleCropTransform())
.into(imageView)
}
}

View File

@ -1,100 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.riotredesign.features.home
import im.vector.riotredesign.features.home.group.SelectedGroupHolder
import im.vector.riotredesign.features.home.room.VisibleRoomHolder
import im.vector.riotredesign.features.home.room.detail.timeline.DefaultItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.MessageItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.RoomMemberItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.RoomNameItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.RoomTopicItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineDateFormatter
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.riotredesign.features.home.room.list.RoomSummaryComparator
import im.vector.riotredesign.features.home.room.list.RoomSummaryController
import org.koin.dsl.module.module
class HomeModule {
val definition = module(override = true) {
single {
TimelineDateFormatter(get())
}
single {
MessageItemFactory(get(), get())
}
single {
RoomNameItemFactory(get())
}
single {
RoomTopicItemFactory(get())
}
single {
RoomMemberItemFactory(get())
}
single {
DefaultItemFactory()
}
single {
TimelineItemFactory(get(), get(), get(), get(), get())
}
single {
HomeNavigator()
}
factory {
RoomSummaryController(get())
}
factory { (roomId: String) ->
TimelineEventController(roomId, get(), get(), get())
}
single {
TimelineMediaSizeProvider()
}
single {
SelectedGroupHolder()
}
single {
VisibleRoomHolder()
}
single {
HomePermalinkHandler(get())
}
single {
RoomSummaryComparator()
}
}
}

View File

@ -1,41 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.riotredesign.features.home.group
import android.widget.ImageView
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.KotlinModel
import im.vector.riotredesign.core.platform.CheckableFrameLayout
import im.vector.riotredesign.features.home.AvatarRenderer
data class GroupSummaryItem(
val groupName: CharSequence,
val avatarUrl: String?,
val isSelected: Boolean,
val listener: (() -> Unit)? = null
) : KotlinModel(R.layout.item_group) {
private val avatarImageView by bind<ImageView>(R.id.groupAvatarImageView)
private val rootView by bind<CheckableFrameLayout>(R.id.itemGroupLayout)
override fun bind() {
rootView.isSelected = isSelected
rootView.setOnClickListener { listener?.invoke() }
AvatarRenderer.render(avatarUrl, groupName.toString(), avatarImageView)
}
}

View File

@ -1,154 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.riotredesign.features.home.room.detail
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.RiotFragment
import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.features.home.AvatarRenderer
import im.vector.riotredesign.features.home.HomePermalinkHandler
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_room_detail.*
import org.koin.android.ext.android.inject
import org.koin.core.parameter.parametersOf
@Parcelize
data class RoomDetailArgs(
val roomId: String,
val eventId: String? = null
) : Parcelable
class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
companion object {
fun newInstance(args: RoomDetailArgs): RoomDetailFragment {
return RoomDetailFragment().apply {
setArguments(args)
}
}
}
private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
private val roomDetailArgs: RoomDetailArgs by args()
private val timelineEventController by inject<TimelineEventController> { parametersOf(roomDetailArgs.roomId) }
private val homePermalinkHandler by inject<HomePermalinkHandler>()
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_room_detail, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setupRecyclerView()
setupToolbar()
setupSendButton()
roomDetailViewModel.subscribe { renderState(it) }
}
override fun onResume() {
super.onResume()
roomDetailViewModel.process(RoomDetailActions.IsDisplayed)
}
private fun setupToolbar() {
val parentActivity = riotActivity
if (parentActivity is ToolbarConfigurable) {
parentActivity.configure(toolbar)
}
}
private fun setupRecyclerView() {
val epoxyVisibilityTracker = EpoxyVisibilityTracker()
epoxyVisibilityTracker.attach(recyclerView)
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager)
recyclerView.layoutManager = layoutManager
recyclerView.setHasFixedSize(true)
timelineEventController.addModelBuildListener { it.dispatchTo(scrollOnNewMessageCallback) }
recyclerView.setController(timelineEventController)
timelineEventController.callback = this
}
private fun setupSendButton() {
sendButton.setOnClickListener {
val textMessage = composerEditText.text.toString()
if (textMessage.isNotBlank()) {
composerEditText.text = null
roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage))
}
}
}
private fun renderState(state: RoomDetailViewState) {
renderRoomSummary(state)
renderTimeline(state)
}
private fun renderTimeline(state: RoomDetailViewState) {
when (state.asyncTimelineData) {
is Success -> {
val timelineData = state.asyncTimelineData()
val lockAutoScroll = timelineData?.let {
it.events == timelineEventController.currentList && it.isLoadingForward
} ?: true
scrollOnNewMessageCallback.isLocked.set(lockAutoScroll)
timelineEventController.update(timelineData)
}
}
}
private fun renderRoomSummary(state: RoomDetailViewState) {
state.asyncRoomSummary()?.let {
toolbarTitleView.text = it.displayName
AvatarRenderer.render(it, toolbarAvatarImageView)
if (it.topic.isNotEmpty()) {
toolbarSubtitleView.visibility = View.VISIBLE
toolbarSubtitleView.text = it.topic
} else {
toolbarSubtitleView.visibility = View.GONE
}
}
}
// TimelineEventController.Callback ************************************************************
override fun onUrlClicked(url: String) {
homePermalinkHandler.launch(url)
}
override fun onEventVisible(event: TimelineEvent, index: Int) {
roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event, index))
}
}

View File

@ -1,141 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.riotredesign.features.home.room.detail
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.rx.rx
import im.vector.riotredesign.core.extensions.lastMinBy
import im.vector.riotredesign.core.platform.RiotViewModel
import im.vector.riotredesign.features.home.room.VisibleRoomHolder
import io.reactivex.rxkotlin.subscribeBy
import org.koin.android.ext.android.get
import timber.log.Timber
import java.util.concurrent.TimeUnit
class RoomDetailViewModel(initialState: RoomDetailViewState,
private val session: Session,
private val visibleRoomHolder: VisibleRoomHolder
) : RiotViewModel<RoomDetailViewState>(initialState), Room.Listener {
private val room = session.getRoom(initialState.roomId)!!
private val roomId = initialState.roomId
private val eventId = initialState.eventId
private val displayedEventsObservable = BehaviorRelay.create<RoomDetailActions.EventDisplayed>()
companion object : MvRxViewModelFactory<RoomDetailViewModel, RoomDetailViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: RoomDetailViewState): RoomDetailViewModel? {
val currentSession = Matrix.getInstance().currentSession
val visibleRoomHolder = viewModelContext.activity.get<VisibleRoomHolder>()
return RoomDetailViewModel(state, currentSession, visibleRoomHolder)
}
}
init {
observeRoomSummary()
observeTimeline()
observeDisplayedEvents()
room.loadRoomMembersIfNeeded()
room.addListener(this)
}
fun process(action: RoomDetailActions) {
when (action) {
is RoomDetailActions.SendMessage -> handleSendMessage(action)
is RoomDetailActions.IsDisplayed -> handleIsDisplayed()
is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action)
}
}
override fun onCleared() {
room.removeListener(this)
super.onCleared()
}
// Room.Listener *******************************************************************************
override fun onReadReceiptsUpdated() {
Timber.v("On read receipts updated")
}
// PRIVATE METHODS *****************************************************************************
private fun handleSendMessage(action: RoomDetailActions.SendMessage) {
room.sendTextMessage(action.text, callback = object : MatrixCallback<Event> {})
}
private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) {
displayedEventsObservable.accept(action)
}
private fun handleIsDisplayed() {
visibleRoomHolder.setVisibleRoom(roomId)
}
private fun observeDisplayedEvents() {
// We are buffering scroll events for one second
// and keep the most recent one to set the read receipt on.
displayedEventsObservable.hide()
.buffer(1, TimeUnit.SECONDS)
.filter { it.isNotEmpty() }
.subscribeBy(onNext = { actions ->
val eventIds = actions.mapNotNull { it.event.root.eventId }
withState { state ->
val newMapOfReadReceipts = HashMap(state.readReceiptsForEventId)
eventIds.forEach {
if (newMapOfReadReceipts.containsKey(it).not()) {
val readReceipts = room.readReceipts(it)
newMapOfReadReceipts[it] = readReceipts
}
}
setState { copy(readReceiptsForEventId = newMapOfReadReceipts) }
}
val mostRecentEvent = actions.lastMinBy { it.index }
mostRecentEvent?.event?.root?.eventId?.let { eventId ->
room.setReadReceipt(eventId, callback = object : MatrixCallback<Void> {})
}
})
.disposeOnClear()
}
private fun observeRoomSummary() {
room.rx().liveRoomSummary()
.execute { async ->
copy(asyncRoomSummary = async)
}
}
private fun observeTimeline() {
room.rx().timeline(eventId)
.execute { timelineData ->
copy(asyncTimelineData = timelineData)
}
}
}

View File

@ -1,49 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.riotredesign.features.home.room.detail.timeline
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.LayoutRes
import im.vector.riotredesign.core.epoxy.KotlinModel
import im.vector.riotredesign.features.home.AvatarRenderer
abstract class AbsMessageItem(private val informationData: MessageInformationData,
@LayoutRes layoutRes: Int
) : KotlinModel(layoutRes) {
protected abstract val avatarImageView: ImageView
protected abstract val memberNameView: TextView
protected abstract val timeView: TextView
override fun bind() {
if (informationData.showInformation) {
avatarImageView.visibility = View.VISIBLE
memberNameView.visibility = View.VISIBLE
timeView.visibility = View.VISIBLE
timeView.text = informationData.time
memberNameView.text = informationData.memberName
AvatarRenderer.render(informationData.avatarUrl, informationData.memberName?.toString(), avatarImageView)
} else {
avatarImageView.visibility = View.GONE
memberNameView.visibility = View.GONE
timeView.visibility = View.GONE
}
}
}

View File

@ -1,40 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.riotredesign.features.home.room.detail.timeline
import android.widget.ImageView
import android.widget.TextView
import im.vector.riotredesign.R
import im.vector.riotredesign.features.media.MediaContentRenderer
class MessageImageItem(
private val mediaData: MediaContentRenderer.Data,
informationData: MessageInformationData
) : AbsMessageItem(informationData, R.layout.item_timeline_event_image_message) {
override val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
override val memberNameView by bind<TextView>(R.id.messageMemberNameView)
override val timeView by bind<TextView>(R.id.messageTimeView)
private val imageView by bind<ImageView>(R.id.messageImageView)
override fun bind() {
super.bind()
MediaContentRenderer.render(mediaData, MediaContentRenderer.Mode.THUMBNAIL, imageView)
}
}

View File

@ -1,110 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.riotredesign.features.home.room.detail.timeline
import android.text.SpannableStringBuilder
import android.text.util.Linkify
import im.vector.matrix.android.api.permalinks.MatrixLinkify
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.core.extensions.localDateTime
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.riotredesign.features.media.MediaContentRenderer
class MessageItemFactory(private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val timelineDateFormatter: TimelineDateFormatter) {
private val messagesDisplayedWithInformation = HashSet<String?>()
fun create(event: TimelineEvent,
nextEvent: TimelineEvent?,
callback: TimelineEventController.Callback?
): AbsMessageItem? {
val roomMember = event.roomMember
val nextRoomMember = nextEvent?.roomMember
val date = event.root.localDateTime()
val nextDate = nextEvent?.root?.localDateTime()
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60))
?: false
if (addDaySeparator
|| nextRoomMember != roomMember
|| nextEvent?.root?.type != EventType.MESSAGE
|| isNextMessageReceivedMoreThanOneHourAgo) {
messagesDisplayedWithInformation.add(event.root.eventId)
}
val messageContent: MessageContent = event.root.content.toModel() ?: return null
val showInformation = messagesDisplayedWithInformation.contains(event.root.eventId)
val time = timelineDateFormatter.formatMessageHour(date)
val avatarUrl = roomMember?.avatarUrl
val memberName = roomMember?.displayName ?: event.root.sender
val informationData = MessageInformationData(time, avatarUrl, memberName, showInformation)
return when (messageContent) {
is MessageTextContent -> buildTextMessageItem(messageContent, informationData, callback)
is MessageImageContent -> buildImageMessageItem(messageContent, informationData)
else -> null
}
}
private fun buildImageMessageItem(messageContent: MessageImageContent,
informationData: MessageInformationData): MessageImageItem? {
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val data = MediaContentRenderer.Data(
url = messageContent.url,
height = messageContent.info?.height,
maxHeight = maxHeight,
width = messageContent.info?.width,
maxWidth = maxWidth,
rotation = messageContent.info?.rotation,
orientation = messageContent.info?.orientation
)
return MessageImageItem(data, informationData)
}
private fun buildTextMessageItem(messageContent: MessageTextContent,
informationData: MessageInformationData,
callback: TimelineEventController.Callback?): MessageTextItem? {
val message = messageContent.body.let {
val spannable = SpannableStringBuilder(it)
MatrixLinkify.addLinks(spannable, object : MatrixPermalinkSpan.Callback {
override fun onUrlClicked(url: String) {
callback?.onUrlClicked(url)
}
})
Linkify.addLinks(spannable, Linkify.ALL)
spannable
}
return MessageTextItem(
message = message,
informationData = informationData
)
}
}

View File

@ -1,39 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.riotredesign.features.home.room.detail.timeline
import android.widget.ImageView
import android.widget.TextView
import im.vector.matrix.android.api.permalinks.MatrixLinkify
import im.vector.riotredesign.R
class MessageTextItem(
val message: CharSequence? = null,
informationData: MessageInformationData
) : AbsMessageItem(informationData, R.layout.item_timeline_event_text_message) {
override val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
override val memberNameView by bind<TextView>(R.id.messageMemberNameView)
override val timeView by bind<TextView>(R.id.messageTimeView)
private val messageView by bind<TextView>(R.id.messageTextView)
override fun bind() {
super.bind()
messageView.text = message
MatrixLinkify.addLinkMovementMethod(messageView)
}
}

View File

@ -1,39 +0,0 @@
/*
*
* * Copyright 2019 New Vector Ltd
* *
* * 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 im.vector.riotredesign.features.home.room.detail.timeline
import android.widget.ImageView
import android.widget.TextView
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.KotlinModel
class ReadReceiptsItem() : KotlinModel(R.layout.item_timeline_read_receipts) {
private val moreText by bind<TextView>(R.id.message_more_than_expected)
private val avatarReceipt1 by bind<ImageView>(R.id.message_avatar_receipt_1)
private val avatarReceipt2 by bind<ImageView>(R.id.message_avatar_receipt_1)
private val avatarReceipt3 by bind<ImageView>(R.id.message_avatar_receipt_1)
private val avatarReceipt4 by bind<ImageView>(R.id.message_avatar_receipt_1)
private val avatarReceipt5 by bind<ImageView>(R.id.message_avatar_receipt_1)
private val avatarReceipts = listOf(avatarReceipt1, avatarReceipt2, avatarReceipt3, avatarReceipt4, avatarReceipt5)
override fun bind() {
}
}

View File

@ -1,114 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.riotredesign.features.home.room.detail.timeline
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyAsyncUtil
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.OnModelVisibilityStateChangedListener
import com.airbnb.epoxy.VisibilityState
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.timeline.TimelineData
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.core.epoxy.KotlinModel
import im.vector.riotredesign.core.extensions.localDateTime
import im.vector.riotredesign.features.home.LoadingItemModel_
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.riotredesign.features.home.room.detail.timeline.paging.PagedListEpoxyController
class TimelineEventController(private val roomId: String,
private val dateFormatter: TimelineDateFormatter,
private val timelineItemFactory: TimelineItemFactory,
private val timelineMediaSizeProvider: TimelineMediaSizeProvider
) : PagedListEpoxyController<TimelineEvent>(
EpoxyAsyncUtil.getAsyncBackgroundHandler(),
EpoxyAsyncUtil.getAsyncBackgroundHandler()
) {
init {
setFilterDuplicates(true)
}
private var isLoadingForward: Boolean = false
private var isLoadingBackward: Boolean = false
private var hasReachedEnd: Boolean = false
var callback: Callback? = null
fun update(timelineData: TimelineData?) {
timelineData?.let {
isLoadingForward = it.isLoadingForward
isLoadingBackward = it.isLoadingBackward
hasReachedEnd = it.events.lastOrNull()?.root?.type == EventType.STATE_ROOM_CREATE
submitList(it.events)
requestModelBuild()
}
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
timelineMediaSizeProvider.recyclerView = recyclerView
}
override fun buildItemModels(currentPosition: Int, items: List<TimelineEvent?>): List<EpoxyModel<*>> {
if (items.isNullOrEmpty()) {
return emptyList()
}
val epoxyModels = ArrayList<EpoxyModel<*>>()
val event = items[currentPosition] ?: return emptyList()
val nextEvent = if (currentPosition + 1 < items.size) items[currentPosition + 1] else null
val date = event.root.localDateTime()
val nextDate = nextEvent?.root?.localDateTime()
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
timelineItemFactory.create(event, nextEvent, callback)?.also {
it.id(event.localId)
it.setOnVisibilityStateChanged(OnModelVisibilityStateChangedListener<KotlinModel, View> { model, view, visibilityState ->
if (visibilityState == VisibilityState.VISIBLE) {
callback?.onEventVisible(event, currentPosition)
}
})
epoxyModels.add(it)
}
if (addDaySeparator) {
val formattedDay = dateFormatter.formatMessageDay(date)
val daySeparatorItem = DaySeparatorItem(formattedDay).id(roomId + formattedDay)
epoxyModels.add(daySeparatorItem)
}
return epoxyModels
}
override fun addModels(models: List<EpoxyModel<*>>) {
LoadingItemModel_()
.id(roomId + "forward_loading_item")
.addIf(isLoadingForward, this)
super.add(models)
LoadingItemModel_()
.id(roomId + "backward_loading_item")
.addIf(!hasReachedEnd, this)
}
interface Callback {
fun onEventVisible(event: TimelineEvent, index: Int)
fun onUrlClicked(url: String)
}
}

View File

@ -1,42 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.riotredesign.features.home.room.detail.timeline
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.core.epoxy.KotlinModel
class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
private val roomNameItemFactory: RoomNameItemFactory,
private val roomTopicItemFactory: RoomTopicItemFactory,
private val roomMemberItemFactory: RoomMemberItemFactory,
private val defaultItemFactory: DefaultItemFactory) {
fun create(event: TimelineEvent,
nextEvent: TimelineEvent?,
callback: TimelineEventController.Callback?): KotlinModel? {
return when (event.root.type) {
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, callback)
EventType.STATE_ROOM_NAME -> roomNameItemFactory.create(event)
EventType.STATE_ROOM_TOPIC -> roomTopicItemFactory.create(event)
EventType.STATE_ROOM_MEMBER -> roomMemberItemFactory.create(event)
else -> defaultItemFactory.create(event)
}
}
}

View File

@ -1,129 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.riotredesign.features.home.room.detail.timeline.paging
import androidx.paging.PagedList
import android.os.Handler
import androidx.recyclerview.widget.DiffUtil
import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.EpoxyViewHolder
/**
* An [EpoxyController] that can work with a [PagedList].
*
* Internally, it caches the model for each item in the [PagedList]. You should override
* [buildItemModel] method to build the model for the given item. Since [PagedList] might include
* `null` items if placeholders are enabled, this method needs to handle `null` values in the list.
*
* By default, the model for each item is added to the model list. To change this behavior (to
* filter items or inject extra items), you can override [addModels] function and manually add built
* models.
*
* @param T The type of the items in the [PagedList].
*/
abstract class PagedListEpoxyController<T>(
/**
* The handler to use for building models. By default this uses the main thread, but you can use
* [EpoxyAsyncUtil.getAsyncBackgroundHandler] to do model building in the background.
*
* The notify thread of your PagedList (from setNotifyExecutor in the PagedList Builder) must be
* the same as this thread. Otherwise Epoxy will crash.
*/
modelBuildingHandler: Handler = EpoxyController.defaultModelBuildingHandler,
/**
* The handler to use when calculating the diff between built model lists.
* By default this uses the main thread, but you can use
* [EpoxyAsyncUtil.getAsyncBackgroundHandler] to do diffing in the background.
*/
diffingHandler: Handler = EpoxyController.defaultDiffingHandler,
/**
* [PagedListEpoxyController] uses an [DiffUtil.ItemCallback] to detect changes between
* [PagedList]s. By default, it relies on simple object equality but you can provide a custom
* one if you don't use all fields in the object in your models.
*/
itemDiffCallback: DiffUtil.ItemCallback<T> = DEFAULT_ITEM_DIFF_CALLBACK as DiffUtil.ItemCallback<T>
) : EpoxyController(modelBuildingHandler, diffingHandler) {
// this is where we keep the already built models
protected val modelCache = PagedListModelCache(
modelBuilder = { pos, item ->
buildItemModels(pos, item)
},
rebuildCallback = {
requestModelBuild()
},
itemDiffCallback = itemDiffCallback,
modelBuildingHandler = modelBuildingHandler
)
var currentList: PagedList<T>? = null
private set
final override fun buildModels() {
addModels(modelCache.getModels())
}
override fun onModelBound(
holder: EpoxyViewHolder,
boundModel: EpoxyModel<*>,
position: Int,
previouslyBoundModel: EpoxyModel<*>?
) {
modelCache.loadAround(boundModel)
}
/**
* This function adds all built models to the adapter. You can override this method to add extra
* items into the model list or remove some.
*/
open fun addModels(models: List<EpoxyModel<*>>) {
super.add(models)
}
/**
* Builds the model for a given item. This must return a single model for each item. If you want
* to inject headers etc, you can override [addModels] function.
*
* If the `item` is `null`, you should provide the placeholder. If your [PagedList] is configured
* without placeholders, you don't need to handle the `null` case.
*/
abstract fun buildItemModels(currentPosition: Int, items: List<T?>): List<EpoxyModel<*>>
/**
* Submit a new paged list.
*
* A diff will be calculated between this list and the previous list so you may still get calls
* to [buildItemModel] with items from the previous list.
*/
fun submitList(newList: PagedList<T>?) {
currentList = newList
modelCache.submitList(newList)
}
companion object {
/**
* [PagedListEpoxyController] calculates a diff on top of the PagedList to check which
* models are invalidated.
* This is the default [DiffUtil.ItemCallback] which uses object equality.
*/
val DEFAULT_ITEM_DIFF_CALLBACK = object : DiffUtil.ItemCallback<Any>() {
override fun areItemsTheSame(oldItem: Any, newItem: Any) = oldItem == newItem
override fun areContentsTheSame(oldItem: Any, newItem: Any) = oldItem == newItem
}
}
}

View File

@ -1,149 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.riotredesign.features.home.room.detail.timeline.paging
import android.annotation.SuppressLint
import android.os.Handler
import androidx.paging.AsyncPagedListDiffer
import androidx.paging.PagedList
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListUpdateCallback
import com.airbnb.epoxy.EpoxyModel
import java.util.concurrent.Executor
import java.util.concurrent.atomic.AtomicBoolean
/**
* A PagedList stream wrapper that caches models built for each item. It tracks changes in paged lists and caches
* models for each item when they are invalidated to avoid rebuilding models for the whole list when PagedList is
* updated.
*/
class PagedListModelCache<T>(
private val modelBuilder: (itemIndex: Int, items: List<T>) -> List<EpoxyModel<*>>,
private val rebuildCallback: () -> Unit,
private val itemDiffCallback: DiffUtil.ItemCallback<T>,
private val diffExecutor: Executor? = null,
private val modelBuildingHandler: Handler
) {
// Int is the index of the pagedList item
// We have to be able to find the pagedlist position coming from an epoxy model to trigger
// LoadAround with accuracy
private val modelCache = linkedMapOf<EpoxyModel<*>, Int>()
private var isCacheStale = AtomicBoolean(true)
/**
* Tracks the last accessed position so that we can report it back to the paged list when models are built.
*/
private var lastPosition: Int? = null
/**
* Observer for the PagedList changes that invalidates the model cache when data is updated.
*/
private val updateCallback = object : ListUpdateCallback {
override fun onChanged(position: Int, count: Int, payload: Any?) {
invalidate()
rebuildCallback()
}
override fun onMoved(fromPosition: Int, toPosition: Int) {
invalidate()
rebuildCallback()
}
override fun onInserted(position: Int, count: Int) {
invalidate()
rebuildCallback()
}
override fun onRemoved(position: Int, count: Int) {
invalidate()
rebuildCallback()
}
}
@SuppressLint("RestrictedApi")
private val asyncDiffer = AsyncPagedListDiffer<T>(
updateCallback,
AsyncDifferConfig.Builder<T>(
itemDiffCallback
).also { builder ->
if (diffExecutor != null) {
builder.setBackgroundThreadExecutor(diffExecutor)
}
// we have to reply on this private API, otherwise, paged list might be changed when models are being built,
// potentially creating concurrent modification problems.
builder.setMainThreadExecutor { runnable: Runnable ->
modelBuildingHandler.post(runnable)
}
}.build()
)
fun submitList(pagedList: PagedList<T>?) {
asyncDiffer.submitList(pagedList)
}
fun getModels(): List<EpoxyModel<*>> {
if (isCacheStale.compareAndSet(true, false)) {
asyncDiffer.currentList?.forEachIndexed { position, _ ->
buildModel(position)
}
}
lastPosition?.let {
triggerLoadAround(it)
}
return modelCache.keys.toList()
}
fun loadAround(model: EpoxyModel<*>) {
modelCache[model]?.let { itemPosition ->
triggerLoadAround(itemPosition)
lastPosition = itemPosition
}
}
// PRIVATE METHODS *****************************************************************************
private fun invalidate() {
modelCache.clear()
isCacheStale.set(true)
}
private fun cacheModelsAtPosition(itemPosition: Int, epoxyModels: Set<EpoxyModel<*>>) {
epoxyModels.forEach {
modelCache[it] = itemPosition
}
}
private fun buildModel(pos: Int) {
if (pos >= asyncDiffer.currentList?.size ?: 0) {
return
}
modelBuilder(pos, asyncDiffer.currentList as List<T>).also {
cacheModelsAtPosition(pos, it.toSet())
}
}
private fun triggerLoadAround(position: Int) {
asyncDiffer.currentList?.let {
if (it.size > 0) {
it.loadAround(Math.min(position, it.size - 1))
}
}
}
}

View File

@ -1,52 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.riotredesign.features.home.room.list
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.KotlinModel
data class RoomCategoryItem(
val title: CharSequence,
val isExpanded: Boolean,
val unreadCount: Int,
val showHighlighted: Boolean,
val listener: (() -> Unit)? = null
) : KotlinModel(R.layout.item_room_category) {
private val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.roomCategoryUnreadCounterBadgeView)
private val titleView by bind<TextView>(R.id.roomCategoryTitleView)
private val rootView by bind<ViewGroup>(R.id.roomCategoryRootView)
private val tintColor by lazy {
ContextCompat.getColor(rootView.context, R.color.bluey_grey_two)
}
override fun bind() {
val expandedArrowDrawableRes = if (isExpanded) R.drawable.ic_expand_more_white else R.drawable.ic_expand_less_white
val expandedArrowDrawable = ContextCompat.getDrawable(rootView.context, expandedArrowDrawableRes)?.also {
DrawableCompat.setTint(it, tintColor)
}
unreadCounterBadgeView.render(unreadCount, showHighlighted)
titleView.setCompoundDrawablesWithIntrinsicBounds(expandedArrowDrawable, null, null, null)
titleView.text = title
rootView.setOnClickListener { listener?.invoke() }
}
}

View File

@ -1,39 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.riotredesign.features.home.room.list
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.room.model.RoomSummary
data class RoomListViewState(
val asyncRooms: Async<RoomSummaries> = Uninitialized,
val selectedRoomId: String? = null
) : MvRxState
data class RoomSummaries(
val favourites: List<RoomSummary>,
val directRooms: List<RoomSummary>,
val groupRooms: List<RoomSummary>,
val lowPriorities: List<RoomSummary>,
val serverNotices: List<RoomSummary>
)
fun RoomSummaries?.isNullOrEmpty(): Boolean {
return this == null || (directRooms.isEmpty() && groupRooms.isEmpty() && favourites.isEmpty() && lowPriorities.isEmpty() && serverNotices.isEmpty())
}

View File

@ -1,124 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.riotredesign.features.home.room.list
import androidx.annotation.StringRes
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringProvider
class RoomSummaryController(private val stringProvider: StringProvider
) : TypedEpoxyController<RoomListViewState>() {
private var isFavoriteRoomsExpanded = true
private var isDirectRoomsExpanded = false
private var isGroupRoomsExpanded = false
private var isLowPriorityRoomsExpanded = false
private var isServerNoticeRoomsExpanded = false
var callback: Callback? = null
override fun buildModels(viewState: RoomListViewState) {
val roomSummaries = viewState.asyncRooms()
val favourites = roomSummaries?.favourites ?: emptyList()
buildRoomCategory(viewState, favourites, R.string.room_list_favourites, isFavoriteRoomsExpanded) {
isFavoriteRoomsExpanded = !isFavoriteRoomsExpanded
}
if (isFavoriteRoomsExpanded) {
buildRoomModels(favourites, viewState.selectedRoomId)
}
val directRooms = roomSummaries?.directRooms ?: emptyList()
buildRoomCategory(viewState, directRooms, R.string.room_list_direct, isDirectRoomsExpanded) {
isDirectRoomsExpanded = !isDirectRoomsExpanded
}
if (isDirectRoomsExpanded) {
buildRoomModels(directRooms, viewState.selectedRoomId)
}
val groupRooms = roomSummaries?.groupRooms ?: emptyList()
buildRoomCategory(viewState, groupRooms, R.string.room_list_group, isGroupRoomsExpanded) {
isGroupRoomsExpanded = !isGroupRoomsExpanded
}
if (isGroupRoomsExpanded) {
buildRoomModels(groupRooms, viewState.selectedRoomId)
}
val lowPriorities = roomSummaries?.lowPriorities ?: emptyList()
buildRoomCategory(viewState, lowPriorities, R.string.room_list_low_priority, isLowPriorityRoomsExpanded) {
isLowPriorityRoomsExpanded = !isLowPriorityRoomsExpanded
}
if (isLowPriorityRoomsExpanded) {
buildRoomModels(lowPriorities, viewState.selectedRoomId)
}
val serverNotices = roomSummaries?.serverNotices ?: emptyList()
buildRoomCategory(viewState, serverNotices, R.string.room_list_system_alert, isServerNoticeRoomsExpanded) {
isServerNoticeRoomsExpanded = !isServerNoticeRoomsExpanded
}
if (isServerNoticeRoomsExpanded) {
buildRoomModels(serverNotices, viewState.selectedRoomId)
}
}
private fun buildRoomCategory(viewState: RoomListViewState, summaries: List<RoomSummary>, @StringRes titleRes: Int, isExpanded: Boolean, mutateExpandedState: () -> Unit) {
//TODO should add some business logic later
val unreadCount = if (summaries.isEmpty()) {
0
} else {
summaries.map { it.notificationCount }.reduce { acc, i -> acc + i }
}
val showHighlighted = summaries.any { it.highlightCount > 0 }
RoomCategoryItem(
title = stringProvider.getString(titleRes).toUpperCase(),
isExpanded = isExpanded,
unreadCount = unreadCount,
showHighlighted = showHighlighted,
listener = {
mutateExpandedState()
setData(viewState)
}
)
.id(titleRes)
.addTo(this)
}
private fun buildRoomModels(summaries: List<RoomSummary>, selectedRoomId: String?) {
summaries.forEach { roomSummary ->
val unreadCount = roomSummary.notificationCount
val showHighlighted = roomSummary.highlightCount > 0
val isSelected = roomSummary.roomId == selectedRoomId
RoomSummaryItem(
roomName = roomSummary.displayName,
avatarUrl = roomSummary.avatarUrl,
isSelected = isSelected,
showHighlighted = showHighlighted,
unreadCount = unreadCount,
listener = { callback?.onRoomSelected(roomSummary) }
)
.id(roomSummary.roomId)
.addTo(this)
}
}
interface Callback {
fun onRoomSelected(room: RoomSummary)
}
}

View File

@ -1,48 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.riotredesign.features.home.room.list
import android.widget.ImageView
import android.widget.TextView
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.KotlinModel
import im.vector.riotredesign.core.platform.CheckableFrameLayout
import im.vector.riotredesign.features.home.AvatarRenderer
data class RoomSummaryItem(
val roomName: CharSequence,
val avatarUrl: String?,
val isSelected: Boolean,
val unreadCount: Int,
val showHighlighted: Boolean,
val listener: (() -> Unit)? = null
) : KotlinModel(R.layout.item_room) {
private val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.roomUnreadCounterBadgeView)
private val titleView by bind<TextView>(R.id.roomNameView)
private val avatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
private val rootView by bind<CheckableFrameLayout>(R.id.itemRoomLayout)
override fun bind() {
unreadCounterBadgeView.render(unreadCount, showHighlighted)
rootView.isChecked = isSelected
rootView.setOnClickListener { listener?.invoke() }
titleView.text = roomName
AvatarRenderer.render(avatarUrl, roomName.toString(), avatarImageView)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@ -1,64 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<ImageView
android:id="@+id/messageAvatarImageView"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/messageMemberNameView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="64dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textSize="15sp"
app:layout_constraintBottom_toTopOf="@+id/toolbarSubtitleView"
app:layout_constraintEnd_toStartOf="@+id/messageTimeView"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/full_names" />
<TextView
android:id="@+id/messageTimeView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textColor="@color/brown_grey"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintTop_toTopOf="@id/messageMemberNameView"
tools:text="@tools:sample/date/hhmm" />
<ImageView
android:id="@+id/messageImageView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="64dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/messageMemberNameView" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,63 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<ImageView
android:id="@+id/messageAvatarImageView"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/messageMemberNameView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="64dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textSize="15sp"
app:layout_constraintBottom_toTopOf="@+id/toolbarSubtitleView"
app:layout_constraintEnd_toStartOf="@+id/messageTimeView"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/full_names" />
<TextView
android:id="@+id/messageTimeView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textColor="@color/brown_grey"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintTop_toTopOf="@id/messageMemberNameView"
tools:text="@tools:sample/date/hhmm" />
<TextView
android:id="@+id/messageTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="64dp"
android:layout_marginBottom="8dp"
android:textColor="@color/dark_grey"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/messageMemberNameView"
tools:text="Alright finished work, heading there in about 20 mins…Ping me when youre outside" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,74 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/read_receipt_avatars_list"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_marginEnd="12dp"
android:layout_marginRight="12dp"
android:orientation="horizontal">
<TextView
android:id="@+id/message_more_than_expected"
android:layout_width="20dp"
android:layout_height="match_parent"
android:gravity="center"
android:textSize="9sp"
tools:text="999+" />
<ImageView
android:id="@+id/message_avatar_receipt_5"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginEnd="6dp"
android:layout_marginRight="6dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop" />
<ImageView
android:id="@+id/message_avatar_receipt_4"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginEnd="6dp"
android:layout_marginRight="6dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop" />
<ImageView
android:id="@+id/message_avatar_receipt_3"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginEnd="6dp"
android:layout_marginRight="6dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop" />
<ImageView
android:id="@+id/message_avatar_receipt_2"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginEnd="6dp"
android:layout_marginRight="6dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop" />
<ImageView
android:id="@+id/message_avatar_receipt_1"
android:layout_width="19dp"
android:layout_height="12dp"
android:layout_marginEnd="6dp"
android:layout_marginRight="6dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop" />
</LinearLayout>
</RelativeLayout>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Base.V21.Theme.Riot" parent="Base.V1.Theme.Riot"></style>
<style name="Base.Theme.Riot" parent="Base.V21.Theme.Riot" />
</resources>

View File

@ -1,15 +0,0 @@
<resources>
<string name="app_name">"Riot X"</string>
<string name="global_retry">"Retry"</string>
<string name="error_no_network">"No network connection"</string>
<string name="error_common">"An error occurred"</string>
<string name="room_list_empty">"Join a room to start using the app."</string>
<string name="room_list_favourites">"Favourites"</string>
<string name="room_list_direct">"People"</string>
<string name="room_list_group">"Rooms"</string>
<string name="room_list_low_priority">"Low priority"</string>
<string name="room_list_system_alert">"System Alerts"</string>
</resources>

View File

@ -1,4 +0,0 @@
<resources>
</resources>

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Widget.Button" parent="Widget.AppCompat.Button">
<item name="android:minHeight">48dp</item>
<item name="android:background">?attr/colorAccent</item>
<item name="android:textColor">@android:color/white</item>
</style>
</resources>

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Riot" parent="Base.Theme.Riot">
</style>
<style name="Theme.Riot.Splash">
<item name="android:windowBackground">@drawable/bg_splash</item>
</style>
</resources>

View File

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Base.V1.Theme.Riot" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">@color/dark</item>
<item name="colorPrimaryDark">@color/dark</item>
<item name="colorAccent">@color/pale_teal</item>
<item name="buttonStyle">@style/Widget.Button</item>
</style>
<style name="Base.Theme.Riot" parent="Base.V1.Theme.Riot" />
</resources>

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@ -1,16 +1,21 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.3.11'
ext.kotlin_version = '1.3.21'
ext.koin_version = '1.0.2'
repositories {
google()
jcenter()
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.0'
classpath 'com.android.tools.build:gradle:3.3.2'
classpath 'com.google.gms:google-services:4.2.0'
classpath "com.airbnb.okreplay:gradle-plugin:1.4.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.6.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@ -19,12 +24,42 @@ buildscript {
allprojects {
repositories {
maven { url "http://dl.bintray.com/piasy/maven" }
maven { url 'https://jitpack.io' }
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
google()
jcenter()
maven { url 'https://jitpack.io' }
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
apply plugin: 'org.sonarqube'
sonarqube {
properties {
property "sonar.projectName", "RiotX-Android"
property "sonar.projectKey", "vector.android.riotx"
property "sonar.host.url", "https://sonarcloud.io"
property "sonar.projectVersion", project(":vector").android.defaultConfig.versionName
property "sonar.sourceEncoding", "UTF-8"
property "sonar.links.homepage", "https://github.com/vector-im/riotX-android/"
property "sonar.links.ci", "https://buildkite.com/matrix-dot-org/riotx-android"
property "sonar.links.scm", "https://github.com/vector-im/riotX-android/"
property "sonar.links.issue", "https://github.com/vector-im/riotX-android/issues"
property "sonar.organization", "new_vector_ltd_organization"
property "sonar.login", project.hasProperty("SONAR_LOGIN") ? SONAR_LOGIN : "invalid"
}
}
project(":vector") {
sonarqube {
properties {
property "sonar.sources", project(":vector").android.sourceSets.main.java.srcDirs
// exclude source code from analyses separated by a colon (:)
// property "sonar.exclusions", "**/*.*"
}
}
}

Binary file not shown.

View File

@ -1,6 +1,6 @@
#Wed Jan 16 18:28:13 CET 2019
#Tue Mar 19 09:53:05 CET 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip

2
gradlew vendored
View File

@ -28,7 +28,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
DEFAULT_JVM_OPTS='"-Xmx64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"

2
gradlew.bat vendored
View File

@ -14,7 +14,7 @@ set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DEFAULT_JVM_OPTS="-Xmx64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

View File

@ -1,4 +1,3 @@
/*
* Copyright 2019 New Vector Ltd
*
@ -18,9 +17,7 @@
package im.vector.matrix.rx
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.model.ReadReceipt
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.timeline.TimelineData
import io.reactivex.Observable
class RxRoom(private val room: Room) {
@ -29,12 +26,8 @@ class RxRoom(private val room: Room) {
return room.roomSummary.asObservable()
}
fun timeline(eventId: String? = null): Observable<TimelineData> {
return room.timeline(eventId).asObservable()
}
fun liveReadReceipts(): Observable<List<ReadReceipt>> {
return room.readReceipts().asObservable()
fun liveRoomMemberIds(): Observable<List<String>> {
return room.getRoomMemberIdsLive().asObservable()
}
}

View File

@ -1,6 +1,7 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'realm-android'
apply plugin: 'okreplay'
@ -19,6 +20,10 @@ repositories {
jcenter()
}
androidExtensions {
experimental = true
}
android {
compileSdkVersion 28
testOptions.unitTests.includeAndroidResources = true
@ -28,12 +33,28 @@ android {
targetSdkVersion 28
versionCode 1
versionName "1.0"
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "git_sdk_revision", "\"${gitRevision()}\""
resValue "string", "git_sdk_revision_unix_date", "\"${gitRevisionUnixDate()}\""
resValue "string", "git_sdk_revision_date", "\"${gitRevisionDate()}\""
}
buildTypes {
debug {
// Set to true to log privacy or sensible data, such as token
buildConfigField "boolean", "LOG_PRIVATE_DATA", "false"
// Set to BODY instead of NONE to enable logging
buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.NONE"
}
release {
buildConfigField "boolean", "LOG_PRIVATE_DATA", "false"
buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.NONE"
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
@ -42,12 +63,31 @@ android {
adbOptions {
installOptions "-g"
}
lintOptions {
lintConfig file("lint.xml")
}
}
static def gitRevision() {
def cmd = "git rev-parse --short HEAD"
return cmd.execute().text.trim()
}
static def gitRevisionUnixDate() {
def cmd = "git show -s --format=%ct HEAD^{commit}"
return cmd.execute().text.trim()
}
static def gitRevisionDate() {
def cmd = "git show -s --format=%ci HEAD^{commit}"
return cmd.execute().text.trim()
}
dependencies {
def arrow_version = "0.8.0"
def support_version = '1.1.0-alpha01'
def support_version = '1.1.0-alpha03'
def moshi_version = '1.8.0'
def lifecycle_version = '2.0.0'
def coroutines_version = "1.0.1"
@ -66,7 +106,7 @@ dependencies {
// Network
implementation 'com.squareup.retrofit2:retrofit:2.4.0'
implementation 'com.squareup.retrofit2:converter-moshi:2.4.0'
implementation 'com.squareup.okhttp3:okhttp:3.10.0'
implementation 'com.squareup.okhttp3:okhttp:3.11.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'
implementation 'com.novoda:merlin:1.1.6'
implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
@ -76,11 +116,8 @@ dependencies {
implementation 'com.github.Zhuinden:realm-monarchy:0.5.1'
kapt 'dk.ilios:realmfieldnameshelper:1.1.1'
// Paging
implementation 'androidx.paging:paging-runtime:2.0.0'
// Work
implementation "android.arch.work:work-runtime-ktx:1.0.0-beta02"
implementation "android.arch.work:work-runtime-ktx:1.0.0"
// FP
implementation "io.arrow-kt:arrow-core:$arrow_version"
@ -95,6 +132,7 @@ dependencies {
// Logging
implementation 'com.jakewharton.timber:timber:4.7.1'
implementation 'com.facebook.stetho:stetho-okhttp3:1.5.0'
debugImplementation 'com.airbnb.okreplay:okreplay:1.4.0'
releaseImplementation 'com.airbnb.okreplay:noop:1.4.0'
@ -103,20 +141,20 @@ dependencies {
testImplementation 'junit:junit:4.12'
testImplementation 'org.robolectric:robolectric:4.0.2'
testImplementation "org.koin:koin-test:$koin_version"
testImplementation 'org.robolectric:shadows-support-v4:3.0'
//testImplementation 'org.robolectric:shadows-support-v4:3.0'
testImplementation "io.mockk:mockk:1.8.13.kotlin13"
testImplementation 'org.amshove.kluent:kluent-android:1.44'
testImplementation "androidx.arch.core:core-testing:$lifecycle_version"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
androidTestImplementation "org.koin:koin-test:$koin_version"
androidTestImplementation 'androidx.test:core:1.1.0'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test:rules:1.1.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
androidTestImplementation 'org.amshove.kluent:kluent-android:1.44'
androidTestImplementation "io.mockk:mockk-android:1.8.13.kotlin13"
androidTestImplementation "androidx.arch.core:core-testing:$lifecycle_version"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
}

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<!-- Modify some severity -->
<!-- Resource -->
<issue id="MissingTranslation" severity="warning" />
<issue id="TypographyEllipsis" severity="error" />
<issue id="ImpliedQuantity" severity="warning" />
<!-- UX -->
<issue id="ButtonOrder" severity="error" />
<!-- Layout -->
<issue id="UnknownIdInLayout" severity="error" />
<issue id="StringFormatCount" severity="error" />
<issue id="HardcodedText" severity="error" />
<issue id="SpUsage" severity="error" />
<issue id="ObsoleteLayoutParam" severity="error" />
<issue id="InefficientWeight" severity="error" />
<issue id="DisableBaselineAlignment" severity="error" />
<issue id="ScrollViewSize" severity="error" />
<!-- RTL -->
<issue id="RtlEnabled" severity="error" />
<issue id="RtlHardcoded" severity="error" />
<issue id="RtlSymmetry" severity="error" />
<!-- Code -->
<issue id="SetTextI18n" severity="error" />
<issue id="ViewConstructor" severity="error" />
<issue id="UseValueOf" severity="error" />
</lint>

View File

@ -16,10 +16,9 @@
package im.vector.matrix.android.session.room.timeline
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.database.helper.add
import im.vector.matrix.android.internal.database.helper.addAll
import im.vector.matrix.android.internal.database.helper.isUnlinked
@ -27,6 +26,9 @@ import im.vector.matrix.android.internal.database.helper.lastStateIndex
import im.vector.matrix.android.internal.database.helper.merge
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeListOfEvents
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeMessageEvent
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeRoomMemberEvent
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.kotlin.createObject
@ -35,9 +37,10 @@ import org.amshove.kluent.shouldBeTrue
import org.amshove.kluent.shouldEqual
import org.junit.Before
import org.junit.Test
import kotlin.random.Random
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
internal class ChunkEntityTest : InstrumentedTest {
private lateinit var monarchy: Monarchy
@ -54,7 +57,7 @@ internal class ChunkEntityTest : InstrumentedTest {
fun add_shouldAdd_whenNotAlreadyIncluded() {
monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeEvent(false)
val fakeEvent = createFakeMessageEvent()
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
chunk.events.size shouldEqual 1
}
@ -64,7 +67,7 @@ internal class ChunkEntityTest : InstrumentedTest {
fun add_shouldNotAdd_whenAlreadyIncluded() {
monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeEvent(false)
val fakeEvent = createFakeMessageEvent()
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
chunk.events.size shouldEqual 1
@ -75,7 +78,7 @@ internal class ChunkEntityTest : InstrumentedTest {
fun add_shouldStateIndexIncremented_whenStateEventIsAddedForward() {
monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeEvent(true)
val fakeEvent = createFakeRoomMemberEvent()
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 1
}
@ -85,7 +88,7 @@ internal class ChunkEntityTest : InstrumentedTest {
fun add_shouldStateIndexNotIncremented_whenNoStateEventIsAdded() {
monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeEvent(false)
val fakeEvent = createFakeMessageEvent()
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 0
}
@ -134,13 +137,13 @@ internal class ChunkEntityTest : InstrumentedTest {
val chunk2: ChunkEntity = realm.createObject()
val eventsForChunk1 = createFakeListOfEvents(30)
val eventsForChunk2 = eventsForChunk1 + createFakeListOfEvents(10)
chunk1.isLast = true
chunk2.isLast = false
chunk1.isLastForward = true
chunk2.isLastForward = false
chunk1.addAll("roomId", eventsForChunk1, PaginationDirection.FORWARDS)
chunk2.addAll("roomId", eventsForChunk2, PaginationDirection.BACKWARDS)
chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
chunk1.events.size shouldEqual 40
chunk1.isLast.shouldBeTrue()
chunk1.isLastForward.shouldBeTrue()
}
}
@ -196,15 +199,4 @@ internal class ChunkEntityTest : InstrumentedTest {
}
}
private fun createFakeListOfEvents(size: Int = 10): List<Event> {
return (0 until size).map { createFakeEvent(Random.nextBoolean()) }
}
private fun createFakeEvent(asStateEvent: Boolean = false): Event {
val eventId = Random.nextLong(System.currentTimeMillis()).toString()
val type = if (asStateEvent) EventType.STATE_ROOM_NAME else EventType.MESSAGE
return Event(type, eventId)
}
}

View File

@ -25,7 +25,7 @@ import kotlin.random.Random
internal class FakeGetContextOfEventTask(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : GetContextOfEventTask {
override fun execute(params: GetContextOfEventTask.Params): Try<TokenChunkEvent> {
override fun execute(params: GetContextOfEventTask.Params): Try<TokenChunkEventPersistor.Result> {
val fakeEvents = RoomDataHelper.createFakeListOfEvents(30)
val tokenChunkEvent = FakeTokenChunkEvent(
Random.nextLong(System.currentTimeMillis()).toString(),
@ -33,7 +33,6 @@ internal class FakeGetContextOfEventTask(private val tokenChunkEventPersistor: T
fakeEvents
)
return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, PaginationDirection.BACKWARDS)
.map { tokenChunkEvent }
}

View File

@ -18,17 +18,15 @@ package im.vector.matrix.android.session.room.timeline
import arrow.core.Try
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEvent
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
import kotlin.random.Random
internal class FakePaginationTask(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : PaginationTask {
override fun execute(params: PaginationTask.Params): Try<TokenChunkEvent> {
override fun execute(params: PaginationTask.Params): Try<TokenChunkEventPersistor.Result> {
val fakeEvents = RoomDataHelper.createFakeListOfEvents(30)
val tokenChunkEvent = FakeTokenChunkEvent(params.from, Random.nextLong(System.currentTimeMillis()).toString(), fakeEvents)
return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, params.direction)
.map { tokenChunkEvent }
}
}

View File

@ -17,9 +17,14 @@
package im.vector.matrix.android.session.room.timeline
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.model.MyMembership
import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.internal.database.helper.addAll
import im.vector.matrix.android.internal.database.helper.addOrUpdate
import im.vector.matrix.android.internal.database.model.ChunkEntity
@ -30,27 +35,56 @@ import kotlin.random.Random
object RoomDataHelper {
private const val FAKE_TEST_SENDER = "@sender:test.org"
private val EVENT_FACTORIES = hashMapOf(
0 to { createFakeMessageEvent() },
1 to { createFakeRoomMemberEvent() }
)
fun createFakeListOfEvents(size: Int = 10): List<Event> {
return (0 until size).map { createFakeEvent(Random.nextBoolean()) }
return (0 until size).mapNotNull {
val nextInt = Random.nextInt(EVENT_FACTORIES.size)
EVENT_FACTORIES[nextInt]?.invoke()
}
}
fun createFakeEvent(asStateEvent: Boolean = false): Event {
val eventId = Random.nextLong(System.currentTimeMillis()).toString()
val type = if (asStateEvent) EventType.STATE_ROOM_NAME else EventType.MESSAGE
return Event(type, eventId)
fun createFakeEvent(type: String,
content: Content? = null,
prevContent: Content? = null,
sender: String = FAKE_TEST_SENDER,
stateKey: String = FAKE_TEST_SENDER
): Event {
return Event(
type = type,
eventId = Random.nextLong().toString(),
content = content,
prevContent = prevContent,
sender = sender,
stateKey = stateKey
)
}
fun createFakeMessageEvent(): Event {
val message = MessageTextContent(MessageType.MSGTYPE_TEXT, "Fake message #${Random.nextLong()}").toContent()
return createFakeEvent(EventType.MESSAGE, message)
}
fun createFakeRoomMemberEvent(): Event {
val roomMember = RoomMember(Membership.JOIN, "Fake name #${Random.nextLong()}").toContent()
return createFakeEvent(EventType.STATE_ROOM_MEMBER, roomMember)
}
fun fakeInitialSync(monarchy: Monarchy, roomId: String) {
monarchy.runTransactionSync { realm ->
val roomEntity = realm.createObject<RoomEntity>(roomId)
roomEntity.membership = MyMembership.JOINED
val eventList = createFakeListOfEvents(30)
roomEntity.membership = Membership.JOIN
val eventList = createFakeListOfEvents(10)
val chunkEntity = realm.createObject<ChunkEntity>().apply {
nextToken = null
prevToken = Random.nextLong(System.currentTimeMillis()).toString()
isLast = true
isLastForward = true
}
chunkEntity.addAll("roomId", eventList, PaginationDirection.FORWARDS)
chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS)
roomEntity.addOrUpdate(chunkEntity)
}
}

View File

@ -1,77 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.matrix.android.session.room.timeline
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.annotation.UiThreadTest
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.LiveDataTestObserver
import im.vector.matrix.android.MainThreadExecutor
import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService
import im.vector.matrix.android.internal.session.room.timeline.TimelineBoundaryCallback
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.PagingRequestHelper
import im.vector.matrix.android.testCoroutineDispatchers
import io.realm.Realm
import io.realm.RealmConfiguration
import org.amshove.kluent.shouldEqual
import org.junit.Before
import org.junit.Rule
import org.junit.Test
internal class TimelineHolderTest : InstrumentedTest {
@get:Rule val testRule = InstantTaskExecutorRule()
private lateinit var monarchy: Monarchy
@Before
fun setup() {
Realm.init(context())
val testConfiguration = RealmConfiguration.Builder().name("test-realm").build()
Realm.deleteRealm(testConfiguration)
monarchy = Monarchy.Builder().setRealmConfiguration(testConfiguration).build()
}
@Test
@UiThreadTest
fun backPaginate_shouldLoadMoreEvents_whenLoadAroundIsCalled() {
val roomId = "roomId"
val taskExecutor = TaskExecutor(testCoroutineDispatchers)
val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy)
val paginationTask = FakePaginationTask(tokenChunkEventPersistor)
val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor)
val boundaryCallback = TimelineBoundaryCallback(roomId, taskExecutor, paginationTask, monarchy, PagingRequestHelper(MainThreadExecutor()))
RoomDataHelper.fakeInitialSync(monarchy, roomId)
val timelineHolder = DefaultTimelineService(roomId, monarchy, taskExecutor, boundaryCallback, getContextOfEventTask, RoomMemberExtractor(monarchy, roomId))
val timelineObserver = LiveDataTestObserver.test(timelineHolder.timeline())
timelineObserver.awaitNextValue().assertHasValue()
var timelineData = timelineObserver.value()
timelineData.events.size shouldEqual 30
(0 until timelineData.events.size).map {
timelineData.events.loadAround(it)
}
timelineObserver.awaitNextValue().assertHasValue()
timelineData = timelineObserver.value()
timelineData.events.size shouldEqual 60
}
}

View File

@ -0,0 +1,103 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.matrix.android.session.room.timeline
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.session.room.EventRelationExtractor
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
import im.vector.matrix.android.internal.session.room.members.SenderRoomMemberExtractor
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.testCoroutineDispatchers
import io.realm.Realm
import io.realm.RealmConfiguration
import org.amshove.kluent.shouldEqual
import org.junit.Before
import org.junit.Test
import timber.log.Timber
import java.util.concurrent.CountDownLatch
internal class TimelineTest : InstrumentedTest {
companion object {
private const val ROOM_ID = "roomId"
}
private lateinit var monarchy: Monarchy
@Before
fun setup() {
Timber.plant(Timber.DebugTree())
Realm.init(context())
val testConfiguration = RealmConfiguration.Builder().name("test-realm").build()
Realm.deleteRealm(testConfiguration)
monarchy = Monarchy.Builder().setRealmConfiguration(testConfiguration).build()
RoomDataHelper.fakeInitialSync(monarchy, ROOM_ID)
}
private fun createTimeline(initialEventId: String? = null): Timeline {
val taskExecutor = TaskExecutor(testCoroutineDispatchers)
val erau = EventRelationsAggregationUpdater(Credentials("", "", "", null, null))
val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy, erau)
val paginationTask = FakePaginationTask(tokenChunkEventPersistor)
val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor)
val roomMemberExtractor = SenderRoomMemberExtractor(ROOM_ID)
val timelineEventFactory = TimelineEventFactory(roomMemberExtractor, EventRelationExtractor())
return DefaultTimeline(
ROOM_ID,
initialEventId,
monarchy.realmConfiguration,
taskExecutor,
getContextOfEventTask,
timelineEventFactory,
paginationTask,
null)
}
@Test
fun backPaginate_shouldLoadMoreEvents_whenPaginateIsCalled() {
val timeline = createTimeline()
timeline.start()
val paginationCount = 30
var initialLoad = 0
val latch = CountDownLatch(2)
var timelineEvents: List<TimelineEvent> = emptyList()
timeline.listener = object : Timeline.Listener {
override fun onUpdated(snapshot: List<TimelineEvent>) {
if (snapshot.isNotEmpty()) {
if (initialLoad == 0) {
initialLoad = snapshot.size
}
timelineEvents = snapshot
latch.countDown()
timeline.paginate(Timeline.Direction.BACKWARDS, paginationCount)
}
}
}
latch.await()
timelineEvents.size shouldEqual initialLoad + paginationCount
timeline.dispose()
}
}

View File

@ -0,0 +1,99 @@
/*
* Copyright (C) 2016 Jeff Gilfelt.
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.network.interceptors
import okhttp3.Interceptor
import okhttp3.Response
import okhttp3.logging.HttpLoggingInterceptor
import okio.Buffer
import java.io.IOException
import java.nio.charset.Charset
/**
* An OkHttp interceptor that logs requests as curl shell commands. They can then
* be copied, pasted and executed inside a terminal environment. This might be
* useful for troubleshooting client/server API interaction during development,
* making it easy to isolate and share requests made by the app. <p> Warning: The
* logs generated by this interceptor have the potential to leak sensitive
* information. It should only be used in a controlled manner or in a
* non-production environment.
*/
internal class CurlLoggingInterceptor(private val logger: HttpLoggingInterceptor.Logger = HttpLoggingInterceptor.Logger.DEFAULT)
: Interceptor {
/**
* Set any additional curl command options (see 'curl --help').
*/
var curlOptions: String? = null
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
var compressed = false
var curlCmd = "curl"
if (curlOptions != null) {
curlCmd += " " + curlOptions!!
}
curlCmd += " -X " + request.method()
val requestBody = request.body()
if (requestBody != null) {
val buffer = Buffer()
requestBody.writeTo(buffer)
var charset: Charset? = UTF8
val contentType = requestBody.contentType()
if (contentType != null) {
charset = contentType.charset(UTF8)
}
// try to keep to a single line and use a subshell to preserve any line breaks
curlCmd += " --data $'" + buffer.readString(charset!!).replace("\n", "\\n") + "'"
}
val headers = request.headers()
var i = 0
val count = headers.size()
while (i < count) {
val name = headers.name(i)
val value = headers.value(i)
if ("Accept-Encoding".equals(name, ignoreCase = true) && "gzip".equals(value, ignoreCase = true)) {
compressed = true
}
curlCmd += " -H \"$name: $value\""
i++
}
curlCmd += ((if (compressed) " --compressed " else " ") + "'" + request.url().toString()
// Replace localhost for emulator by localhost for shell
.replace("://10.0.2.2:8080/".toRegex(), "://127.0.0.1:8080/")
+ "'")
// Add Json formatting
curlCmd += " | python -m json.tool"
logger.log("--- cURL (" + request.url() + ")")
logger.log(curlCmd)
return chain.proceed(request)
}
companion object {
private val UTF8 = Charset.forName("UTF-8")
}
}

View File

@ -0,0 +1,76 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.network.interceptors
import androidx.annotation.NonNull
import im.vector.matrix.android.BuildConfig
import okhttp3.logging.HttpLoggingInterceptor
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import timber.log.Timber
class FormattedJsonHttpLogger : HttpLoggingInterceptor.Logger {
companion object {
private const val INDENT_SPACE = 2
}
/**
* Log the message and try to log it again as a JSON formatted string
* Note: it can consume a lot of memory but it is only in DEBUG mode
*
* @param message
*/
@Synchronized
override fun log(@NonNull message: String) {
// In RELEASE there is no log, but for sure, test again BuildConfig.DEBUG
if (BuildConfig.DEBUG) {
Timber.v(message)
if (message.startsWith("{")) {
// JSON Detected
try {
val o = JSONObject(message)
logJson(o.toString(INDENT_SPACE))
} catch (e: JSONException) {
// Finally this is not a JSON string...
Timber.e(e)
}
} else if (message.startsWith("[")) {
// JSON Array detected
try {
val o = JSONArray(message)
logJson(o.toString(INDENT_SPACE))
} catch (e: JSONException) {
// Finally not JSON...
Timber.e(e)
}
}
// Else not a json string to log
}
}
private fun logJson(formattedJson: String) {
val arr = formattedJson.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
for (s in arr) {
Timber.v(s)
}
}
}

View File

@ -21,11 +21,13 @@ import androidx.lifecycle.ProcessLifecycleOwner
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.matrix.android.internal.auth.AuthModule
import im.vector.matrix.android.internal.di.MatrixKoinComponent
import im.vector.matrix.android.internal.di.MatrixKoinHolder
import im.vector.matrix.android.internal.di.MatrixModule
import im.vector.matrix.android.internal.di.NetworkModule
import im.vector.matrix.android.internal.network.UserAgentHolder
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
import org.koin.standalone.inject
import java.util.concurrent.atomic.AtomicBoolean
@ -38,8 +40,9 @@ import java.util.concurrent.atomic.AtomicBoolean
class Matrix private constructor(context: Context) : MatrixKoinComponent {
private val authenticator by inject<Authenticator>()
private val userAgentHolder by inject<UserAgentHolder>()
private val backgroundDetectionObserver by inject<BackgroundDetectionObserver>()
lateinit var currentSession: Session
var currentSession: Session? = null
init {
Monarchy.init(context)
@ -48,10 +51,11 @@ class Matrix private constructor(context: Context) : MatrixKoinComponent {
val authModule = AuthModule().definition
MatrixKoinHolder.instance.loadModules(listOf(matrixModule, networkModule, authModule))
ProcessLifecycleOwner.get().lifecycle.addObserver(backgroundDetectionObserver)
val lastActiveSession = authenticator.getLastActiveSession()
if (lastActiveSession != null) {
currentSession = lastActiveSession
currentSession.open()
authenticator.getLastActiveSession()?.also {
currentSession = it
it.open()
it.setFilter(FilterService.FilterPreset.RiotFilter)
it.startSync()
}
}
@ -59,6 +63,15 @@ class Matrix private constructor(context: Context) : MatrixKoinComponent {
return authenticator
}
/**
* Set application flavor, to alter user agent.
*/
fun setApplicationFlavor(flavor: String) {
userAgentHolder.setApplicationFlavor(flavor)
}
fun getUserAgent() = userAgentHolder.userAgent
companion object {
private lateinit var instance: Matrix
private val isInit = AtomicBoolean(false)

View File

@ -24,7 +24,7 @@ interface MatrixCallback<in T> {
/**
* On success method, default to no-op
* @param data the data successfuly returned from the async function
* @param data the data successfully returned from the async function
*/
fun onSuccess(data: T) {
//no-op

View File

@ -44,6 +44,10 @@ object MatrixPatterns {
private val MATRIX_EVENT_IDENTIFIER_REGEX = "\\$[A-Z0-9]+$DOMAIN_REGEX"
val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER = Pattern.compile(MATRIX_EVENT_IDENTIFIER_REGEX, Pattern.CASE_INSENSITIVE)
// regex pattern to find message ids in a string.
private val MATRIX_EVENT_IDENTIFIER_V3_REGEX = "\\$[A-Z0-9/+]+"
val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3 = Pattern.compile(MATRIX_EVENT_IDENTIFIER_V3_REGEX, Pattern.CASE_INSENSITIVE)
// regex pattern to find group ids in a string.
private val MATRIX_GROUP_IDENTIFIER_REGEX = "\\+[A-Z0-9=_\\-./]+$DOMAIN_REGEX"
val PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER = Pattern.compile(MATRIX_GROUP_IDENTIFIER_REGEX, Pattern.CASE_INSENSITIVE)
@ -116,7 +120,8 @@ object MatrixPatterns {
* @return true if the string is a valid event id.
*/
fun isEventId(str: String?): Boolean {
return str != null && PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER.matcher(str).matches()
return str != null
&& (PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER.matcher(str).matches() || PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3.matcher(str).matches())
}
/**

View File

@ -19,7 +19,7 @@ package im.vector.matrix.android.api.failure
import java.io.IOException
/**
* This class allows to expose differents kind of error to be then handled by the application.
* This class allows to expose different kinds of error to be then handled by the application.
* As it is a sealed class, you typically use it like that :
* when(failure) {
* is NetworkConnection -> Unit

View File

@ -33,6 +33,7 @@ data class MatrixError(
const val FORBIDDEN = "M_FORBIDDEN"
const val UNKNOWN = "M_UNKNOWN"
const val UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"
const val MISSING_TOKEN = "M_MISSING_TOKEN"
const val BAD_JSON = "M_BAD_JSON"
const val NOT_JSON = "M_NOT_JSON"
const val NOT_FOUND = "M_NOT_FOUND"

View File

@ -89,6 +89,4 @@ object MatrixLinkify {
}
}
}
}

View File

@ -18,15 +18,28 @@ package im.vector.matrix.android.api.session
import androidx.annotation.MainThread
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.cache.CacheService
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.matrix.android.api.session.user.UserService
/**
* This interface defines interactions with a session.
* An instance of a session will be provided by the SDK.
*/
interface Session : RoomService, GroupService {
interface Session :
RoomService,
GroupService,
UserService,
CryptoService,
CacheService,
SignOutService,
FilterService {
/**
* The params associated to the session
@ -39,6 +52,18 @@ interface Session : RoomService, GroupService {
@MainThread
fun open()
/**
* This method start the sync thread.
*/
@MainThread
fun startSync()
/**
* This method stop the sync thread.
*/
@MainThread
fun stopSync()
/**
* This method allow to close a session. It does stop some services.
*/
@ -50,6 +75,11 @@ interface Session : RoomService, GroupService {
*/
fun contentUrlResolver(): ContentUrlResolver
/**
* Returns the ContentUploadProgressTracker associated with the session
*/
fun contentUploadProgressTracker(): ContentUploadStateTracker
/**
* Add a listener to the session.
* @param listener the listener to add.
@ -65,8 +95,11 @@ interface Session : RoomService, GroupService {
/**
* A global session listener to get notified for some events.
*/
// Not used at the moment
interface Listener
interface Listener {
/**
* The access token is not valid anymore
*/
fun onInvalidToken()
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.matrix.android.api.session.cache
import im.vector.matrix.android.api.MatrixCallback
/**
* This interface defines a method to sign out. It's implemented at the session level.
*/
interface CacheService {
/**
* Clear the whole cached data, except credentials. Once done, the session is closed and has to be opened again
*/
fun clearCache(callback: MatrixCallback<Unit>)
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.matrix.android.api.session.content
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
@Parcelize
data class ContentAttachmentData(
val size: Long = 0,
val duration: Long? = 0,
val date: Long = 0,
val height: Long? = 0,
val width: Long? = 0,
val name: String? = null,
val path: String,
val mimeType: String,
val type: Type
) : Parcelable {
enum class Type {
FILE,
IMAGE,
AUDIO,
VIDEO
}
}

View File

@ -14,24 +14,29 @@
* limitations under the License.
*/
package im.vector.riotredesign.features.home.group
package im.vector.matrix.android.api.session.content
import arrow.core.Option
import com.jakewharton.rxrelay2.BehaviorRelay
import im.vector.matrix.android.api.session.group.model.GroupSummary
import io.reactivex.Observable
interface ContentUploadStateTracker {
class SelectedGroupHolder {
fun track(key: String, updateListener: UpdateListener)
private val selectedGroupStream = BehaviorRelay.createDefault<Option<GroupSummary>>(Option.empty())
fun untrack(key: String, updateListener: UpdateListener)
fun setSelectedGroup(group: GroupSummary?) {
val optionValue = Option.fromNullable(group)
selectedGroupStream.accept(optionValue)
fun setFailure(key: String)
fun setSuccess(key: String)
fun setProgress(key: String, current: Long, total: Long)
interface UpdateListener {
fun onUpdate(state: State)
}
fun selectedGroup(): Observable<Option<GroupSummary>> {
return selectedGroupStream.hide()
sealed class State {
object Idle : State()
data class ProgressData(val current: Long, val total: Long) : State()
object Success : State()
object Failure : State()
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.matrix.android.api.session.crypto
interface CryptoService {
// Not supported for the moment
fun isCryptoEnabled() = false
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.matrix.android.api.session.events.model
import com.squareup.moshi.JsonClass
/**
* <code>
* {
* "chunk": [
* {
* "type": "m.reaction",
* "key": "👍",
* "count": 3
* }
* ],
* "limited": false,
* "count": 1
* },
* </code>
*/
@JsonClass(generateAdapter = true)
data class AggregatedAnnotation (
override val limited: Boolean? = false,
override val count: Int? = 0,
val chunk: List<RelationChunkInfo>? = null
) : UnsignedRelationInfo

View File

@ -0,0 +1,53 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.matrix.android.api.session.events.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* <code>
* {
* "m.annotation": {
* "chunk": [
* {
* "type": "m.reaction",
* "key": "👍",
* "count": 3
* }
* ],
* "limited": false,
* "count": 1
* },
* "m.reference": {
* "chunk": [
* {
* "type": "m.room.message",
* "event_id": "$some_event_id"
* }
* ],
* "limited": false,
* "count": 1
* }
* }
* </code>
*/
@JsonClass(generateAdapter = true)
data class AggregatedRelations(
@Json(name = "m.annotation") val annotations: AggregatedAnnotation? = null,
@Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null
)

View File

@ -0,0 +1,26 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.matrix.android.api.session.events.model
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class DefaultUnsignedRelationInfo(
override val limited: Boolean? = false,
override val count: Int? = 0,
val chunk: List<Map<String, Any>>? = null
) : UnsignedRelationInfo

View File

@ -35,6 +35,18 @@ inline fun <reified T> Content?.toModel(): T? {
}
}
/**
* This methods is a facility method to map a model to a json Content
*/
@Suppress("UNCHECKED_CAST")
inline fun <reified T> T?.toContent(): Content? {
return this?.let {
val moshi = MoshiProvider.providesMoshi()
val moshiAdapter = moshi.adapter(T::class.java)
return moshiAdapter.toJsonValue(it) as Content
}
}
/**
* Generic event class with all possible fields for events.
* The content and prevContent json fields can easily be mapped to a model with [toModel] method.
@ -42,7 +54,7 @@ inline fun <reified T> Content?.toModel(): T? {
@JsonClass(generateAdapter = true)
data class Event(
@Json(name = "type") val type: String,
@Json(name = "event_id") val eventId: String?,
@Json(name = "event_id") val eventId: String? = null,
@Json(name = "content") val content: Content? = null,
@Json(name = "prev_content") val prevContent: Content? = null,
@Json(name = "origin_server_ts") val originServerTs: Long? = null,

View File

@ -65,6 +65,11 @@ object EventType {
const val CALL_ANSWER = "m.call.answer"
const val CALL_HANGUP = "m.call.hangup"
// Relation Events
const val REACTION = "m.reaction"
private val STATE_EVENTS = listOf(
STATE_ROOM_NAME,
STATE_ROOM_TOPIC,
@ -86,4 +91,10 @@ object EventType {
return STATE_EVENTS.contains(type)
}
fun isCallEvent(type: String): Boolean {
return type == CALL_INVITE
|| type == CALL_CANDIDATES
|| type == CALL_ANSWER
|| type == CALL_HANGUP
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.matrix.android.api.session.events.model
import com.squareup.moshi.JsonClass
/**
* <code>
* {
* "type": "m.reaction",
* "key": "👍",
* "count": 3
* }
* </code>
*/
@JsonClass(generateAdapter = true)
data class RelationChunkInfo(
val type: String,
val key: String,
val count: Int
)

View File

@ -0,0 +1,31 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.matrix.android.api.session.events.model
/**
* Constants defining known event relation types from Matrix specifications.
*/
object RelationType {
/** Lets you define an event which annotates an existing event.*/
const val ANNOTATION = "m.annotation"
/** Lets you define an event which replaces an existing event.*/
const val REPLACE = "m.replace"
/** ets you define an event which references an existing event.*/
const val REFERENCE = "m.reference"
}

View File

@ -24,5 +24,6 @@ data class UnsignedData(
@Json(name = "age") val age: Long?,
@Json(name = "redacted_because") val redactedEvent: Event? = null,
@Json(name = "transaction_id") val transactionId: String? = null,
@Json(name = "prev_content") val prevContent: Map<String, Any>? = null
@Json(name = "prev_content") val prevContent: Map<String, Any>? = null,
@Json(name = "m.relations") val relations: AggregatedRelations? = null
)

View File

@ -13,10 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.core.extensions
package im.vector.matrix.android.api.session.events.model
fun CharSequence.firstCharAsString(): String {
return if (isNotEmpty()) this[0].toString() else ""
interface UnsignedRelationInfo {
val limited : Boolean?
val count: Int?
}

View File

@ -17,16 +17,18 @@
package im.vector.matrix.android.api.session.room
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.session.room.members.RoomMembersService
import im.vector.matrix.android.api.session.room.members.MembershipService
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.annotation.ReactionService
import im.vector.matrix.android.api.session.room.read.ReadService
import im.vector.matrix.android.api.session.room.send.SendService
import im.vector.matrix.android.api.session.room.state.StateService
import im.vector.matrix.android.api.session.room.timeline.TimelineService
/**
* This interface defines methods to interact within a room.
*/
interface Room : TimelineService, SendService, ReadService, RoomMembersService {
interface Room : TimelineService, SendService, ReadService, MembershipService, StateService , ReactionService{
/**
* The roomId of this room
@ -39,26 +41,4 @@ interface Room : TimelineService, SendService, ReadService, RoomMembersService {
*/
val roomSummary: LiveData<RoomSummary>
/**
* Add a listener to the room.
* @param listener the listener to add.
*/
fun addListener(listener: Listener)
/**
* Remove a listener from the room.
* @param listener the listener to remove.
*/
fun removeListener(listener: Listener)
/**
* A listener defined at the room level to listen for some events from the different room services.
*/
interface Listener : ReadService.Listener {
override fun onReadReceiptsUpdated() {
// no-op
}
}
}

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