Compare commits

..

5 Commits

1885 changed files with 8434 additions and 144435 deletions

View File

@ -1,53 +0,0 @@
# 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 GPlay 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"
artifact_paths:
- "vector/build/outputs/apk/gplay/debug/*.apk"
branches: "!master"
plugins:
- docker#v3.1.0:
image: "runmymind/docker-android-sdk"

- label: "Assemble FDroid 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 lintFdroidRelease assembleFdroidDebug --stacktrace"
artifact_paths:
- "vector/build/outputs/apk/fdroid/debug/*.apk"
branches: "!master"
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"

View File

@ -1,10 +0,0 @@
### 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,5 +10,3 @@
/build
/captures
.externalNativeBuild

/tmp

29
.idea/codeStyles/Project.xml generated Normal file
View File

@ -0,0 +1,29 @@
<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>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

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

13
.idea/dictionaries/ganfra.xml generated Normal file
View File

@ -0,0 +1,13 @@
<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 Normal file
View File

@ -0,0 +1,20 @@
<?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

@ -0,0 +1,6 @@
<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 Normal file
View File

@ -0,0 +1,34 @@
<?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>

12
.idea/runConfigurations.xml generated Normal file
View File

@ -0,0 +1,12 @@
<?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>

View File

@ -1,60 +0,0 @@
# 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

View File

View File

@ -1,131 +0,0 @@
Changes in RiotX 0.5.0 (2019-XX-XX)
===================================================

Features:
-

Improvements:
- Reduce default release build log level, and lab option to enable more logs.

Other changes:
-

Bugfix:
- Fix crash due to missing informationData (#535)
- Progress in initial sync dialog is decreasing for a step and should not (#532)

Translations:
-

Build:
- Fix issue with version name (#533)
- Fix rendering issue of accepted third party invitation event

Changes in RiotX 0.4.0 (2019-XX-XX)
===================================================

Features:
- Display read receipts in timeline (#81)

Improvements:
- Reactions: Reinstate the ability to react with non-unicode keys (#307)

Bugfix:
- Fix text diff linebreak display (#441)
- Date change message repeats for each redaction until a normal message (#358)
- Slide-in reply icon is distorted (#423)
- Regression / e2e replies not encrypted
- Some video won't play
- Privacy: remove log of notifiable event (#519)
- Fix crash with EmojiCompat (#530)

Changes in RiotX 0.3.0 (2019-08-08)
===================================================

Features:
- Create Direct Room flow
- Handle `/markdown` command

Improvements:
- UI for pending edits (#193)
- UX image preview screen transition (#393)
- Basic support for resending failed messages (retry/remove)
- Enable proper cancellation of suspending functions (including db transaction)
- Enhances network connectivity checks in SDK
- Add "View Edit History" item in the message bottom sheet (#401)
- Cancel sync request on pause and timeout to 0 after pause (#404)

Other changes:
- Show sync progress also in room detail screen (#403)

Bugfix:
- Edited message: link confusion when (edited) appears in body (#398)
- Close detail room screen when the room is left with another client (#256)
- Clear notification for a room left on another client
- Fix messages with empty `in_reply_to` not rendering (#447)
- Fix clear cache (#408) and Logout (#205)
- Fix `(edited)` link can be copied to clipboard (#402)

Build:
- Split APK: generate one APK per arch, to reduce APK size of about 30%


Changes in RiotX 0.2.0 (2019-07-18)
===================================================

Features:
- Message Editing: View edit history (#121)
- Rooms filtering (#304)
- Edit in encrypted room

Improvements:
- Handle click on redacted events: view source and create permalink
- Improve long tap menu: reply on top, more compact (#368)
- Quick reply in timeline with swipe gesture (#167)
- Improve edit of replies
- Improve performance on Room Members and Users management (#381)

Other changes:
- migrate from rxbinding 2 to rxbinding 3

Bugfix:
- Fix regression on permalink click
- Fix crash reported by the PlayStore (#341)
- Fix Chat composer separator color in dark/black theme
- Fix bad layout for room directory filter (#349)
- Fix Copying link from a message shouldn't open context menu (#364)

Changes in RiotX 0.1.0 (2019-07-11)
===================================================

First release!

Mode details here: https://medium.com/@RiotChat/introducing-the-riotx-beta-for-android-b17952e8f771


=======================================================
+ TEMPLATE WHEN PREPARING A NEW RELEASE +
=======================================================


Changes in RiotX 0.0.0 (2019-XX-XX)
===================================================

Features:
-

Improvements:
-

Other changes:
-

Bugfix:
-

Translations:
-

Build:
-

View File

@ -1,76 +0,0 @@
# 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
View File

@ -1,176 +0,0 @@
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,37 +1 @@
[![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)
[![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 beta but in active development.

It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-android) with a new user experience. RiotX will become the official replacement as soon as all features are implemented.

[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" alt="Get it on Google Play" height="60">](https://play.google.com/store/apps/details?id=im.vector.riotx)

Nightly build: [![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)

# New Android SDK

RiotX is based on a new Android SDK fully written in Kotlin (like RiotX). In order to make the early development as fast as possible, RiotX and the new SDK currently share the same git repository. We will make separate repos once the API is stable enough.


# Roadmap

The current target is to release an application out of beta with the same level of features (and even more) as Riot.
The roadmap has 3 phases:

- [phase 0](https://github.com/vector-im/riotX-android/labels/phase0): Prototyping / Project setup
- [phase 1](https://github.com/vector-im/riotX-android/labels/phase1): Beta release to the Play Store
- [phase 2](https://github.com/vector-im/riotX-android/labels/phase2): Out of beta


## Contributing

Please refer to [CONTRIBUTING.md](https://github.com/vector-im/riotX-android/blob/develop/CONTRIBUTING.md) if you want to contribute on Matrix Android projects!

Come chat with the community in the dedicated Matrix [room](https://matrix.to/#/#riotx:matrix.org).
# riot-android-redesign-PoC

View File

105
app/build.gradle Normal file
View File

@ -0,0 +1,105 @@
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

@ -14,7 +14,7 @@
* limitations under the License.
*/

package im.vector.riotx
package im.vector.riotredesign

import androidx.test.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4
@ -35,6 +35,6 @@ class ExampleInstrumentedTest {
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getTargetContext()
assertEquals("im.vector.riotx", appContext.packageName)
assertEquals("im.vector.riotredesign", appContext.packageName)
}
}

View File

@ -0,0 +1,31 @@
<?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

@ -0,0 +1,46 @@
/*
* 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

@ -0,0 +1,47 @@
/*
* 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.di

import android.content.Context
import android.content.Context.MODE_PRIVATE
import im.vector.riotredesign.core.resources.LocaleProvider
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository
import org.koin.dsl.module.module

class AppModule(private val context: Context) {

val definition = module {

single {
LocaleProvider(context.resources)
}

single {
StringProvider(context.resources)
}

single {
context.getSharedPreferences("im.vector.riot", MODE_PRIVATE)
}

single {
RoomSelectionRepository(get())
}

}
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/

package im.vector.riotx.core.epoxy
package im.vector.riotredesign.core.epoxy

import android.view.View
import com.airbnb.epoxy.EpoxyHolder
@ -26,15 +26,15 @@ import kotlin.reflect.KProperty
*
* See [SampleKotlinModelWithHolder] for a usage example.
*/
abstract class VectorEpoxyHolder : EpoxyHolder() {
lateinit var view: View
abstract class KotlinEpoxyHolder : EpoxyHolder() {
private lateinit var view: View

override fun bindView(itemView: View) {
view = itemView
}

protected fun <V : View> bind(id: Int): ReadOnlyProperty<VectorEpoxyHolder, V> =
Lazy { holder: VectorEpoxyHolder, prop ->
protected fun <V : View> bind(id: Int): ReadOnlyProperty<KotlinEpoxyHolder, V> =
Lazy { holder: KotlinEpoxyHolder, prop ->
holder.view.findViewById(id) as V?
?: throw IllegalStateException("View ID $id for '${prop.name}' not found.")
}
@ -44,13 +44,13 @@ abstract class VectorEpoxyHolder : EpoxyHolder() {
* https://github.com/JakeWharton/kotterknife
*/
private class Lazy<V>(
private val initializer: (VectorEpoxyHolder, KProperty<*>) -> V
) : ReadOnlyProperty<VectorEpoxyHolder, V> {
private val initializer: (KotlinEpoxyHolder, KProperty<*>) -> V
) : ReadOnlyProperty<KotlinEpoxyHolder, V> {
private object EMPTY

private var value: Any? = EMPTY

override fun getValue(thisRef: VectorEpoxyHolder, property: KProperty<*>): V {
override fun getValue(thisRef: KotlinEpoxyHolder, property: KProperty<*>): V {
if (value == EMPTY) {
value = initializer(thisRef, property)
}

View File

@ -0,0 +1,74 @@
/*
* 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

@ -14,7 +14,7 @@
* limitations under the License.
*/

package im.vector.riotx.core.extensions
package im.vector.riotredesign.core.extensions

import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
@ -23,14 +23,10 @@ fun AppCompatActivity.addFragment(fragment: Fragment, frameId: Int) {
supportFragmentManager.inTransaction { add(frameId, fragment) }
}

fun AppCompatActivity.replaceFragment(fragment: Fragment, frameId: Int, tag: String? = null) {
supportFragmentManager.inTransaction { replace(frameId, fragment, tag) }
fun AppCompatActivity.replaceFragment(fragment: Fragment, frameId: Int) {
supportFragmentManager.inTransaction { replace(frameId, fragment) }
}

fun AppCompatActivity.addFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
supportFragmentManager.inTransaction { replace(frameId, fragment).addToBackStack(tag) }
}

fun AppCompatActivity.hideKeyboard() {
currentFocus?.hideKeyboard()
}
}

View File

@ -14,10 +14,9 @@
* limitations under the License.
*/

package im.vector.riotx.core.di
package im.vector.riotredesign.core.extensions

interface HasScreenInjector {

fun injector(): ScreenComponent

fun CharSequence.firstCharAsString(): String {
return if (isNotEmpty()) this[0].toString() else ""
}

View File

@ -14,26 +14,26 @@
* limitations under the License.
*/

package im.vector.riotx.core.extensions
package im.vector.riotredesign.core.extensions

import android.text.Editable
import android.text.InputType
import android.text.TextWatcher
import android.view.MotionEvent
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import androidx.annotation.DrawableRes
import im.vector.riotx.R
import im.vector.riotx.core.platform.SimpleTextWatcher
import im.vector.riotredesign.R

fun EditText.setupAsSearch(@DrawableRes searchIconRes: Int = R.drawable.ic_filter,
@DrawableRes clearIconRes: Int = R.drawable.ic_x_green) {

addTextChangedListener(object : SimpleTextWatcher() {
override fun afterTextChanged(s: Editable) {
val clearIcon = if (s.isNotEmpty()) clearIconRes else 0
setCompoundDrawablesWithIntrinsicBounds(searchIconRes, 0, clearIcon, 0)
fun EditText.setupAsSearch() {
addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(editable: Editable?) {
val clearIcon = if (editable?.isNotEmpty() == true) R.drawable.ic_clear_white else 0
setCompoundDrawablesWithIntrinsicBounds(0, 0, clearIcon, 0)
}

override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
})

maxLines = 1

View File

@ -14,12 +14,15 @@
* limitations under the License.
*/

package im.vector.riotx.core.extensions
package im.vector.riotredesign.core.extensions

import im.vector.matrix.android.api.session.events.model.Event
import im.vector.riotx.core.resources.DateProvider
import org.threeten.bp.Instant
import org.threeten.bp.LocalDateTime
import org.threeten.bp.ZoneId


fun Event.localDateTime(): LocalDateTime {
return DateProvider.toLocalDateTime(originServerTs)
val instant = Instant.ofEpochMilli(originServerTs ?: 0)
return LocalDateTime.ofInstant(instant, ZoneId.systemDefault())
}

View File

@ -14,30 +14,30 @@
* limitations under the License.
*/

package im.vector.riotx.core.extensions
package im.vector.riotredesign.core.extensions

import androidx.fragment.app.Fragment

fun Fragment.addFragment(fragment: Fragment, frameId: Int) {
fun androidx.fragment.app.Fragment.addFragment(fragment: Fragment, frameId: Int) {
fragmentManager?.inTransaction { add(frameId, fragment) }
}

fun Fragment.replaceFragment(fragment: Fragment, frameId: Int) {
fun androidx.fragment.app.Fragment.replaceFragment(fragment: Fragment, frameId: Int) {
fragmentManager?.inTransaction { replace(frameId, fragment) }
}

fun Fragment.addFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
fun androidx.fragment.app.Fragment.addFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
fragmentManager?.inTransaction { replace(frameId, fragment).addToBackStack(tag) }
}

fun Fragment.addChildFragment(fragment: Fragment, frameId: Int) {
fun androidx.fragment.app.Fragment.addChildFragment(fragment: Fragment, frameId: Int) {
childFragmentManager.inTransaction { add(frameId, fragment) }
}

fun Fragment.replaceChildFragment(fragment: Fragment, frameId: Int) {
fun androidx.fragment.app.Fragment.replaceChildFragment(fragment: Fragment, frameId: Int) {
childFragmentManager.inTransaction { replace(frameId, fragment) }
}

fun Fragment.addChildFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
fun androidx.fragment.app.Fragment.addChildFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
childFragmentManager.inTransaction { replace(frameId, fragment).addToBackStack(tag) }
}
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/

package im.vector.riotx.core.extensions
package im.vector.riotredesign.core.extensions

import androidx.fragment.app.FragmentTransaction

View File

@ -0,0 +1,38 @@
/*
*
* * 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.extensions

/**
* Returns the last element yielding the smallest value of the given function or `null` if there are no elements.
*/
public inline fun <T, R : Comparable<R>> Iterable<T>.lastMinBy(selector: (T) -> R): T? {
val iterator = iterator()
if (!iterator.hasNext()) return null
var minElem = iterator.next()
var minValue = selector(minElem)
while (iterator.hasNext()) {
val e = iterator.next()
val v = selector(e)
if (minValue >= v) {
minElem = e
minValue = v
}
}
return minElem
}

View File

@ -14,15 +14,13 @@
* limitations under the License.
*/

package im.vector.riotx.core.extensions
package im.vector.riotredesign.core.extensions

import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import im.vector.riotx.core.utils.FirstThrottler
import im.vector.riotx.core.utils.EventObserver
import im.vector.riotx.core.utils.LiveEvent
import im.vector.riotredesign.core.utils.LiveEvent
import im.vector.riotredesign.core.utils.EventObserver

inline fun <T> LiveData<T>.observeK(owner: LifecycleOwner, crossinline observer: (T?) -> Unit) {
this.observe(owner, Observer { observer(it) })
@ -34,18 +32,4 @@ inline fun <T> LiveData<T>.observeNotNull(owner: LifecycleOwner, crossinline obs

inline fun <T> LiveData<LiveEvent<T>>.observeEvent(owner: LifecycleOwner, crossinline observer: (T) -> Unit) {
this.observe(owner, EventObserver { it.run(observer) })
}

inline fun <T> LiveData<LiveEvent<T>>.observeEventFirstThrottle(owner: LifecycleOwner, minimumInterval: Long, crossinline observer: (T) -> Unit) {
val firstThrottler = FirstThrottler(minimumInterval)

this.observe(owner, EventObserver {
if (firstThrottler.canHandle()) {
it.run(observer)
}
})
}

fun <T> MutableLiveData<LiveEvent<T>>.postLiveEvent(content: T) {
this.postValue(LiveEvent(content))
}
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/

package im.vector.riotx.core.extensions
package im.vector.riotredesign.core.extensions

import android.content.Context
import android.view.View

View File

@ -14,19 +14,21 @@
* limitations under the License.
*/

package im.vector.riotx.core.resources
package im.vector.riotredesign.core.glide;

import im.vector.riotx.features.settings.VectorPreferences
import javax.inject.Inject
import android.content.Context;
import android.util.Log;

class UserPreferencesProvider @Inject constructor(private val vectorPreferences: VectorPreferences) {
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.module.AppGlideModule;

fun shouldShowHiddenEvents(): Boolean {
return vectorPreferences.shouldShowHiddenEvents()
}
@GlideModule
public final class MyAppGlideModule extends AppGlideModule {

fun shouldShowReadReceipts(): Boolean {
return vectorPreferences.showReadReceipts()
@Override
public void applyOptions(Context context, GlideBuilder builder) {
builder.setLogLevel(Log.ERROR);
}

}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/

package im.vector.riotx.core.platform
package im.vector.riotredesign.core.platform

import android.content.Context
import android.util.AttributeSet

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/

package im.vector.riotx.core.platform
package im.vector.riotredesign.core.platform

import androidx.recyclerview.widget.ListUpdateCallback

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/

package im.vector.riotx.core.platform
package im.vector.riotredesign.core.platform

interface OnBackPressed {

View File

@ -14,19 +14,19 @@
* limitations under the License.
*/

package im.vector.riotx.features.home.room.detail.timeline.animation
package im.vector.riotredesign.core.platform

import androidx.recyclerview.widget.DefaultItemAnimator
import com.airbnb.mvrx.BaseMvRxActivity
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable

private const val ANIM_DURATION_IN_MILLIS = 100L
abstract class RiotActivity : BaseMvRxActivity() {

class TimelineItemAnimator : DefaultItemAnimator() {
private val uiDisposables = CompositeDisposable()

init {
addDuration = ANIM_DURATION_IN_MILLIS
removeDuration = 0
moveDuration = 0
changeDuration = 0
protected fun Disposable.disposeOnDestroy(): Disposable {
uiDisposables.add(this)
return this
}

}

View File

@ -14,29 +14,29 @@
* limitations under the License.
*/

package im.vector.matrix.android.api.session.content
package im.vector.riotredesign.core.platform

import android.os.Bundle
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import com.airbnb.mvrx.BaseMvRxFragment
import com.airbnb.mvrx.MvRx

@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 {
abstract class RiotFragment : BaseMvRxFragment(), OnBackPressed {

enum class Type {
FILE,
IMAGE,
AUDIO,
VIDEO
val riotActivity: RiotActivity by lazy {
activity as RiotActivity
}

override fun onBackPressed(): Boolean {
return false
}

override fun invalidate() {
//no-ops by default
}

protected fun setArguments(args: Parcelable? = null) {
arguments = args?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } }
}

}

View File

@ -14,12 +14,10 @@
* limitations under the License.
*/

package im.vector.matrix.android.api.session.room.failure
package im.vector.riotredesign.core.platform

import im.vector.matrix.android.api.failure.Failure
import com.airbnb.mvrx.BaseMvRxViewModel
import com.airbnb.mvrx.MvRxState

sealed class CreateRoomFailure : Failure.FeatureFailure() {

object CreatedWithTimeout: CreateRoomFailure()

}
abstract class RiotViewModel<S : MvRxState>(initialState: S)
: BaseMvRxViewModel<S>(initialState, debugMode = false)

View File

@ -14,14 +14,14 @@
* limitations under the License.
*/

package im.vector.riotx.core.platform
package im.vector.riotredesign.core.platform

import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import im.vector.riotx.R
import im.vector.riotredesign.R
import kotlinx.android.synthetic.main.view_state.view.*

class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0)
@ -30,7 +30,7 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
sealed class State {
object Content : State()
object Loading : State()
data class Empty(val title: CharSequence? = null, val image: Drawable? = null, val message: CharSequence? = null) : State()
data class Empty(val message: CharSequence? = null) : State()
data class Error(val message: CharSequence? = null) : State()
}

@ -52,7 +52,7 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?

init {
View.inflate(context, R.layout.view_state, this)
layoutParams = LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
errorRetryView.setOnClickListener {
eventCallback?.onRetryClicked()
}
@ -62,33 +62,35 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?

private fun update(newState: State) {
when (newState) {
is State.Content -> {
is StateView.State.Content -> {
progressBar.visibility = View.INVISIBLE
errorView.visibility = View.INVISIBLE
emptyView.visibility = View.INVISIBLE
contentView?.visibility = View.VISIBLE
}
is State.Loading -> {
is StateView.State.Loading -> {
progressBar.visibility = View.VISIBLE
errorView.visibility = View.INVISIBLE
emptyView.visibility = View.INVISIBLE
contentView?.visibility = View.INVISIBLE
}
is State.Empty -> {
is StateView.State.Empty -> {
progressBar.visibility = View.INVISIBLE
errorView.visibility = View.INVISIBLE
emptyView.visibility = View.VISIBLE
emptyImageView.setImageDrawable(newState.image)
emptyMessageView.text = newState.message
emptyTitleView.text = newState.title
contentView?.visibility = View.INVISIBLE
if (contentView != null) {
contentView!!.visibility = View.INVISIBLE
}
}
is State.Error -> {
is StateView.State.Error -> {
progressBar.visibility = View.INVISIBLE
errorView.visibility = View.VISIBLE
emptyView.visibility = View.INVISIBLE
errorMessageView.text = newState.message
contentView?.visibility = View.INVISIBLE
if (contentView != null) {
contentView!!.visibility = View.INVISIBLE
}
}
}
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/

package im.vector.riotx.core.platform
package im.vector.riotredesign.core.platform

import androidx.appcompat.widget.Toolbar

View File

@ -14,16 +14,17 @@
* limitations under the License.
*/

package im.vector.riotx.core.resources
package im.vector.riotredesign.core.resources

import android.content.res.Resources
import androidx.core.os.ConfigurationCompat
import java.util.*
import javax.inject.Inject

class LocaleProvider @Inject constructor(private val resources: Resources) {
class LocaleProvider(private val resources: Resources) {

fun current(): Locale {
return ConfigurationCompat.getLocales(resources.configuration)[0]
}


}

View File

@ -14,16 +14,13 @@
* limitations under the License.
*/

package im.vector.matrix.android.internal.util
package im.vector.riotredesign.core.resources

import android.content.res.Resources
import androidx.annotation.NonNull
import androidx.annotation.StringRes
import dagger.Reusable
import javax.inject.Inject

@Reusable
internal class StringProvider @Inject constructor(private val resources: Resources) {
class StringProvider(private val resources: Resources) {

/**
* Returns a localized string from the application's package's

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/

package im.vector.riotx.core.utils
package im.vector.riotredesign.core.utils

import androidx.lifecycle.Observer

View File

@ -0,0 +1,41 @@
/*
* 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

@ -0,0 +1,57 @@
/*
* 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

@ -0,0 +1,124 @@
/*
* 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.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.Gravity
import android.view.MenuItem
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.widget.Toolbar
import androidx.core.view.GravityCompat
import androidx.fragment.app.FragmentManager
import com.airbnb.mvrx.viewModel
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.observeEvent
import im.vector.riotredesign.core.extensions.replaceFragment
import im.vector.riotredesign.core.platform.OnBackPressed
import im.vector.riotredesign.core.platform.RiotActivity
import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.features.home.room.detail.LoadingRoomDetailFragment
import kotlinx.android.synthetic.main.activity_home.*
import org.koin.android.ext.android.inject
import org.koin.standalone.StandAloneContext.loadKoinModules


class HomeActivity : RiotActivity(), ToolbarConfigurable {


private val homeActivityViewModel: HomeActivityViewModel by viewModel()
private val homeNavigator by inject<HomeNavigator>()

override fun onCreate(savedInstanceState: Bundle?) {
loadKoinModules(listOf(HomeModule().definition))
homeNavigator.activity = this
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
if (savedInstanceState == null) {
val homeDrawerFragment = HomeDrawerFragment.newInstance()
val loadingDetail = LoadingRoomDetailFragment.newInstance()
replaceFragment(loadingDetail, R.id.homeDetailFragmentContainer)
replaceFragment(homeDrawerFragment, R.id.homeDrawerFragmentContainer)
}
homeActivityViewModel.openRoomLiveData.observeEvent(this) {
homeNavigator.openRoomDetail(it, null)
}
}

override fun onDestroy() {
homeNavigator.activity = null
super.onDestroy()
}

override fun configure(toolbar: Toolbar) {
setSupportActionBar(toolbar)
supportActionBar?.setHomeButtonEnabled(true)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val drawerToggle = ActionBarDrawerToggle(this, drawerLayout, toolbar, 0, 0)
drawerLayout.addDrawerListener(drawerToggle)
drawerToggle.syncState()
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
drawerLayout.openDrawer(GravityCompat.START)
return true
}
}

return true
}

override fun onBackPressed() {
if (drawerLayout.isDrawerOpen(Gravity.LEFT)) {
drawerLayout.closeDrawer(Gravity.LEFT)
} else {
val handled = recursivelyDispatchOnBackPressed(supportFragmentManager)
if (!handled) {
super.onBackPressed()
}
}
}

private fun recursivelyDispatchOnBackPressed(fm: FragmentManager): Boolean {
if (fm.backStackEntryCount == 0)
return false
val reverseOrder = fm.fragments.filter { it is OnBackPressed }.reversed()
for (f in reverseOrder) {
val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager)
if (handledByChildFragments) {
return true
}
val backPressable = f as OnBackPressed
if (backPressable.onBackPressed()) {
return true
}
}
return false
}


companion object {
fun newIntent(context: Context): Intent {
return Intent(context, HomeActivity::class.java)
}

}

}

View File

@ -0,0 +1,77 @@
/*
* 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 androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.rx.rx
import im.vector.riotredesign.core.platform.RiotViewModel
import im.vector.riotredesign.core.utils.LiveEvent
import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository
import io.reactivex.rxkotlin.subscribeBy
import org.koin.android.ext.android.get

class EmptyState : MvRxState

class HomeActivityViewModel(state: EmptyState,
private val session: Session,
roomSelectionRepository: RoomSelectionRepository
) : RiotViewModel<EmptyState>(state) {

companion object : MvRxViewModelFactory<HomeActivityViewModel, EmptyState> {

@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: EmptyState): HomeActivityViewModel? {
val session = Matrix.getInstance().currentSession
val roomSelectionRepository = viewModelContext.activity.get<RoomSelectionRepository>()
return HomeActivityViewModel(state, session, roomSelectionRepository)
}
}

private val _openRoomLiveData = MutableLiveData<LiveEvent<String>>()
val openRoomLiveData: LiveData<LiveEvent<String>>
get() = _openRoomLiveData

init {
val lastSelectedRoomId = roomSelectionRepository.lastSelectedRoom()
if (lastSelectedRoomId == null || session.getRoom(lastSelectedRoomId) == null) {
getTheFirstRoomWhenAvailable()
} else {
_openRoomLiveData.postValue(LiveEvent(lastSelectedRoomId))
}
}

private fun getTheFirstRoomWhenAvailable() {
session.rx().liveRoomSummaries()
.filter { it.isNotEmpty() }
.first(emptyList())
.subscribeBy {
val firstRoom = it.firstOrNull()
if (firstRoom != null) {
_openRoomLiveData.postValue(LiveEvent(firstRoom.roomId))
}
}
.disposeOnClear()
}


}

View File

@ -0,0 +1,52 @@
/*
* 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.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.replaceChildFragment
import im.vector.riotredesign.core.platform.RiotFragment
import im.vector.riotredesign.features.home.group.GroupListFragment
import im.vector.riotredesign.features.home.room.list.RoomListFragment

class HomeDrawerFragment : RiotFragment() {

companion object {

fun newInstance(): HomeDrawerFragment {
return HomeDrawerFragment()
}
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_home_drawer, container, false)
}

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (savedInstanceState == null) {
val groupListFragment = GroupListFragment.newInstance()
replaceChildFragment(groupListFragment, R.id.groupListFragmentContainer)
val roomListFragment = RoomListFragment.newInstance()
replaceChildFragment(roomListFragment, R.id.roomListFragmentContainer)
}
}

}

View File

@ -0,0 +1,100 @@
/*
* 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

@ -0,0 +1,75 @@
/*
* 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.view.Gravity
import androidx.fragment.app.FragmentManager
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.addFragmentToBackstack
import im.vector.riotredesign.core.extensions.replaceFragment
import im.vector.riotredesign.features.home.room.detail.RoomDetailArgs
import im.vector.riotredesign.features.home.room.detail.RoomDetailFragment
import kotlinx.android.synthetic.main.activity_home.*
import timber.log.Timber

class HomeNavigator {

var activity: HomeActivity? = null

private var currentRoomId: String? = null

fun openRoomDetail(roomId: String,
eventId: String?,
addToBackstack: Boolean = false) {
Timber.v("Open room detail $roomId - $eventId - $addToBackstack")
if (!addToBackstack && isRoomOpened(roomId)) {
return
}
activity?.let {
val args = RoomDetailArgs(roomId, eventId)
val roomDetailFragment = RoomDetailFragment.newInstance(args)
it.drawerLayout?.closeDrawer(Gravity.LEFT)
if (addToBackstack) {
it.addFragmentToBackstack(roomDetailFragment, R.id.homeDetailFragmentContainer, roomId)
} else {
currentRoomId = roomId
clearBackStack(it.supportFragmentManager)
it.replaceFragment(roomDetailFragment, R.id.homeDetailFragmentContainer)
}
}
}

fun openGroupDetail(groupId: String) {
Timber.v("Open group detail $groupId")
}

fun openUserDetail(userId: String) {
Timber.v("Open user detail $userId")
}

fun isRoomOpened(roomId: String): Boolean {
return currentRoomId == roomId
}

private fun clearBackStack(fragmentManager: FragmentManager) {
if (fragmentManager.backStackEntryCount > 0) {
val first = fragmentManager.getBackStackEntryAt(0)
fragmentManager.popBackStack(first.id, FragmentManager.POP_BACK_STACK_INCLUSIVE)
}
}

}

View File

@ -0,0 +1,54 @@
/*
* 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.net.Uri
import im.vector.matrix.android.api.permalinks.PermalinkData
import im.vector.matrix.android.api.permalinks.PermalinkParser

class HomePermalinkHandler(private val navigator: HomeNavigator) {

fun launch(deepLink: String?) {
val uri = deepLink?.let { Uri.parse(it) }
launch(uri)
}

fun launch(deepLink: Uri?) {
if (deepLink == null) {
return
}
val permalinkData = PermalinkParser.parse(deepLink)
when (permalinkData) {
is PermalinkData.EventLink -> {
navigator.openRoomDetail(permalinkData.roomIdOrAlias, permalinkData.eventId, true)
}
is PermalinkData.RoomLink -> {
navigator.openRoomDetail(permalinkData.roomIdOrAlias, null, true)
}
is PermalinkData.GroupLink -> {
navigator.openGroupDetail(permalinkData.groupId)
}
is PermalinkData.UserLink -> {
navigator.openUserDetail(permalinkData.userId)
}
is PermalinkData.FallbackLink -> {

}
}
}

}

View File

@ -14,14 +14,11 @@
* limitations under the License.
*/

package im.vector.riotx.core.dialogs
package im.vector.riotredesign.features.home

import android.content.Context
import android.widget.ProgressBar
import com.airbnb.epoxy.ModelView

internal class DialogCallAdapter(context: Context) : DialogAdapter(context) {

init {
add(DialogListItem.StartVoiceCall)
add(DialogListItem.StartVideoCall)
}
}
@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
class LoadingItem(context: Context) : ProgressBar(context)

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/

package im.vector.riotx.features.home.group
package im.vector.riotredesign.features.home.group

import im.vector.matrix.android.api.session.group.model.GroupSummary

View File

@ -14,23 +14,22 @@
* limitations under the License.
*/

package im.vector.riotx.features.home.group
package im.vector.riotredesign.features.home.group

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.platform.StateView
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.home.HomeNavigator
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.RiotFragment
import im.vector.riotredesign.core.platform.StateView
import kotlinx.android.synthetic.main.fragment_group_list.*
import javax.inject.Inject

class GroupListFragment : VectorBaseFragment(), GroupSummaryController.Callback {
class GroupListFragment : RiotFragment(), GroupSummaryController.Callback {

companion object {
fun newInstance(): GroupListFragment {
@ -40,35 +39,36 @@ class GroupListFragment : VectorBaseFragment(), GroupSummaryController.Callback

private val viewModel: GroupListViewModel by fragmentViewModel()

@Inject lateinit var groupListViewModelFactory: GroupListViewModel.Factory
@Inject lateinit var homeNavigator: HomeNavigator
@Inject lateinit var groupController: GroupSummaryController
private lateinit var groupController: GroupSummaryController

override fun getLayoutResId() = R.layout.fragment_group_list

override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_group_list, container, false)
}

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
groupController.callback = this
stateView.contentView = groupListEpoxyRecyclerView
groupListEpoxyRecyclerView.setController(groupController)
groupController = GroupSummaryController(this)
stateView.contentView = epoxyRecyclerView
epoxyRecyclerView.setController(groupController)
viewModel.subscribe { renderState(it) }
viewModel.openGroupLiveData.observeEvent(this) {
homeNavigator.openSelectedGroup(it)
}
}

private fun renderState(state: GroupListViewState) {
when (state.asyncGroups) {
is Incomplete -> stateView.state = StateView.State.Loading
is Success -> stateView.state = StateView.State.Content
is Incomplete -> renderLoading()
is Success -> renderSuccess(state)
}
}

private fun renderSuccess(state: GroupListViewState) {
stateView.state = StateView.State.Content
groupController.setData(state)
}

private fun renderLoading() {
stateView.state = StateView.State.Loading
}

override fun onGroupSelected(groupSummary: GroupSummary) {
viewModel.accept(GroupListActions.SelectGroup(groupSummary))
}

View File

@ -0,0 +1,79 @@
/*
* 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 com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.rx.rx
import im.vector.riotredesign.core.platform.RiotViewModel
import org.koin.android.ext.android.get

class GroupListViewModel(initialState: GroupListViewState,
private val selectedGroupHolder: SelectedGroupHolder,
private val session: Session
) : RiotViewModel<GroupListViewState>(initialState) {

companion object : MvRxViewModelFactory<GroupListViewModel, GroupListViewState> {

@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: GroupListViewState): GroupListViewModel? {
val currentSession = Matrix.getInstance().currentSession
val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupHolder>()
return GroupListViewModel(state, selectedGroupHolder, currentSession)
}
}

init {
observeGroupSummaries()
observeState()
}

private fun observeState() {
subscribe {
selectedGroupHolder.setSelectedGroup(it.selectedGroup)
}
}

fun accept(action: GroupListActions) {
when (action) {
is GroupListActions.SelectGroup -> handleSelectGroup(action)
}
}

// PRIVATE METHODS *****************************************************************************

private fun handleSelectGroup(action: GroupListActions.SelectGroup) = withState { state ->
if (state.selectedGroup?.groupId != action.groupSummary.groupId) {
setState { copy(selectedGroup = action.groupSummary) }
} else {
setState { copy(selectedGroup = null) }
}
}


private fun observeGroupSummaries() {
session
.rx().liveGroupSummaries()
.execute { async ->
copy(asyncGroups = async)
}
}


}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/

package im.vector.riotx.features.home.group
package im.vector.riotredesign.features.home.group

import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState

View File

@ -14,16 +14,13 @@
* limitations under the License.
*/

package im.vector.riotx.features.home.group
package im.vector.riotredesign.features.home.group

import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject

class GroupSummaryController @Inject constructor(private val avatarRenderer: AvatarRenderer): TypedEpoxyController<GroupListViewState>() {

var callback: Callback? = null
class GroupSummaryController(private val callback: Callback? = null
) : TypedEpoxyController<GroupListViewState>() {

override fun buildModels(viewState: GroupListViewState) {
buildGroupModels(viewState.asyncGroups(), viewState.selectedGroup)
@ -35,15 +32,14 @@ class GroupSummaryController @Inject constructor(private val avatarRenderer: Ava
}
summaries.forEach { groupSummary ->
val isSelected = groupSummary.groupId == selected?.groupId
groupSummaryItem {
avatarRenderer(avatarRenderer)
id(groupSummary.groupId)
groupId(groupSummary.groupId)
groupName(groupSummary.displayName)
selected(isSelected)
avatarUrl(groupSummary.avatarUrl)
listener { callback?.onGroupSelected(groupSummary) }
}
GroupSummaryItem(
groupName = groupSummary.displayName,
avatarUrl = groupSummary.avatarUrl,
isSelected = isSelected,
listener = { callback?.onGroupSelected(groupSummary) }
)
.id(groupSummary.groupId)
.addTo(this)
}
}

View File

@ -0,0 +1,41 @@
/*
* 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

@ -14,13 +14,25 @@
* limitations under the License.
*/

package im.vector.riotx.features.home.group
package im.vector.riotredesign.features.home.group

import arrow.core.Option
import com.jakewharton.rxrelay2.BehaviorRelay
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.riotx.core.utils.RxStore
import javax.inject.Inject
import javax.inject.Singleton
import io.reactivex.Observable

@Singleton
class SelectedGroupStore @Inject constructor() : RxStore<Option<GroupSummary>>(Option.empty())
class SelectedGroupHolder {

private val selectedGroupStream = BehaviorRelay.createDefault<Option<GroupSummary>>(Option.empty())

fun setSelectedGroup(group: GroupSummary?) {
val optionValue = Option.fromNullable(group)
selectedGroupStream.accept(optionValue)
}

fun selectedGroup(): Observable<Option<GroupSummary>> {
return selectedGroupStream.hide()
}


}

View File

@ -14,25 +14,23 @@
* limitations under the License.
*/

package im.vector.riotx.core.utils
package im.vector.riotredesign.features.home.room

import com.jakewharton.rxrelay2.BehaviorRelay
import io.reactivex.Observable
import io.reactivex.schedulers.Schedulers
import io.reactivex.subjects.BehaviorSubject

open class RxStore<T>(defaultValue: T? = null) {
class VisibleRoomHolder {

private val storeSubject: BehaviorRelay<T> = if (defaultValue == null) {
BehaviorRelay.create<T>()
} else {
BehaviorRelay.createDefault(defaultValue)
private val visibleRoomStream = BehaviorRelay.create<String>()

fun setVisibleRoom(roomId: String) {
visibleRoomStream.accept(roomId)
}

fun observe(): Observable<T> {
return storeSubject.hide().observeOn(Schedulers.computation())
fun visibleRoom(): Observable<String> {
return visibleRoomStream.hide()
}

fun post(value: T) {
storeSubject.accept(value)
}
}

}

View File

@ -0,0 +1,49 @@
/*
* 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.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.bumptech.glide.Glide
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.RiotFragment
import kotlinx.android.synthetic.main.fragment_loading_room_detail.*

class LoadingRoomDetailFragment : RiotFragment() {

companion object {

fun newInstance(): LoadingRoomDetailFragment {
return LoadingRoomDetailFragment()
}
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_loading_room_detail, container, false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Glide.with(this)
.load(R.drawable.riot_splash)
.into(animatedLogoImageView)
}


}

View File

@ -5,7 +5,7 @@
* 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
* 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,
@ -14,12 +14,14 @@
* limitations under the License.
*/

package im.vector.riotx.core.extensions
package im.vector.riotredesign.features.home.room.detail

import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent

fun TimelineEvent.canReact(): Boolean {
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
return root.getClearType() == EventType.MESSAGE && root.sendState.isSent() && !root.isRedacted()
}
sealed class RoomDetailActions {

data class SendMessage(val text: String) : RoomDetailActions()
object IsDisplayed : RoomDetailActions()
data class EventDisplayed(val event: TimelineEvent, val index: Int) : RoomDetailActions()

}

View File

@ -0,0 +1,154 @@
/*
* 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

@ -0,0 +1,141 @@
/*
* 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

@ -14,19 +14,23 @@
* limitations under the License.
*/

package im.vector.riotx.features.home.room.detail.composer
package im.vector.riotredesign.features.home.room.detail

import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.features.home.room.detail.RoomDetailArgs
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


data class TextComposerViewState(val roomId: String,
val asyncUsers: Async<List<User>> = Uninitialized
data class RoomDetailViewState(
val roomId: String,
val eventId: String?,
val readReceiptsForEventId: Map<String, List<ReadReceipt>> = emptyMap(),
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
val asyncTimelineData: Async<TimelineData> = Uninitialized
) : MvRxState {

constructor(args: RoomDetailArgs) : this(roomId = args.roomId)
constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)

}

View File

@ -14,15 +14,18 @@
* limitations under the License.
*/

package im.vector.riotx.features.home.room.detail
package im.vector.riotredesign.features.home.room.detail

import androidx.recyclerview.widget.LinearLayoutManager
import im.vector.riotx.core.platform.DefaultListUpdateCallback
import im.vector.riotredesign.core.platform.DefaultListUpdateCallback
import java.util.concurrent.atomic.AtomicBoolean

class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager) : DefaultListUpdateCallback {

var isLocked = AtomicBoolean(true)

override fun onInserted(position: Int, count: Int) {
if (position == 0 && layoutManager.findFirstVisibleItemPosition() == 0) {
if (isLocked.compareAndSet(false, true) && position == 0 && layoutManager.findFirstVisibleItemPosition() == 0) {
layoutManager.scrollToPosition(0)
}
}

View File

@ -0,0 +1,49 @@
/*
* 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

@ -14,13 +14,16 @@
* limitations under the License.
*/

package im.vector.riotx.core.dialogs
package im.vector.riotredesign.features.home.room.detail.timeline

import android.content.Context
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.KotlinModel

internal class DialogSendItemAdapter(context: Context, items: MutableList<DialogListItem>) : DialogAdapter(context) {
class BlankItem
: KotlinModel(R.layout.item_timeline_event_blank) {

init {
addAll(items)
override fun bind() {
//no-op
}
}

}

View File

@ -14,24 +14,19 @@
* limitations under the License.
*/

package im.vector.riotx.core.dialogs
package im.vector.riotredesign.features.home.room.detail.timeline

import android.view.View
import android.widget.ImageView
import android.widget.TextView
import butterknife.BindView
import butterknife.ButterKnife
import im.vector.riotx.R
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.KotlinModel

class DialogListItemHolder(view: View) {
data class DaySeparatorItem(
val formattedDay: CharSequence
) : KotlinModel(R.layout.item_timeline_event_day_separator) {

@BindView(R.id.adapter_item_dialog_icon)
lateinit var icon: ImageView
private val dayTextView by bind<TextView>(R.id.itemDayTextView)

@BindView(R.id.adapter_item_dialog_text)
lateinit var text: TextView

init {
ButterKnife.bind(this, view)
override fun bind() {
dayTextView.text = formattedDay
}
}

View File

@ -14,19 +14,19 @@
* limitations under the License.
*/

package im.vector.riotx.features.home.room.list
package im.vector.riotredesign.features.home.room.detail.timeline

import androidx.recyclerview.widget.DefaultItemAnimator
import android.widget.TextView
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.KotlinModel

private const val ANIM_DURATION_IN_MILLIS = 200L
class DefaultItem(
val text: CharSequence? = null
) : KotlinModel(R.layout.item_timeline_event_default) {

class RoomListAnimator : DefaultItemAnimator() {
private val messageView by bind<TextView>(R.id.stateMessageView)

init {
addDuration = ANIM_DURATION_IN_MILLIS
removeDuration = ANIM_DURATION_IN_MILLIS
moveDuration = 0
changeDuration = 0
override fun bind() {
messageView.text = text
}

}

View File

@ -13,16 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.worker

import android.content.Context
import androidx.work.Worker
import androidx.work.WorkerParameters
package im.vector.riotredesign.features.home.room.detail.timeline

internal class AlwaysSuccessfulWorker(context: Context, params: WorkerParameters)
: Worker(context, params) {
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent

override fun doWork(): Result {
return Result.success()
class DefaultItemFactory {

fun create(event: TimelineEvent): DefaultItem? {
val text = "${event.root.type} events are not yet handled"
return DefaultItem(text = text)
}

}

View File

@ -0,0 +1,40 @@
/*
* 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

@ -14,12 +14,11 @@
* limitations under the License.
*/

package im.vector.matrix.android.internal.session.content
package im.vector.riotredesign.features.home.room.detail.timeline

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class ContentUploadResponse(
@Json(name = "content_uri") val contentUri: String
data class MessageInformationData(
val time: CharSequence? = null,
val avatarUrl: String?,
val memberName: CharSequence? = null,
val showInformation: Boolean = true
)

View File

@ -0,0 +1,110 @@
/*
* 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

@ -0,0 +1,39 @@
/*
* 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

@ -0,0 +1,37 @@
/*
* 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
import im.vector.riotredesign.features.home.AvatarRenderer

class NoticeItem(private val noticeText: CharSequence? = null,
private val avatarUrl: String?,
private val memberName: CharSequence? = null)
: KotlinModel(R.layout.item_timeline_event_notice) {

private val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView)
private val noticeTextView by bind<TextView>(R.id.itemNoticeTextView)

override fun bind() {
noticeTextView.text = noticeText
AvatarRenderer.render(avatarUrl, memberName?.toString(), avatarImageView)
}
}

View File

@ -0,0 +1,39 @@
/*
*
* * 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

@ -0,0 +1,113 @@
/*
* 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.TextUtils
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringProvider


//TODO : complete with call membership events
class RoomMemberItemFactory(private val stringProvider: StringProvider) {

fun create(event: TimelineEvent): NoticeItem? {
val roomMember = event.roomMember ?: return null
val noticeText = buildRoomMemberNotice(event) ?: return null
return NoticeItem(noticeText, roomMember.avatarUrl, roomMember.displayName)
}

private fun buildRoomMemberNotice(event: TimelineEvent): String? {
val eventContent: RoomMember? = event.root.content.toModel()
val prevEventContent: RoomMember? = event.root.prevContent.toModel()
val isMembershipEvent = prevEventContent?.membership != eventContent?.membership
return if (isMembershipEvent) {
buildMembershipNotice(event, eventContent, prevEventContent)
} else {
buildProfileNotice(event, eventContent, prevEventContent)
}
}

private fun buildProfileNotice(event: TimelineEvent, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
val displayText = StringBuilder()
// Check display name has been changed
if (!TextUtils.equals(eventContent?.displayName, prevEventContent?.displayName)) {
val displayNameText = when {
prevEventContent?.displayName.isNullOrEmpty() -> stringProvider.getString(R.string.notice_display_name_set, event.root.sender, eventContent?.displayName)
eventContent?.displayName.isNullOrEmpty() -> stringProvider.getString(R.string.notice_display_name_removed, event.root.sender, prevEventContent?.displayName)
else -> stringProvider.getString(R.string.notice_display_name_changed_from, event.root.sender, prevEventContent?.displayName, eventContent?.displayName)
}
displayText.append(displayNameText)
}
// Check whether the avatar has been changed
if (!TextUtils.equals(eventContent?.avatarUrl, prevEventContent?.avatarUrl)) {
val displayAvatarText = if (displayText.isNotEmpty()) {
displayText.append(" ")
stringProvider.getString(R.string.notice_avatar_changed_too)
} else {
stringProvider.getString(R.string.notice_avatar_url_changed, event.roomMember?.displayName)
}
displayText.append(displayAvatarText)
}
return displayText.toString()
}

private fun buildMembershipNotice(event: TimelineEvent, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
val senderDisplayName = event.roomMember?.displayName ?: return null
val targetDisplayName = eventContent?.displayName ?: event.root.sender
return when {
Membership.INVITE == eventContent?.membership -> {
// TODO get userId
val selfUserId: String = ""
when {
eventContent.thirdPartyInvite != null -> stringProvider.getString(R.string.notice_room_third_party_registered_invite, targetDisplayName, eventContent.thirdPartyInvite?.displayName)
TextUtils.equals(event.root.stateKey, selfUserId)
-> stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName)
event.root.stateKey.isNullOrEmpty() -> stringProvider.getString(R.string.notice_room_invite_no_invitee, senderDisplayName)
else -> stringProvider.getString(R.string.notice_room_invite, senderDisplayName, targetDisplayName)
}
}
Membership.JOIN == eventContent?.membership -> stringProvider.getString(R.string.notice_room_join, senderDisplayName)
Membership.LEAVE == eventContent?.membership -> // 2 cases here: this member may have left voluntarily or they may have been "left" by someone else ie. kicked
return if (TextUtils.equals(event.root.sender, event.root.stateKey)) {
if (prevEventContent?.membership == Membership.INVITE) {
stringProvider.getString(R.string.notice_room_reject, senderDisplayName)
} else {
stringProvider.getString(R.string.notice_room_leave, senderDisplayName)
}
} else if (prevEventContent?.membership == Membership.INVITE) {
stringProvider.getString(R.string.notice_room_withdraw, senderDisplayName, targetDisplayName)
} else if (prevEventContent?.membership == Membership.JOIN) {
stringProvider.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName)
} else if (prevEventContent?.membership == Membership.BAN) {
stringProvider.getString(R.string.notice_room_unban, senderDisplayName, targetDisplayName)
} else {
null
}
Membership.BAN == eventContent?.membership -> stringProvider.getString(R.string.notice_room_ban, senderDisplayName, targetDisplayName)
Membership.KNOCK == eventContent?.membership -> stringProvider.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName)
else -> null
}
}


}


View File

@ -0,0 +1,44 @@
/*
* 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.TextUtils
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomNameContent
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringProvider

class RoomNameItemFactory(private val stringProvider: StringProvider) {

fun create(event: TimelineEvent): NoticeItem? {

val content: RoomNameContent? = event.root.content.toModel()
val roomMember = event.roomMember
if (content == null || roomMember == null) {
return null
}
val text = if (!TextUtils.isEmpty(content.name)) {
stringProvider.getString(R.string.notice_room_name_changed, roomMember.displayName, content.name)
} else {
stringProvider.getString(R.string.notice_room_name_removed, roomMember.displayName)
}
return NoticeItem(text, roomMember.avatarUrl, roomMember.displayName)
}


}

View File

@ -0,0 +1,43 @@
/*
* 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.toModel
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringProvider

class RoomTopicItemFactory(private val stringProvider: StringProvider) {

fun create(event: TimelineEvent): NoticeItem? {

val content: RoomTopicContent? = event.root.content.toModel()
val roomMember = event.roomMember
if (content == null || roomMember == null) {
return null
}
val text = if (content.topic.isNullOrEmpty()) {
stringProvider.getString(R.string.notice_room_topic_removed, roomMember.displayName)
} else {
stringProvider.getString(R.string.notice_room_topic_changed, roomMember.displayName, content.topic)
}
return NoticeItem(text, roomMember.avatarUrl, roomMember.displayName)
}


}

View File

@ -14,24 +14,20 @@
* limitations under the License.
*/

package im.vector.riotx.core.resources
package im.vector.riotredesign.features.home.room.detail.timeline

import org.threeten.bp.Instant
import im.vector.riotredesign.core.resources.LocaleProvider
import org.threeten.bp.LocalDateTime
import org.threeten.bp.ZoneId
import org.threeten.bp.format.DateTimeFormatter

object DateProvider {
class TimelineDateFormatter(private val localeProvider: LocaleProvider) {

private val zoneId = ZoneId.systemDefault()

fun toLocalDateTime(timestamp: Long?): LocalDateTime {
val instant = Instant.ofEpochMilli(timestamp ?: 0)
return LocalDateTime.ofInstant(instant, zoneId)
fun formatMessageHour(localDateTime: LocalDateTime): String {
return DateTimeFormatter.ofPattern("H:mm", localeProvider.current()).format(localDateTime)
}

fun currentLocalDateTime(): LocalDateTime {
val instant = Instant.now()
return LocalDateTime.ofInstant(instant, zoneId)
fun formatMessageDay(localDateTime: LocalDateTime): String {
return DateTimeFormatter.ofPattern("EEE d MMM", localeProvider.current()).format(localDateTime)
}

}

View File

@ -0,0 +1,114 @@
/*
* 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

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

@ -14,14 +14,11 @@
* limitations under the License.
*/

package im.vector.riotx.features.home.room.detail.timeline.helper
package im.vector.riotredesign.features.home.room.detail.timeline.helper

import androidx.recyclerview.widget.RecyclerView
import im.vector.riotx.core.di.ScreenScope
import javax.inject.Inject

@ScreenScope
class TimelineMediaSizeProvider @Inject constructor() {
class TimelineMediaSizeProvider {

lateinit var recyclerView: RecyclerView
private var cachedSize: Pair<Int, Int>? = null

View File

@ -0,0 +1,129 @@
/*
* 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

@ -0,0 +1,149 @@
/*
* 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

@ -0,0 +1,52 @@
/*
* 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

@ -14,12 +14,16 @@
* limitations under the License.
*/

package im.vector.riotx.features.home
package im.vector.riotredesign.features.home.room.list

import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotx.core.utils.RxStore
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class HomeRoomListObservableStore @Inject constructor() : RxStore<List<RoomSummary>>()
sealed class RoomListActions {

data class SelectRoom(val roomSummary: RoomSummary) : RoomListActions()

object RoomDisplayed : RoomListActions()

data class FilterRooms(val roomName: CharSequence? = null) : RoomListActions()

}

View File

@ -0,0 +1,113 @@
/*
* 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.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.activityViewModel
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.setupAsSearch
import im.vector.riotredesign.core.platform.RiotFragment
import im.vector.riotredesign.core.platform.StateView
import im.vector.riotredesign.features.home.HomeNavigator
import kotlinx.android.synthetic.main.fragment_room_list.*
import org.koin.android.ext.android.inject

class RoomListFragment : RiotFragment(), RoomSummaryController.Callback {

companion object {
fun newInstance(): RoomListFragment {
return RoomListFragment()
}
}

private val homeNavigator by inject<HomeNavigator>()
private val roomController by inject<RoomSummaryController>()
private val homeViewModel: RoomListViewModel by activityViewModel()

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_room_list, container, false)
}

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
roomController.callback = this
stateView.contentView = epoxyRecyclerView
epoxyRecyclerView.setController(roomController)
setupFilterView()
homeViewModel.subscribe { renderState(it) }
}

private fun renderState(state: RoomListViewState) {
when (state.asyncRooms) {
is Incomplete -> renderLoading()
is Success -> renderSuccess(state)
is Fail -> renderFailure(state.asyncRooms.error)
}
}

private fun renderSuccess(state: RoomListViewState) {
if (state.asyncRooms().isNullOrEmpty()) {
stateView.state = StateView.State.Empty(getString(R.string.room_list_empty))
} else {
stateView.state = StateView.State.Content
}
roomController.setData(state)
}

private fun renderLoading() {
stateView.state = StateView.State.Loading
}

private fun renderFailure(error: Throwable) {
val message = when (error) {
is Failure.NetworkConnection -> getString(R.string.error_no_network)
else -> getString(R.string.error_common)
}
stateView.state = StateView.State.Error(message)
}

private fun setupFilterView() {
filterRoomView.setupAsSearch()
filterRoomView.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) = Unit

override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit

override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
homeViewModel.accept(RoomListActions.FilterRooms(s))
}
})
}

// RoomSummaryController.Callback **************************************************************

override fun onRoomSelected(room: RoomSummary) {
homeViewModel.accept(RoomListActions.SelectRoom(room))
homeNavigator.openRoomDetail(room.roomId, null)
}

}

View File

@ -0,0 +1,171 @@
/*
* 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 arrow.core.Option
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.session.Session
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.tag.RoomTag
import im.vector.matrix.rx.rx
import im.vector.riotredesign.core.platform.RiotViewModel
import im.vector.riotredesign.features.home.group.SelectedGroupHolder
import im.vector.riotredesign.features.home.room.VisibleRoomHolder
import io.reactivex.Observable
import io.reactivex.functions.Function3
import io.reactivex.rxkotlin.subscribeBy
import org.koin.android.ext.android.get
import java.util.concurrent.TimeUnit

typealias RoomListFilterName = CharSequence

class RoomListViewModel(initialState: RoomListViewState,
private val session: Session,
private val selectedGroupHolder: SelectedGroupHolder,
private val visibleRoomHolder: VisibleRoomHolder,
private val roomSelectionRepository: RoomSelectionRepository,
private val roomSummaryComparator: RoomSummaryComparator)
: RiotViewModel<RoomListViewState>(initialState) {

companion object : MvRxViewModelFactory<RoomListViewModel, RoomListViewState> {

@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: RoomListViewState): RoomListViewModel? {
val currentSession = Matrix.getInstance().currentSession
val roomSelectionRepository = viewModelContext.activity.get<RoomSelectionRepository>()
val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupHolder>()
val visibleRoomHolder = viewModelContext.activity.get<VisibleRoomHolder>()
val roomSummaryComparator = viewModelContext.activity.get<RoomSummaryComparator>()
return RoomListViewModel(state, currentSession, selectedGroupHolder, visibleRoomHolder, roomSelectionRepository, roomSummaryComparator)
}
}


private val roomListFilter = BehaviorRelay.createDefault<Option<RoomListFilterName>>(Option.empty())

init {
observeRoomSummaries()
observeVisibleRoom()
}

fun accept(action: RoomListActions) {
when (action) {
is RoomListActions.SelectRoom -> handleSelectRoom(action)
is RoomListActions.FilterRooms -> handleFilterRooms(action)
}
}

// PRIVATE METHODS *****************************************************************************

private fun handleSelectRoom(action: RoomListActions.SelectRoom) = withState { state ->
if (state.selectedRoomId != action.roomSummary.roomId) {
roomSelectionRepository.saveLastSelectedRoom(action.roomSummary.roomId)
}
}

private fun handleFilterRooms(action: RoomListActions.FilterRooms) {
val optionalFilter = Option.fromNullable(action.roomName)
roomListFilter.accept(optionalFilter)
}

private fun observeVisibleRoom() {
visibleRoomHolder.visibleRoom()
.subscribeBy {
setState { copy(selectedRoomId = it) }
}
.disposeOnClear()
}

private fun observeRoomSummaries() {
Observable.combineLatest<List<RoomSummary>, Option<GroupSummary>, Option<RoomListFilterName>, RoomSummaries>(
session.rx().liveRoomSummaries().throttleLast(300, TimeUnit.MILLISECONDS),
selectedGroupHolder.selectedGroup(),
roomListFilter.throttleLast(300, TimeUnit.MILLISECONDS),
Function3 { rooms, selectedGroupOption, filterRoomOption ->
val filteredRooms = filterRooms(rooms, filterRoomOption)
val selectedGroup = selectedGroupOption.orNull()
val filteredDirectRooms = filteredRooms
.filter { it.isDirect }
.filter {
if (selectedGroup == null) {
true
} else {
it.otherMemberIds
.intersect(selectedGroup.userIds)
.isNotEmpty()
}
}

val filteredGroupRooms = filteredRooms
.filter { !it.isDirect }
.filter {
selectedGroup?.roomIds?.contains(it.roomId) ?: true
}
buildRoomSummaries(filteredDirectRooms + filteredGroupRooms)
}
)
.execute { async ->
copy(
asyncRooms = async
)
}
}

private fun filterRooms(rooms: List<RoomSummary>, filterRoomOption: Option<RoomListFilterName>): List<RoomSummary> {
val filterRoom = filterRoomOption.orNull()
return rooms.filter {
if (filterRoom.isNullOrBlank()) {
true
} else {
it.displayName.contains(other = filterRoom, ignoreCase = true)
}
}
}

private fun buildRoomSummaries(rooms: List<RoomSummary>): RoomSummaries {
val favourites = ArrayList<RoomSummary>()
val directChats = ArrayList<RoomSummary>()
val groupRooms = ArrayList<RoomSummary>()
val lowPriorities = ArrayList<RoomSummary>()
val serverNotices = ArrayList<RoomSummary>()

for (room in rooms) {
val tags = room.tags.map { it.name }
when {
tags.contains(RoomTag.ROOM_TAG_SERVER_NOTICE) -> serverNotices.add(room)
tags.contains(RoomTag.ROOM_TAG_FAVOURITE) -> favourites.add(room)
tags.contains(RoomTag.ROOM_TAG_LOW_PRIORITY) -> lowPriorities.add(room)
room.isDirect -> directChats.add(room)
else -> groupRooms.add(room)
}
}

return RoomSummaries(
favourites = favourites.sortedWith(roomSummaryComparator),
directRooms = directChats.sortedWith(roomSummaryComparator),
groupRooms = groupRooms.sortedWith(roomSummaryComparator),
lowPriorities = lowPriorities.sortedWith(roomSummaryComparator),
serverNotices = serverNotices.sortedWith(roomSummaryComparator)
)
}


}

View File

@ -0,0 +1,39 @@
/*
* 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

@ -14,21 +14,23 @@
* limitations under the License.
*/

package im.vector.riotx.features.home.room.detail.timeline.item
package im.vector.riotredesign.features.home.room.list

import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import android.content.SharedPreferences

@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class RedactedMessageItem : AbsMessageItem<RedactedMessageItem.Holder>() {
private const val SHARED_PREFS_SELECTED_ROOM_KEY = "SHARED_PREFS_SELECTED_ROOM_KEY"

override fun getViewType() = STUB_ID
class RoomSelectionRepository(private val sharedPreferences: SharedPreferences) {

override fun shouldShowReactionAtBottom() = false

class Holder : AbsMessageItem.Holder(STUB_ID)

companion object {
private const val STUB_ID = R.id.messageContentRedactedStub
fun lastSelectedRoom(): String? {
return sharedPreferences.getString(SHARED_PREFS_SELECTED_ROOM_KEY, null)
}

fun saveLastSelectedRoom(roomId: String) {
sharedPreferences.edit()
.putString(SHARED_PREFS_SELECTED_ROOM_KEY, roomId)
.apply()
}

}

View File

@ -0,0 +1,70 @@
/*
* 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 im.vector.matrix.android.api.session.room.model.RoomSummary

class RoomSummaryComparator
: Comparator<RoomSummary> {

override fun compare(leftRoomSummary: RoomSummary?, rightRoomSummary: RoomSummary?): Int {
val retValue: Int
var leftHighlightCount = 0
var rightHighlightCount = 0
var leftNotificationCount = 0
var rightNotificationCount = 0
var rightTimestamp = 0L
var leftTimestamp = 0L

if (null != leftRoomSummary) {
leftHighlightCount = leftRoomSummary.highlightCount
leftNotificationCount = leftRoomSummary.notificationCount
leftTimestamp = leftRoomSummary.lastMessage?.originServerTs ?: 0
}
if (null != rightRoomSummary) {
rightHighlightCount = rightRoomSummary.highlightCount
rightNotificationCount = rightRoomSummary.notificationCount
rightTimestamp = rightRoomSummary.lastMessage?.originServerTs ?: 0
}

if (leftRoomSummary?.lastMessage == null) {
retValue = 1
} else if (rightRoomSummary?.lastMessage == null) {
retValue = -1
} else if (rightHighlightCount > 0 && leftHighlightCount == 0) {
retValue = 1
} else if (rightHighlightCount == 0 && leftHighlightCount > 0) {
retValue = -1
} else if (rightNotificationCount > 0 && leftNotificationCount == 0) {
retValue = 1
} else if (rightNotificationCount == 0 && leftNotificationCount > 0) {
retValue = -1
} else {
val deltaTimestamp = rightTimestamp - leftTimestamp
if (deltaTimestamp > 0) {
retValue = 1
} else if (deltaTimestamp < 0) {
retValue = -1
} else {
retValue = 0
}
}
return retValue

}

}

View File

@ -0,0 +1,124 @@
/*
* 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

@ -14,7 +14,7 @@
* limitations under the License.
*/

package im.vector.riotx.features.home.room.list
package im.vector.riotredesign.features.home.room.list

object RoomSummaryFormatter {

@ -26,7 +26,7 @@ object RoomSummaryFormatter {
*/
fun formatUnreadMessagesCounter(count: Int): String {
return if (count > 999) {
"${count / 1000}.${count % 1000 / 100}k"
"${count / 1000}.${count % 1000 / 100}K"
} else {
count.toString()
}

View File

@ -0,0 +1,48 @@
/*
* 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)
}
}

View File

@ -13,14 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.list
package im.vector.riotredesign.features.home.room.list

import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.appcompat.widget.AppCompatTextView
import im.vector.riotx.R
import im.vector.riotx.features.themes.ThemeUtils
import im.vector.riotredesign.R

class UnreadCounterBadgeView : AppCompatTextView {

@ -30,24 +29,24 @@ class UnreadCounterBadgeView : AppCompatTextView {

constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

fun render(state: State) {
if (state.count == 0) {
fun render(count: Int, highlighted: Boolean) {
if (count == 0) {
visibility = View.INVISIBLE
} else {
visibility = View.VISIBLE
val bgRes = if (state.highlighted) {
val bgRes = if (highlighted) {
R.drawable.bg_unread_highlight
} else {
ThemeUtils.getResourceId(context, R.drawable.bg_unread_notification_light)
R.drawable.bg_unread_notification
}
setBackgroundResource(bgRes)
text = RoomSummaryFormatter.formatUnreadMessagesCounter(state.count)
text = RoomSummaryFormatter.formatUnreadMessagesCounter(count)
}
}

data class State(
val count: Int,
val highlighted: Boolean
)
enum class Status {
NOTIFICATION,
HIGHLIGHT
}

}

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