forked from GitHub-Mirror/riotX-android
Compare commits
5 Commits
feature/cs
...
feature/re
Author | SHA1 | Date | |
---|---|---|---|
4048658c56 | |||
8d13f08574 | |||
a5032920c2 | |||
68b9ba7b99 | |||
56af92d759 |
@ -1,41 +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 Debug version"
|
|
||||||
agents:
|
|
||||||
# We use a medium sized instance instead of the normal small ones because
|
|
||||||
# gradle build is long
|
|
||||||
queue: "medium"
|
|
||||||
commands:
|
|
||||||
- "./gradlew clean lintGplayRelease assembleGplayDebug --stacktrace"
|
|
||||||
- "./gradlew lintFdroidRelease assembleFdroidDebug --stacktrace"
|
|
||||||
artifact_paths:
|
|
||||||
- "vector/build/outputs/apk/gplay/debug/*.apk"
|
|
||||||
- "vector/build/outputs/apk/fdroid/debug/*.apk"
|
|
||||||
branches: "develop feature/*"
|
|
||||||
plugins:
|
|
||||||
- docker#v3.1.0:
|
|
||||||
image: "runmymind/docker-android-sdk"
|
|
||||||
|
|
||||||
- label: "Build Google Play unsigned APK"
|
|
||||||
agents:
|
|
||||||
# We use a medium sized instance instead of the normal small ones because
|
|
||||||
# gradle build is long
|
|
||||||
queue: "medium"
|
|
||||||
commands:
|
|
||||||
- "./gradlew clean assembleGplayRelease --stacktrace"
|
|
||||||
artifact_paths:
|
|
||||||
- "vector/build/outputs/apk/gplay/release/*.apk"
|
|
||||||
branches: "master"
|
|
||||||
plugins:
|
|
||||||
- docker#v3.1.0:
|
|
||||||
image: "runmymind/docker-android-sdk"
|
|
||||||
|
|
||||||
# Code quality
|
|
||||||
|
|
||||||
- label: "Code quality"
|
|
||||||
command: "./tools/check/check_code_quality.sh"
|
|
10
.github/PULL_REQUEST_TEMPLATE.md
vendored
10
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -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
2
.gitignore
vendored
@ -10,5 +10,3 @@
|
|||||||
/build
|
/build
|
||||||
/captures
|
/captures
|
||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
|
|
||||||
/tmp
|
|
||||||
|
29
.idea/codeStyles/Project.xml
generated
Normal file
29
.idea/codeStyles/Project.xml
generated
Normal 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
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal 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
13
.idea/dictionaries/ganfra.xml
generated
Normal 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
20
.idea/gradle.xml
generated
Normal 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>
|
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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
34
.idea/misc.xml
generated
Normal 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
12
.idea/runConfigurations.xml
generated
Normal 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>
|
60
.travis.yml
60
.travis.yml
@ -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
|
|
49
CHANGES.md
49
CHANGES.md
@ -1,49 +0,0 @@
|
|||||||
Changes in RiotX 0.XX (2019-XX-XX)
|
|
||||||
===================================================
|
|
||||||
|
|
||||||
Features:
|
|
||||||
- Contextual action menu for messages in room
|
|
||||||
|
|
||||||
Improvements:
|
|
||||||
-
|
|
||||||
|
|
||||||
Other changes:
|
|
||||||
-
|
|
||||||
|
|
||||||
Bugfix:
|
|
||||||
-
|
|
||||||
|
|
||||||
Translations:
|
|
||||||
-
|
|
||||||
|
|
||||||
Build:
|
|
||||||
-
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
=======================================================
|
|
||||||
+ TEMPLATE WHEN PREPARING A NEW RELEASE +
|
|
||||||
=======================================================
|
|
||||||
|
|
||||||
|
|
||||||
Changes in RiotX 0.XX (2019-XX-XX)
|
|
||||||
===================================================
|
|
||||||
|
|
||||||
Features:
|
|
||||||
-
|
|
||||||
|
|
||||||
Improvements:
|
|
||||||
-
|
|
||||||
|
|
||||||
Other changes:
|
|
||||||
-
|
|
||||||
|
|
||||||
Bugfix:
|
|
||||||
-
|
|
||||||
|
|
||||||
Translations:
|
|
||||||
-
|
|
||||||
|
|
||||||
Build:
|
|
||||||
-
|
|
||||||
|
|
@ -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 [](https://matrix.to/#/#riot-android:matrix.org) room.
|
|
||||||
|
|
||||||
Dedicated room for RiotX: [](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
176
LICENSE
@ -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
|
|
21
README.md
21
README.md
@ -1,20 +1 @@
|
|||||||
[](https://buildkite.com/matrix-dot-org/riotx-android)
|
# riot-android-redesign-PoC
|
||||||
[](https://translate.riot.im/engage/riot-android/?utm_source=widget)
|
|
||||||
[](https://matrix.to/#/#riotx:matrix.org)
|
|
||||||
[](https://sonarcloud.io/dashboard?id=vector.android.riotx)
|
|
||||||
[](https://sonarcloud.io/dashboard?id=vector.android.riotx)
|
|
||||||
[](https://sonarcloud.io/dashboard?id=vector.android.riotx)
|
|
||||||
|
|
||||||
# RiotX Android
|
|
||||||
|
|
||||||
RiotX is an Android Matrix Client currently in development. The application is not yet available on the PlayStore.
|
|
||||||
|
|
||||||
It's based on a new Matrix SDK, written in Kotlin.
|
|
||||||
|
|
||||||
Download nightly build here: [](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)
|
|
||||||
|
|
||||||
Matrix Room: [](https://matrix.to/#/#riotx:matrix.org)
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Please refer to [CONTRIBUTING.md](https://github.com/vector-im/riotX-android/blob/develop/CONTRIBUTING.md) if you want to contribute the Matrix on Android projects!
|
|
||||||
|
0
vector/.gitignore → app/.gitignore
vendored
0
vector/.gitignore → app/.gitignore
vendored
105
app/build.gradle
Normal file
105
app/build.gradle
Normal 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'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
31
app/src/main/AndroidManifest.xml
Normal file
31
app/src/main/AndroidManifest.xml
Normal 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>
|
46
app/src/main/java/im/vector/riotredesign/Riot.kt
Normal file
46
app/src/main/java/im/vector/riotredesign/Riot.kt
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -18,15 +18,9 @@ package im.vector.riotredesign.core.di
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Context.MODE_PRIVATE
|
import android.content.Context.MODE_PRIVATE
|
||||||
import im.vector.matrix.android.api.Matrix
|
|
||||||
import im.vector.riotredesign.core.resources.ColorProvider
|
|
||||||
import im.vector.riotredesign.core.resources.LocaleProvider
|
import im.vector.riotredesign.core.resources.LocaleProvider
|
||||||
import im.vector.riotredesign.core.resources.StringProvider
|
import im.vector.riotredesign.core.resources.StringProvider
|
||||||
import im.vector.riotredesign.features.home.group.SelectedGroupStore
|
|
||||||
import im.vector.riotredesign.features.home.room.VisibleRoomStore
|
|
||||||
import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository
|
import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository
|
||||||
import im.vector.riotredesign.features.home.room.list.RoomSummaryComparator
|
|
||||||
import im.vector.riotredesign.features.notifications.NotificationDrawerManager
|
|
||||||
import org.koin.dsl.module.module
|
import org.koin.dsl.module.module
|
||||||
|
|
||||||
class AppModule(private val context: Context) {
|
class AppModule(private val context: Context) {
|
||||||
@ -41,10 +35,6 @@ class AppModule(private val context: Context) {
|
|||||||
StringProvider(context.resources)
|
StringProvider(context.resources)
|
||||||
}
|
}
|
||||||
|
|
||||||
single {
|
|
||||||
ColorProvider(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
single {
|
single {
|
||||||
context.getSharedPreferences("im.vector.riot", MODE_PRIVATE)
|
context.getSharedPreferences("im.vector.riot", MODE_PRIVATE)
|
||||||
}
|
}
|
||||||
@ -53,26 +43,5 @@ class AppModule(private val context: Context) {
|
|||||||
RoomSelectionRepository(get())
|
RoomSelectionRepository(get())
|
||||||
}
|
}
|
||||||
|
|
||||||
single {
|
|
||||||
SelectedGroupStore()
|
|
||||||
}
|
|
||||||
|
|
||||||
single {
|
|
||||||
VisibleRoomStore()
|
|
||||||
}
|
|
||||||
|
|
||||||
single {
|
|
||||||
RoomSummaryComparator()
|
|
||||||
}
|
|
||||||
|
|
||||||
single {
|
|
||||||
NotificationDrawerManager(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
factory {
|
|
||||||
Matrix.getInstance().currentSession!!
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -26,15 +26,15 @@ import kotlin.reflect.KProperty
|
|||||||
*
|
*
|
||||||
* See [SampleKotlinModelWithHolder] for a usage example.
|
* See [SampleKotlinModelWithHolder] for a usage example.
|
||||||
*/
|
*/
|
||||||
abstract class VectorEpoxyHolder : EpoxyHolder() {
|
abstract class KotlinEpoxyHolder : EpoxyHolder() {
|
||||||
lateinit var view: View
|
private lateinit var view: View
|
||||||
|
|
||||||
override fun bindView(itemView: View) {
|
override fun bindView(itemView: View) {
|
||||||
view = itemView
|
view = itemView
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun <V : View> bind(id: Int): ReadOnlyProperty<VectorEpoxyHolder, V> =
|
protected fun <V : View> bind(id: Int): ReadOnlyProperty<KotlinEpoxyHolder, V> =
|
||||||
Lazy { holder: VectorEpoxyHolder, prop ->
|
Lazy { holder: KotlinEpoxyHolder, prop ->
|
||||||
holder.view.findViewById(id) as V?
|
holder.view.findViewById(id) as V?
|
||||||
?: throw IllegalStateException("View ID $id for '${prop.name}' not found.")
|
?: throw IllegalStateException("View ID $id for '${prop.name}' not found.")
|
||||||
}
|
}
|
||||||
@ -44,13 +44,13 @@ abstract class VectorEpoxyHolder : EpoxyHolder() {
|
|||||||
* https://github.com/JakeWharton/kotterknife
|
* https://github.com/JakeWharton/kotterknife
|
||||||
*/
|
*/
|
||||||
private class Lazy<V>(
|
private class Lazy<V>(
|
||||||
private val initializer: (VectorEpoxyHolder, KProperty<*>) -> V
|
private val initializer: (KotlinEpoxyHolder, KProperty<*>) -> V
|
||||||
) : ReadOnlyProperty<VectorEpoxyHolder, V> {
|
) : ReadOnlyProperty<KotlinEpoxyHolder, V> {
|
||||||
private object EMPTY
|
private object EMPTY
|
||||||
|
|
||||||
private var value: Any? = 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) {
|
if (value == EMPTY) {
|
||||||
value = initializer(thisRef, property)
|
value = initializer(thisRef, property)
|
||||||
}
|
}
|
@ -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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -29,8 +29,4 @@ fun AppCompatActivity.replaceFragment(fragment: Fragment, frameId: Int) {
|
|||||||
|
|
||||||
fun AppCompatActivity.addFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
|
fun AppCompatActivity.addFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
|
||||||
supportFragmentManager.inTransaction { replace(frameId, fragment).addToBackStack(tag) }
|
supportFragmentManager.inTransaction { replace(frameId, fragment).addToBackStack(tag) }
|
||||||
}
|
|
||||||
|
|
||||||
fun AppCompatActivity.hideKeyboard() {
|
|
||||||
currentFocus?.hideKeyboard()
|
|
||||||
}
|
}
|
@ -13,10 +13,10 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package im.vector.matrix.android.api.session.events.model
|
|
||||||
|
package im.vector.riotredesign.core.extensions
|
||||||
|
|
||||||
|
|
||||||
interface UnsignedRelationInfo {
|
fun CharSequence.firstCharAsString(): String {
|
||||||
val limited : Boolean?
|
return if (isNotEmpty()) this[0].toString() else ""
|
||||||
val count: Int?
|
|
||||||
}
|
}
|
@ -17,9 +17,12 @@
|
|||||||
package im.vector.riotredesign.core.extensions
|
package im.vector.riotredesign.core.extensions
|
||||||
|
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.riotredesign.core.resources.DateProvider
|
import org.threeten.bp.Instant
|
||||||
import org.threeten.bp.LocalDateTime
|
import org.threeten.bp.LocalDateTime
|
||||||
|
import org.threeten.bp.ZoneId
|
||||||
|
|
||||||
|
|
||||||
fun Event.localDateTime(): LocalDateTime {
|
fun Event.localDateTime(): LocalDateTime {
|
||||||
return DateProvider.toLocalDateTime(originServerTs)
|
val instant = Instant.ofEpochMilli(originServerTs ?: 0)
|
||||||
|
return LocalDateTime.ofInstant(instant, ZoneId.systemDefault())
|
||||||
}
|
}
|
@ -14,22 +14,19 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.matrix.android.api.util
|
package im.vector.riotredesign.core.platform
|
||||||
|
|
||||||
class CancelableBag : Cancelable {
|
import com.airbnb.mvrx.BaseMvRxActivity
|
||||||
|
import io.reactivex.disposables.CompositeDisposable
|
||||||
|
import io.reactivex.disposables.Disposable
|
||||||
|
|
||||||
private val cancelableList = ArrayList<Cancelable>()
|
abstract class RiotActivity : BaseMvRxActivity() {
|
||||||
|
|
||||||
fun add(cancelable: Cancelable) {
|
private val uiDisposables = CompositeDisposable()
|
||||||
cancelableList.add(cancelable)
|
|
||||||
|
protected fun Disposable.disposeOnDestroy(): Disposable {
|
||||||
|
uiDisposables.add(this)
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cancel() {
|
|
||||||
cancelableList.forEach { it.cancel() }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Cancelable.addTo(cancelables: CancelableBag) {
|
|
||||||
cancelables.add(this)
|
|
||||||
}
|
}
|
@ -14,29 +14,29 @@
|
|||||||
* limitations under the License.
|
* 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 android.os.Parcelable
|
||||||
import kotlinx.android.parcel.Parcelize
|
import com.airbnb.mvrx.BaseMvRxFragment
|
||||||
|
import com.airbnb.mvrx.MvRx
|
||||||
|
|
||||||
@Parcelize
|
abstract class RiotFragment : BaseMvRxFragment(), OnBackPressed {
|
||||||
data class ContentAttachmentData(
|
|
||||||
val size: Long = 0,
|
|
||||||
val duration: Long? = 0,
|
|
||||||
val date: Long = 0,
|
|
||||||
val height: Long? = 0,
|
|
||||||
val width: Long? = 0,
|
|
||||||
val name: String? = null,
|
|
||||||
val path: String,
|
|
||||||
val mimeType: String,
|
|
||||||
val type: Type
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
enum class Type {
|
val riotActivity: RiotActivity by lazy {
|
||||||
FILE,
|
activity as RiotActivity
|
||||||
IMAGE,
|
}
|
||||||
AUDIO,
|
|
||||||
VIDEO
|
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) } }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -18,7 +18,6 @@ package im.vector.riotredesign.core.platform
|
|||||||
|
|
||||||
import com.airbnb.mvrx.BaseMvRxViewModel
|
import com.airbnb.mvrx.BaseMvRxViewModel
|
||||||
import com.airbnb.mvrx.MvRxState
|
import com.airbnb.mvrx.MvRxState
|
||||||
import im.vector.riotredesign.BuildConfig
|
|
||||||
|
|
||||||
abstract class VectorViewModel<S : MvRxState>(initialState: S)
|
abstract class RiotViewModel<S : MvRxState>(initialState: S)
|
||||||
: BaseMvRxViewModel<S>(initialState, debugMode = BuildConfig.DEBUG)
|
: BaseMvRxViewModel<S>(initialState, debugMode = false)
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -16,58 +16,39 @@
|
|||||||
|
|
||||||
package im.vector.riotredesign.features.home
|
package im.vector.riotredesign.features.home
|
||||||
|
|
||||||
import android.app.ProgressDialog
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.Gravity
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.core.view.GravityCompat
|
import androidx.core.view.GravityCompat
|
||||||
import androidx.drawerlayout.widget.DrawerLayout
|
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import com.airbnb.mvrx.viewModel
|
import com.airbnb.mvrx.viewModel
|
||||||
import im.vector.matrix.android.api.Matrix
|
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotredesign.core.extensions.hideKeyboard
|
|
||||||
import im.vector.riotredesign.core.extensions.observeEvent
|
import im.vector.riotredesign.core.extensions.observeEvent
|
||||||
import im.vector.riotredesign.core.extensions.replaceFragment
|
import im.vector.riotredesign.core.extensions.replaceFragment
|
||||||
import im.vector.riotredesign.core.platform.OnBackPressed
|
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.core.platform.ToolbarConfigurable
|
||||||
import im.vector.riotredesign.core.platform.VectorBaseActivity
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.LoadingRoomDetailFragment
|
import im.vector.riotredesign.features.home.room.detail.LoadingRoomDetailFragment
|
||||||
import im.vector.riotredesign.features.rageshake.BugReporter
|
|
||||||
import im.vector.riotredesign.features.rageshake.VectorUncaughtExceptionHandler
|
|
||||||
import im.vector.riotredesign.features.settings.VectorSettingsActivity
|
|
||||||
import im.vector.riotredesign.features.workers.signout.SignOutUiWorker
|
|
||||||
import kotlinx.android.synthetic.main.activity_home.*
|
import kotlinx.android.synthetic.main.activity_home.*
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.android.scope.ext.android.bindScope
|
import org.koin.standalone.StandAloneContext.loadKoinModules
|
||||||
import org.koin.android.scope.ext.android.getOrCreateScope
|
|
||||||
|
|
||||||
|
|
||||||
class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
class HomeActivity : RiotActivity(), ToolbarConfigurable {
|
||||||
|
|
||||||
|
|
||||||
private val homeActivityViewModel: HomeActivityViewModel by viewModel()
|
private val homeActivityViewModel: HomeActivityViewModel by viewModel()
|
||||||
private val homeNavigator by inject<HomeNavigator>()
|
private val homeNavigator by inject<HomeNavigator>()
|
||||||
|
|
||||||
private var progress: ProgressDialog? = null
|
|
||||||
|
|
||||||
private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
|
|
||||||
override fun onDrawerStateChanged(newState: Int) {
|
|
||||||
hideKeyboard()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getLayoutRes() = R.layout.activity_home
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
loadKoinModules(listOf(HomeModule().definition))
|
||||||
bindScope(getOrCreateScope(HomeModule.HOME_SCOPE))
|
|
||||||
homeNavigator.activity = this
|
homeNavigator.activity = this
|
||||||
drawerLayout.addDrawerListener(drawerListener)
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_home)
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
val homeDrawerFragment = HomeDrawerFragment.newInstance()
|
val homeDrawerFragment = HomeDrawerFragment.newInstance()
|
||||||
val loadingDetail = LoadingRoomDetailFragment.newInstance()
|
val loadingDetail = LoadingRoomDetailFragment.newInstance()
|
||||||
@ -77,40 +58,13 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
|||||||
homeActivityViewModel.openRoomLiveData.observeEvent(this) {
|
homeActivityViewModel.openRoomLiveData.observeEvent(this) {
|
||||||
homeNavigator.openRoomDetail(it, null)
|
homeNavigator.openRoomDetail(it, null)
|
||||||
}
|
}
|
||||||
homeActivityViewModel.isLoading.observe(this, Observer<Boolean> {
|
|
||||||
// TODO better UI
|
|
||||||
if (it) {
|
|
||||||
progress?.dismiss()
|
|
||||||
progress = ProgressDialog(this)
|
|
||||||
progress?.setMessage(getString(R.string.room_recents_create_room))
|
|
||||||
progress?.show()
|
|
||||||
} else {
|
|
||||||
progress?.dismiss()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
drawerLayout.removeDrawerListener(drawerListener)
|
|
||||||
homeNavigator.activity = null
|
homeNavigator.activity = null
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
|
|
||||||
if (VectorUncaughtExceptionHandler.didAppCrash(this)) {
|
|
||||||
VectorUncaughtExceptionHandler.clearAppCrashStatus(this)
|
|
||||||
|
|
||||||
AlertDialog.Builder(this)
|
|
||||||
.setMessage(R.string.send_bug_report_app_crashed)
|
|
||||||
.setCancelable(false)
|
|
||||||
.setPositiveButton(R.string.yes) { _, _ -> BugReporter.openBugReportScreen(this) }
|
|
||||||
.setNegativeButton(R.string.no) { _, _ -> BugReporter.deleteCrashFile(this) }
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun configure(toolbar: Toolbar) {
|
override fun configure(toolbar: Toolbar) {
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
supportActionBar?.setHomeButtonEnabled(true)
|
supportActionBar?.setHomeButtonEnabled(true)
|
||||||
@ -120,35 +74,20 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
|||||||
drawerToggle.syncState()
|
drawerToggle.syncState()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMenuRes() = R.menu.home
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
android.R.id.home -> {
|
android.R.id.home -> {
|
||||||
drawerLayout.openDrawer(GravityCompat.START)
|
drawerLayout.openDrawer(GravityCompat.START)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.sliding_menu_settings -> {
|
|
||||||
startActivity(VectorSettingsActivity.getIntent(this, "TODO"))
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
R.id.sliding_menu_sign_out -> {
|
|
||||||
SignOutUiWorker(this).perform(Matrix.getInstance().currentSession!!)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// TODO Temporary code here to create a room
|
|
||||||
R.id.tmp_menu_create_room -> {
|
|
||||||
homeActivityViewModel.createRoom()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
if (drawerLayout.isDrawerOpen(Gravity.LEFT)) {
|
||||||
drawerLayout.closeDrawer(GravityCompat.START)
|
drawerLayout.closeDrawer(Gravity.LEFT)
|
||||||
} else {
|
} else {
|
||||||
val handled = recursivelyDispatchOnBackPressed(supportFragmentManager)
|
val handled = recursivelyDispatchOnBackPressed(supportFragmentManager)
|
||||||
if (!handled) {
|
if (!handled) {
|
@ -22,44 +22,36 @@ import com.airbnb.mvrx.MvRxState
|
|||||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
import im.vector.matrix.android.api.Matrix
|
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.Session
|
||||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
|
||||||
import im.vector.matrix.android.api.session.sync.FilterService
|
|
||||||
import im.vector.matrix.rx.rx
|
import im.vector.matrix.rx.rx
|
||||||
import im.vector.riotredesign.core.platform.VectorViewModel
|
import im.vector.riotredesign.core.platform.RiotViewModel
|
||||||
import im.vector.riotredesign.core.utils.LiveEvent
|
import im.vector.riotredesign.core.utils.LiveEvent
|
||||||
import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository
|
import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository
|
||||||
import io.reactivex.rxkotlin.subscribeBy
|
import io.reactivex.rxkotlin.subscribeBy
|
||||||
import org.koin.android.ext.android.get
|
import org.koin.android.ext.android.get
|
||||||
|
|
||||||
data class EmptyState(val isEmpty: Boolean = true) : MvRxState
|
class EmptyState : MvRxState
|
||||||
|
|
||||||
class HomeActivityViewModel(state: EmptyState,
|
class HomeActivityViewModel(state: EmptyState,
|
||||||
private val session: Session,
|
private val session: Session,
|
||||||
roomSelectionRepository: RoomSelectionRepository
|
roomSelectionRepository: RoomSelectionRepository
|
||||||
) : VectorViewModel<EmptyState>(state), Session.Listener {
|
) : RiotViewModel<EmptyState>(state) {
|
||||||
|
|
||||||
companion object : MvRxViewModelFactory<HomeActivityViewModel, EmptyState> {
|
companion object : MvRxViewModelFactory<HomeActivityViewModel, EmptyState> {
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
override fun create(viewModelContext: ViewModelContext, state: EmptyState): HomeActivityViewModel? {
|
override fun create(viewModelContext: ViewModelContext, state: EmptyState): HomeActivityViewModel? {
|
||||||
val session = Matrix.getInstance().currentSession!!
|
val session = Matrix.getInstance().currentSession
|
||||||
val roomSelectionRepository = viewModelContext.activity.get<RoomSelectionRepository>()
|
val roomSelectionRepository = viewModelContext.activity.get<RoomSelectionRepository>()
|
||||||
return HomeActivityViewModel(state, session, roomSelectionRepository)
|
return HomeActivityViewModel(state, session, roomSelectionRepository)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val _isLoading = MutableLiveData<Boolean>()
|
|
||||||
val isLoading: LiveData<Boolean>
|
|
||||||
get() = _isLoading
|
|
||||||
|
|
||||||
private val _openRoomLiveData = MutableLiveData<LiveEvent<String>>()
|
private val _openRoomLiveData = MutableLiveData<LiveEvent<String>>()
|
||||||
val openRoomLiveData: LiveData<LiveEvent<String>>
|
val openRoomLiveData: LiveData<LiveEvent<String>>
|
||||||
get() = _openRoomLiveData
|
get() = _openRoomLiveData
|
||||||
|
|
||||||
init {
|
init {
|
||||||
session.addListener(this)
|
|
||||||
val lastSelectedRoomId = roomSelectionRepository.lastSelectedRoom()
|
val lastSelectedRoomId = roomSelectionRepository.lastSelectedRoom()
|
||||||
if (lastSelectedRoomId == null || session.getRoom(lastSelectedRoomId) == null) {
|
if (lastSelectedRoomId == null || session.getRoom(lastSelectedRoomId) == null) {
|
||||||
getTheFirstRoomWhenAvailable()
|
getTheFirstRoomWhenAvailable()
|
||||||
@ -81,35 +73,5 @@ class HomeActivityViewModel(state: EmptyState,
|
|||||||
.disposeOnClear()
|
.disposeOnClear()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createRoom(createRoomParams: CreateRoomParams = CreateRoomParams()) {
|
|
||||||
_isLoading.value = true
|
|
||||||
|
|
||||||
session.createRoom(createRoomParams, object : MatrixCallback<String> {
|
|
||||||
override fun onSuccess(data: String) {
|
|
||||||
_isLoading.value = false
|
|
||||||
// Open room id
|
|
||||||
_openRoomLiveData.postValue(LiveEvent(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
|
||||||
_isLoading.value = false
|
|
||||||
super.onFailure(failure)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared() {
|
|
||||||
super.onCleared()
|
|
||||||
|
|
||||||
session.removeListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ==========================================================================================
|
|
||||||
* Session listener
|
|
||||||
* ========================================================================================== */
|
|
||||||
|
|
||||||
override fun onInvalidToken() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@ -17,13 +17,16 @@
|
|||||||
package im.vector.riotredesign.features.home
|
package im.vector.riotredesign.features.home
|
||||||
|
|
||||||
import android.os.Bundle
|
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.R
|
||||||
import im.vector.riotredesign.core.extensions.replaceChildFragment
|
import im.vector.riotredesign.core.extensions.replaceChildFragment
|
||||||
import im.vector.riotredesign.core.platform.VectorBaseFragment
|
import im.vector.riotredesign.core.platform.RiotFragment
|
||||||
import im.vector.riotredesign.features.home.group.GroupListFragment
|
import im.vector.riotredesign.features.home.group.GroupListFragment
|
||||||
import im.vector.riotredesign.features.home.room.list.RoomListFragment
|
import im.vector.riotredesign.features.home.room.list.RoomListFragment
|
||||||
|
|
||||||
class HomeDrawerFragment : VectorBaseFragment() {
|
class HomeDrawerFragment : RiotFragment() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@ -32,7 +35,9 @@ class HomeDrawerFragment : VectorBaseFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLayoutResId() = R.layout.fragment_home_drawer
|
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?) {
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.riotredesign.features.home
|
package im.vector.riotredesign.features.home
|
||||||
|
|
||||||
import androidx.core.view.GravityCompat
|
import android.view.Gravity
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotredesign.core.extensions.addFragmentToBackstack
|
import im.vector.riotredesign.core.extensions.addFragmentToBackstack
|
||||||
@ -30,21 +30,23 @@ class HomeNavigator {
|
|||||||
|
|
||||||
var activity: HomeActivity? = null
|
var activity: HomeActivity? = null
|
||||||
|
|
||||||
private var rootRoomId: String? = null
|
private var currentRoomId: String? = null
|
||||||
|
|
||||||
fun openRoomDetail(roomId: String,
|
fun openRoomDetail(roomId: String,
|
||||||
eventId: String?,
|
eventId: String?,
|
||||||
addToBackstack: Boolean = false) {
|
addToBackstack: Boolean = false) {
|
||||||
Timber.v("Open room detail $roomId - $eventId - $addToBackstack")
|
Timber.v("Open room detail $roomId - $eventId - $addToBackstack")
|
||||||
|
if (!addToBackstack && isRoomOpened(roomId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
activity?.let {
|
activity?.let {
|
||||||
//TODO enable eventId permalink. It doesn't work enough at the moment.
|
val args = RoomDetailArgs(roomId, eventId)
|
||||||
val args = RoomDetailArgs(roomId)
|
|
||||||
val roomDetailFragment = RoomDetailFragment.newInstance(args)
|
val roomDetailFragment = RoomDetailFragment.newInstance(args)
|
||||||
it.drawerLayout?.closeDrawer(GravityCompat.START)
|
it.drawerLayout?.closeDrawer(Gravity.LEFT)
|
||||||
if (addToBackstack) {
|
if (addToBackstack) {
|
||||||
it.addFragmentToBackstack(roomDetailFragment, R.id.homeDetailFragmentContainer, roomId)
|
it.addFragmentToBackstack(roomDetailFragment, R.id.homeDetailFragmentContainer, roomId)
|
||||||
} else {
|
} else {
|
||||||
rootRoomId = roomId
|
currentRoomId = roomId
|
||||||
clearBackStack(it.supportFragmentManager)
|
clearBackStack(it.supportFragmentManager)
|
||||||
it.replaceFragment(roomDetailFragment, R.id.homeDetailFragmentContainer)
|
it.replaceFragment(roomDetailFragment, R.id.homeDetailFragmentContainer)
|
||||||
}
|
}
|
||||||
@ -59,7 +61,9 @@ class HomeNavigator {
|
|||||||
Timber.v("Open user detail $userId")
|
Timber.v("Open user detail $userId")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private Methods *****************************************************************************
|
fun isRoomOpened(roomId: String): Boolean {
|
||||||
|
return currentRoomId == roomId
|
||||||
|
}
|
||||||
|
|
||||||
private fun clearBackStack(fragmentManager: FragmentManager) {
|
private fun clearBackStack(fragmentManager: FragmentManager) {
|
||||||
if (fragmentManager.backStackEntryCount > 0) {
|
if (fragmentManager.backStackEntryCount > 0) {
|
||||||
@ -68,8 +72,4 @@ class HomeNavigator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isRoot(roomId: String): Boolean {
|
|
||||||
return rootRoomId == roomId
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotredesign.core.epoxy
|
package im.vector.riotredesign.features.home
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
@ -17,20 +17,19 @@
|
|||||||
package im.vector.riotredesign.features.home.group
|
package im.vector.riotredesign.features.home.group
|
||||||
|
|
||||||
import android.os.Bundle
|
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.Incomplete
|
||||||
import com.airbnb.mvrx.Success
|
import com.airbnb.mvrx.Success
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
|
import im.vector.riotredesign.core.platform.RiotFragment
|
||||||
import im.vector.riotredesign.core.platform.StateView
|
import im.vector.riotredesign.core.platform.StateView
|
||||||
import im.vector.riotredesign.core.platform.VectorBaseFragment
|
|
||||||
import im.vector.riotredesign.features.home.HomeModule
|
|
||||||
import kotlinx.android.synthetic.main.fragment_group_list.*
|
import kotlinx.android.synthetic.main.fragment_group_list.*
|
||||||
import org.koin.android.ext.android.inject
|
|
||||||
import org.koin.android.scope.ext.android.bindScope
|
|
||||||
import org.koin.android.scope.ext.android.getOrCreateScope
|
|
||||||
|
|
||||||
class GroupListFragment : VectorBaseFragment(), GroupSummaryController.Callback {
|
class GroupListFragment : RiotFragment(), GroupSummaryController.Callback {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun newInstance(): GroupListFragment {
|
fun newInstance(): GroupListFragment {
|
||||||
@ -39,14 +38,16 @@ class GroupListFragment : VectorBaseFragment(), GroupSummaryController.Callback
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val viewModel: GroupListViewModel by fragmentViewModel()
|
private val viewModel: GroupListViewModel by fragmentViewModel()
|
||||||
private val groupController by inject<GroupSummaryController>()
|
|
||||||
|
|
||||||
override fun getLayoutResId() = R.layout.fragment_group_list
|
private lateinit var groupController: GroupSummaryController
|
||||||
|
|
||||||
|
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?) {
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
bindScope(getOrCreateScope(HomeModule.GROUP_LIST_SCOPE))
|
groupController = GroupSummaryController(this)
|
||||||
groupController.callback = this
|
|
||||||
stateView.contentView = epoxyRecyclerView
|
stateView.contentView = epoxyRecyclerView
|
||||||
epoxyRecyclerView.setController(groupController)
|
epoxyRecyclerView.setController(groupController)
|
||||||
viewModel.subscribe { renderState(it) }
|
viewModel.subscribe { renderState(it) }
|
||||||
@ -55,7 +56,7 @@ class GroupListFragment : VectorBaseFragment(), GroupSummaryController.Callback
|
|||||||
private fun renderState(state: GroupListViewState) {
|
private fun renderState(state: GroupListViewState) {
|
||||||
when (state.asyncGroups) {
|
when (state.asyncGroups) {
|
||||||
is Incomplete -> renderLoading()
|
is Incomplete -> renderLoading()
|
||||||
is Success -> renderSuccess(state)
|
is Success -> renderSuccess(state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -16,25 +16,25 @@
|
|||||||
|
|
||||||
package im.vector.riotredesign.features.home.group
|
package im.vector.riotredesign.features.home.group
|
||||||
|
|
||||||
import arrow.core.Option
|
|
||||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
|
import im.vector.matrix.android.api.Matrix
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.rx.rx
|
import im.vector.matrix.rx.rx
|
||||||
import im.vector.riotredesign.core.platform.VectorViewModel
|
import im.vector.riotredesign.core.platform.RiotViewModel
|
||||||
import org.koin.android.ext.android.get
|
import org.koin.android.ext.android.get
|
||||||
|
|
||||||
class GroupListViewModel(initialState: GroupListViewState,
|
class GroupListViewModel(initialState: GroupListViewState,
|
||||||
private val selectedGroupHolder: SelectedGroupStore,
|
private val selectedGroupHolder: SelectedGroupHolder,
|
||||||
private val session: Session
|
private val session: Session
|
||||||
) : VectorViewModel<GroupListViewState>(initialState) {
|
) : RiotViewModel<GroupListViewState>(initialState) {
|
||||||
|
|
||||||
companion object : MvRxViewModelFactory<GroupListViewModel, GroupListViewState> {
|
companion object : MvRxViewModelFactory<GroupListViewModel, GroupListViewState> {
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
override fun create(viewModelContext: ViewModelContext, state: GroupListViewState): GroupListViewModel? {
|
override fun create(viewModelContext: ViewModelContext, state: GroupListViewState): GroupListViewModel? {
|
||||||
val currentSession = viewModelContext.activity.get<Session>()
|
val currentSession = Matrix.getInstance().currentSession
|
||||||
val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupStore>()
|
val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupHolder>()
|
||||||
return GroupListViewModel(state, selectedGroupHolder, currentSession)
|
return GroupListViewModel(state, selectedGroupHolder, currentSession)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,8 +46,7 @@ class GroupListViewModel(initialState: GroupListViewState,
|
|||||||
|
|
||||||
private fun observeState() {
|
private fun observeState() {
|
||||||
subscribe {
|
subscribe {
|
||||||
val selectedGroup = Option.fromNullable(it.selectedGroup)
|
selectedGroupHolder.setSelectedGroup(it.selectedGroup)
|
||||||
selectedGroupHolder.post(selectedGroup)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -19,9 +19,8 @@ package im.vector.riotredesign.features.home.group
|
|||||||
import com.airbnb.epoxy.TypedEpoxyController
|
import com.airbnb.epoxy.TypedEpoxyController
|
||||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||||
|
|
||||||
class GroupSummaryController : TypedEpoxyController<GroupListViewState>() {
|
class GroupSummaryController(private val callback: Callback? = null
|
||||||
|
) : TypedEpoxyController<GroupListViewState>() {
|
||||||
var callback: Callback? = null
|
|
||||||
|
|
||||||
override fun buildModels(viewState: GroupListViewState) {
|
override fun buildModels(viewState: GroupListViewState) {
|
||||||
buildGroupModels(viewState.asyncGroups(), viewState.selectedGroup)
|
buildGroupModels(viewState.asyncGroups(), viewState.selectedGroup)
|
||||||
@ -33,14 +32,14 @@ class GroupSummaryController : TypedEpoxyController<GroupListViewState>() {
|
|||||||
}
|
}
|
||||||
summaries.forEach { groupSummary ->
|
summaries.forEach { groupSummary ->
|
||||||
val isSelected = groupSummary.groupId == selected?.groupId
|
val isSelected = groupSummary.groupId == selected?.groupId
|
||||||
groupSummaryItem {
|
GroupSummaryItem(
|
||||||
id(groupSummary.groupId)
|
groupName = groupSummary.displayName,
|
||||||
groupId(groupSummary.groupId)
|
avatarUrl = groupSummary.avatarUrl,
|
||||||
groupName(groupSummary.displayName)
|
isSelected = isSelected,
|
||||||
selected(isSelected)
|
listener = { callback?.onGroupSelected(groupSummary) }
|
||||||
avatarUrl(groupSummary.avatarUrl)
|
)
|
||||||
listener { callback?.onGroupSelected(groupSummary) }
|
.id(groupSummary.groupId)
|
||||||
}
|
.addTo(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -17,7 +17,22 @@
|
|||||||
package im.vector.riotredesign.features.home.group
|
package im.vector.riotredesign.features.home.group
|
||||||
|
|
||||||
import arrow.core.Option
|
import arrow.core.Option
|
||||||
|
import com.jakewharton.rxrelay2.BehaviorRelay
|
||||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||||
import im.vector.riotredesign.core.utils.RxStore
|
import io.reactivex.Observable
|
||||||
|
|
||||||
class SelectedGroupStore : 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -14,24 +14,23 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotredesign.core.utils
|
package im.vector.riotredesign.features.home.room
|
||||||
|
|
||||||
import com.jakewharton.rxrelay2.BehaviorRelay
|
import com.jakewharton.rxrelay2.BehaviorRelay
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
|
import io.reactivex.subjects.BehaviorSubject
|
||||||
|
|
||||||
open class RxStore<T>(defaultValue: T? = null) {
|
class VisibleRoomHolder {
|
||||||
|
|
||||||
private val storeSubject: BehaviorRelay<T> = if (defaultValue == null) {
|
private val visibleRoomStream = BehaviorRelay.create<String>()
|
||||||
BehaviorRelay.create<T>()
|
|
||||||
} else {
|
fun setVisibleRoom(roomId: String) {
|
||||||
BehaviorRelay.createDefault(defaultValue)
|
visibleRoomStream.accept(roomId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun observe(): Observable<T> {
|
fun visibleRoom(): Observable<String> {
|
||||||
return storeSubject.hide().distinctUntilChanged()
|
return visibleRoomStream.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun post(value: T) {
|
|
||||||
storeSubject.accept(value)
|
}
|
||||||
}
|
|
||||||
}
|
|
@ -16,14 +16,16 @@
|
|||||||
|
|
||||||
package im.vector.riotredesign.features.home.room.detail
|
package im.vector.riotredesign.features.home.room.detail
|
||||||
|
|
||||||
import android.graphics.drawable.AnimationDrawable
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotredesign.core.platform.VectorBaseFragment
|
import im.vector.riotredesign.core.platform.RiotFragment
|
||||||
import kotlinx.android.synthetic.main.fragment_loading_room_detail.*
|
import kotlinx.android.synthetic.main.fragment_loading_room_detail.*
|
||||||
|
|
||||||
class LoadingRoomDetailFragment : VectorBaseFragment() {
|
class LoadingRoomDetailFragment : RiotFragment() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@ -32,15 +34,15 @@ class LoadingRoomDetailFragment : VectorBaseFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLayoutResId() = R.layout.fragment_loading_room_detail
|
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?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
Glide.with(this)
|
||||||
val background = animatedLogoImageView.background
|
.load(R.drawable.riot_splash)
|
||||||
if (background is AnimationDrawable) {
|
.into(animatedLogoImageView)
|
||||||
background.start()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -14,17 +14,14 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.matrix.android.api.session.room.model.create
|
package im.vector.riotredesign.features.home.room.detail
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
|
|
||||||
enum class CreateRoomPreset {
|
sealed class RoomDetailActions {
|
||||||
@Json(name = "private_chat")
|
|
||||||
PRESET_PRIVATE_CHAT,
|
|
||||||
|
|
||||||
@Json(name = "public_chat")
|
data class SendMessage(val text: String) : RoomDetailActions()
|
||||||
PRESET_PUBLIC_CHAT,
|
object IsDisplayed : RoomDetailActions()
|
||||||
|
data class EventDisplayed(val event: TimelineEvent, val index: Int) : RoomDetailActions()
|
||||||
|
|
||||||
@Json(name = "trusted_private_chat")
|
|
||||||
PRESET_TRUSTED_PRIVATE_CHAT
|
|
||||||
}
|
}
|
@ -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))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -19,16 +19,14 @@ package im.vector.riotredesign.features.home.room.detail
|
|||||||
import com.airbnb.mvrx.Async
|
import com.airbnb.mvrx.Async
|
||||||
import com.airbnb.mvrx.MvRxState
|
import com.airbnb.mvrx.MvRxState
|
||||||
import com.airbnb.mvrx.Uninitialized
|
import com.airbnb.mvrx.Uninitialized
|
||||||
|
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.model.RoomSummary
|
||||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineData
|
import im.vector.matrix.android.api.session.room.timeline.TimelineData
|
||||||
import im.vector.matrix.android.api.session.user.model.User
|
|
||||||
|
|
||||||
data class RoomDetailViewState(
|
data class RoomDetailViewState(
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val eventId: String?,
|
val eventId: String?,
|
||||||
val timeline: Timeline? = null,
|
val readReceiptsForEventId: Map<String, List<ReadReceipt>> = emptyMap(),
|
||||||
val inviter: Async<User> = Uninitialized,
|
|
||||||
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
||||||
val asyncTimelineData: Async<TimelineData> = Uninitialized
|
val asyncTimelineData: Async<TimelineData> = Uninitialized
|
||||||
) : MvRxState {
|
) : MvRxState {
|
@ -18,11 +18,14 @@ package im.vector.riotredesign.features.home.room.detail
|
|||||||
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import im.vector.riotredesign.core.platform.DefaultListUpdateCallback
|
import im.vector.riotredesign.core.platform.DefaultListUpdateCallback
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager) : DefaultListUpdateCallback {
|
class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager) : DefaultListUpdateCallback {
|
||||||
|
|
||||||
|
var isLocked = AtomicBoolean(true)
|
||||||
|
|
||||||
override fun onInserted(position: Int, count: Int) {
|
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)
|
layoutManager.scrollToPosition(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -14,14 +14,16 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotredesign.core.platform
|
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||||
|
|
||||||
import android.os.Bundle
|
import im.vector.riotredesign.R
|
||||||
|
import im.vector.riotredesign.core.epoxy.KotlinModel
|
||||||
|
|
||||||
interface Restorable {
|
class BlankItem
|
||||||
|
: KotlinModel(R.layout.item_timeline_event_blank) {
|
||||||
|
|
||||||
fun onSaveInstanceState(outState: Bundle)
|
override fun bind() {
|
||||||
|
//no-op
|
||||||
fun onRestoreInstanceState(savedInstanceState: Bundle?)
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -14,24 +14,19 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotredesign.core.dialogs
|
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import butterknife.BindView
|
|
||||||
import butterknife.ButterKnife
|
|
||||||
import im.vector.riotredesign.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)
|
private val dayTextView by bind<TextView>(R.id.itemDayTextView)
|
||||||
lateinit var icon: ImageView
|
|
||||||
|
|
||||||
@BindView(R.id.adapter_item_dialog_text)
|
override fun bind() {
|
||||||
lateinit var text: TextView
|
dayTextView.text = formattedDay
|
||||||
|
|
||||||
init {
|
|
||||||
ButterKnife.bind(this, view)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -14,19 +14,19 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotredesign.core.resources
|
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||||
|
|
||||||
import org.threeten.bp.Instant
|
import android.widget.TextView
|
||||||
import org.threeten.bp.LocalDateTime
|
import im.vector.riotredesign.R
|
||||||
import org.threeten.bp.ZoneId
|
import im.vector.riotredesign.core.epoxy.KotlinModel
|
||||||
|
|
||||||
object DateProvider {
|
class DefaultItem(
|
||||||
|
val text: CharSequence? = null
|
||||||
|
) : KotlinModel(R.layout.item_timeline_event_default) {
|
||||||
|
|
||||||
private val zoneId = ZoneId.systemDefault()
|
private val messageView by bind<TextView>(R.id.stateMessageView)
|
||||||
|
|
||||||
fun toLocalDateTime(timestamp: Long?): LocalDateTime {
|
override fun bind() {
|
||||||
val instant = Instant.ofEpochMilli(timestamp ?: 0)
|
messageView.text = text
|
||||||
return LocalDateTime.ofInstant(instant, zoneId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -14,19 +14,15 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotredesign.features.home.room.detail.timeline.animation
|
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||||
|
|
||||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
|
|
||||||
private const val ANIM_DURATION_IN_MILLIS = 100L
|
class DefaultItemFactory {
|
||||||
|
|
||||||
class TimelineItemAnimator : DefaultItemAnimator() {
|
fun create(event: TimelineEvent): DefaultItem? {
|
||||||
|
val text = "${event.root.type} events are not yet handled"
|
||||||
init {
|
return DefaultItem(text = text)
|
||||||
addDuration = ANIM_DURATION_IN_MILLIS
|
|
||||||
removeDuration = 0
|
|
||||||
moveDuration = 0
|
|
||||||
changeDuration = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -14,12 +14,11 @@
|
|||||||
* limitations under the License.
|
* 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
|
data class MessageInformationData(
|
||||||
import com.squareup.moshi.JsonClass
|
val time: CharSequence? = null,
|
||||||
|
val avatarUrl: String?,
|
||||||
@JsonClass(generateAdapter = true)
|
val memberName: CharSequence? = null,
|
||||||
data class ContentUploadResponse(
|
val showInformation: Boolean = true
|
||||||
@Json(name = "content_uri") val contentUri: String
|
|
||||||
)
|
)
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -14,32 +14,26 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotredesign.features.home.room.detail.timeline.item
|
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||||
|
|
||||||
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import im.vector.matrix.android.api.permalinks.MatrixLinkify
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
|
|
||||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
|
class MessageTextItem(
|
||||||
abstract class DefaultItem : BaseEventItem<DefaultItem.Holder>() {
|
val message: CharSequence? = null,
|
||||||
|
informationData: MessageInformationData
|
||||||
|
) : AbsMessageItem(informationData, R.layout.item_timeline_event_text_message) {
|
||||||
|
|
||||||
@EpoxyAttribute
|
override val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
|
||||||
var text: CharSequence? = null
|
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(holder: Holder) {
|
override fun bind() {
|
||||||
holder.messageView.text = text
|
super.bind()
|
||||||
}
|
messageView.text = message
|
||||||
|
MatrixLinkify.addLinkMovementMethod(messageView)
|
||||||
override fun getStubType(): Int = STUB_ID
|
|
||||||
|
|
||||||
class Holder : BaseHolder() {
|
|
||||||
override fun getStubId(): Int = STUB_ID
|
|
||||||
|
|
||||||
val messageView by bind<TextView>(R.id.stateMessageView)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val STUB_ID = R.id.messageContentDefaultStub
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -14,25 +14,24 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotredesign.features.home.room.detail.timeline.item
|
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||||
|
|
||||||
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
|
||||||
import com.airbnb.epoxy.EpoxyModelWithHolder
|
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
|
import im.vector.riotredesign.core.epoxy.KotlinModel
|
||||||
|
import im.vector.riotredesign.features.home.AvatarRenderer
|
||||||
|
|
||||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_day_separator)
|
class NoticeItem(private val noticeText: CharSequence? = null,
|
||||||
abstract class DaySeparatorItem : EpoxyModelWithHolder<DaySeparatorItem.Holder>() {
|
private val avatarUrl: String?,
|
||||||
|
private val memberName: CharSequence? = null)
|
||||||
|
: KotlinModel(R.layout.item_timeline_event_notice) {
|
||||||
|
|
||||||
@EpoxyAttribute lateinit var formattedDay: CharSequence
|
private val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView)
|
||||||
|
private val noticeTextView by bind<TextView>(R.id.itemNoticeTextView)
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind() {
|
||||||
holder.dayTextView.text = formattedDay
|
noticeTextView.text = noticeText
|
||||||
}
|
AvatarRenderer.render(avatarUrl, memberName?.toString(), avatarImageView)
|
||||||
|
|
||||||
class Holder : VectorEpoxyHolder() {
|
|
||||||
val dayTextView by bind<TextView>(R.id.itemDayTextView)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -14,38 +14,29 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotredesign.features.home.room.detail.timeline.factory
|
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||||
|
|
||||||
import android.text.TextUtils
|
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.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.Membership
|
import im.vector.matrix.android.api.session.room.model.Membership
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomMember
|
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotredesign.core.resources.StringProvider
|
import im.vector.riotredesign.core.resources.StringProvider
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.RoomMemberEventHelper
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_
|
|
||||||
|
|
||||||
|
|
||||||
//TODO : complete with call membership events¬
|
//TODO : complete with call membership events
|
||||||
class RoomMemberItemFactory(private val stringProvider: StringProvider) {
|
class RoomMemberItemFactory(private val stringProvider: StringProvider) {
|
||||||
|
|
||||||
fun create(event: TimelineEvent): NoticeItem? {
|
fun create(event: TimelineEvent): NoticeItem? {
|
||||||
val eventContent: RoomMember? = event.root.content.toModel()
|
val roomMember = event.roomMember ?: return null
|
||||||
val prevEventContent: RoomMember? = event.root.prevContent.toModel()
|
val noticeText = buildRoomMemberNotice(event) ?: return null
|
||||||
val noticeText = buildRoomMemberNotice(event, eventContent, prevEventContent) ?: return null
|
return NoticeItem(noticeText, roomMember.avatarUrl, roomMember.displayName)
|
||||||
val senderAvatar = RoomMemberEventHelper.senderAvatar(eventContent, prevEventContent, event)
|
|
||||||
val senderName = RoomMemberEventHelper.senderName(eventContent, prevEventContent, event)
|
|
||||||
|
|
||||||
return NoticeItem_()
|
|
||||||
.userId(event.root.sender ?: "")
|
|
||||||
.noticeText(noticeText)
|
|
||||||
.avatarUrl(senderAvatar)
|
|
||||||
.memberName(senderName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildRoomMemberNotice(event: TimelineEvent, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
|
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
|
val isMembershipEvent = prevEventContent?.membership != eventContent?.membership
|
||||||
return if (isMembershipEvent) {
|
return if (isMembershipEvent) {
|
||||||
buildMembershipNotice(event, eventContent, prevEventContent)
|
buildMembershipNotice(event, eventContent, prevEventContent)
|
||||||
@ -59,13 +50,9 @@ class RoomMemberItemFactory(private val stringProvider: StringProvider) {
|
|||||||
// Check display name has been changed
|
// Check display name has been changed
|
||||||
if (!TextUtils.equals(eventContent?.displayName, prevEventContent?.displayName)) {
|
if (!TextUtils.equals(eventContent?.displayName, prevEventContent?.displayName)) {
|
||||||
val displayNameText = when {
|
val displayNameText = when {
|
||||||
prevEventContent?.displayName.isNullOrEmpty() ->
|
prevEventContent?.displayName.isNullOrEmpty() -> stringProvider.getString(R.string.notice_display_name_set, event.root.sender, eventContent?.displayName)
|
||||||
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)
|
||||||
eventContent?.displayName.isNullOrEmpty() ->
|
else -> stringProvider.getString(R.string.notice_display_name_changed_from, event.root.sender, prevEventContent?.displayName, eventContent?.displayName)
|
||||||
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)
|
displayText.append(displayNameText)
|
||||||
}
|
}
|
||||||
@ -75,7 +62,7 @@ class RoomMemberItemFactory(private val stringProvider: StringProvider) {
|
|||||||
displayText.append(" ")
|
displayText.append(" ")
|
||||||
stringProvider.getString(R.string.notice_avatar_changed_too)
|
stringProvider.getString(R.string.notice_avatar_changed_too)
|
||||||
} else {
|
} else {
|
||||||
stringProvider.getString(R.string.notice_avatar_url_changed, event.senderName)
|
stringProvider.getString(R.string.notice_avatar_url_changed, event.roomMember?.displayName)
|
||||||
}
|
}
|
||||||
displayText.append(displayAvatarText)
|
displayText.append(displayAvatarText)
|
||||||
}
|
}
|
||||||
@ -83,34 +70,27 @@ class RoomMemberItemFactory(private val stringProvider: StringProvider) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun buildMembershipNotice(event: TimelineEvent, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
|
private fun buildMembershipNotice(event: TimelineEvent, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
|
||||||
val senderDisplayName = event.senderName ?: event.root.sender
|
val senderDisplayName = event.roomMember?.displayName ?: return null
|
||||||
val targetDisplayName = eventContent?.displayName ?: event.root.sender
|
val targetDisplayName = eventContent?.displayName ?: event.root.sender
|
||||||
return when {
|
return when {
|
||||||
Membership.INVITE == eventContent?.membership -> {
|
Membership.INVITE == eventContent?.membership -> {
|
||||||
// TODO get userId
|
// TODO get userId
|
||||||
val selfUserId = ""
|
val selfUserId: String = ""
|
||||||
when {
|
when {
|
||||||
eventContent.thirdPartyInvite != null ->
|
eventContent.thirdPartyInvite != null -> stringProvider.getString(R.string.notice_room_third_party_registered_invite, targetDisplayName, eventContent.thirdPartyInvite?.displayName)
|
||||||
stringProvider.getString(R.string.notice_room_third_party_registered_invite,
|
TextUtils.equals(event.root.stateKey, selfUserId)
|
||||||
targetDisplayName, eventContent.thirdPartyInvite?.displayName)
|
-> stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName)
|
||||||
TextUtils.equals(event.root.stateKey, selfUserId) ->
|
event.root.stateKey.isNullOrEmpty() -> stringProvider.getString(R.string.notice_room_invite_no_invitee, senderDisplayName)
|
||||||
stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName)
|
else -> stringProvider.getString(R.string.notice_room_invite, senderDisplayName, targetDisplayName)
|
||||||
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 ->
|
Membership.JOIN == eventContent?.membership -> stringProvider.getString(R.string.notice_room_join, senderDisplayName)
|
||||||
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
|
||||||
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)) {
|
return if (TextUtils.equals(event.root.sender, event.root.stateKey)) {
|
||||||
if (prevEventContent?.membership == Membership.INVITE) {
|
if (prevEventContent?.membership == Membership.INVITE) {
|
||||||
stringProvider.getString(R.string.notice_room_reject, senderDisplayName)
|
stringProvider.getString(R.string.notice_room_reject, senderDisplayName)
|
||||||
} else {
|
} else {
|
||||||
val leftDisplayName = RoomMemberEventHelper.senderName(eventContent, prevEventContent, event)
|
stringProvider.getString(R.string.notice_room_leave, senderDisplayName)
|
||||||
stringProvider.getString(R.string.notice_room_leave, leftDisplayName)
|
|
||||||
}
|
}
|
||||||
} else if (prevEventContent?.membership == Membership.INVITE) {
|
} else if (prevEventContent?.membership == Membership.INVITE) {
|
||||||
stringProvider.getString(R.string.notice_room_withdraw, senderDisplayName, targetDisplayName)
|
stringProvider.getString(R.string.notice_room_withdraw, senderDisplayName, targetDisplayName)
|
||||||
@ -121,10 +101,8 @@ class RoomMemberItemFactory(private val stringProvider: StringProvider) {
|
|||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
Membership.BAN == eventContent?.membership ->
|
Membership.BAN == eventContent?.membership -> stringProvider.getString(R.string.notice_room_ban, senderDisplayName, targetDisplayName)
|
||||||
stringProvider.getString(R.string.notice_room_ban, senderDisplayName, targetDisplayName)
|
Membership.KNOCK == eventContent?.membership -> stringProvider.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName)
|
||||||
Membership.KNOCK == eventContent?.membership ->
|
|
||||||
stringProvider.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName)
|
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -14,31 +14,30 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotredesign.features.home.room.detail.timeline.factory
|
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||||
|
|
||||||
import android.text.TextUtils
|
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.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomNameContent
|
import im.vector.matrix.android.api.session.room.model.RoomNameContent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotredesign.core.resources.StringProvider
|
import im.vector.riotredesign.core.resources.StringProvider
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_
|
|
||||||
|
|
||||||
class RoomNameItemFactory(private val stringProvider: StringProvider) {
|
class RoomNameItemFactory(private val stringProvider: StringProvider) {
|
||||||
|
|
||||||
fun create(event: TimelineEvent): NoticeItem? {
|
fun create(event: TimelineEvent): NoticeItem? {
|
||||||
|
|
||||||
val content: RoomNameContent = event.root.content.toModel() ?: return null
|
val content: RoomNameContent? = event.root.content.toModel()
|
||||||
val text = if (!TextUtils.isEmpty(content.name)) {
|
val roomMember = event.roomMember
|
||||||
stringProvider.getString(R.string.notice_room_name_changed, event.senderName, content.name)
|
if (content == null || roomMember == null) {
|
||||||
} else {
|
return null
|
||||||
stringProvider.getString(R.string.notice_room_name_removed, event.senderName)
|
|
||||||
}
|
}
|
||||||
return NoticeItem_()
|
val text = if (!TextUtils.isEmpty(content.name)) {
|
||||||
.noticeText(text)
|
stringProvider.getString(R.string.notice_room_name_changed, roomMember.displayName, content.name)
|
||||||
.avatarUrl(event.senderAvatar)
|
} else {
|
||||||
.memberName(event.senderName)
|
stringProvider.getString(R.string.notice_room_name_removed, roomMember.displayName)
|
||||||
|
}
|
||||||
|
return NoticeItem(text, roomMember.avatarUrl, roomMember.displayName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -14,30 +14,29 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotredesign.features.home.room.detail.timeline.factory
|
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.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
|
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotredesign.core.resources.StringProvider
|
import im.vector.riotredesign.core.resources.StringProvider
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_
|
|
||||||
|
|
||||||
class RoomTopicItemFactory(private val stringProvider: StringProvider) {
|
class RoomTopicItemFactory(private val stringProvider: StringProvider) {
|
||||||
|
|
||||||
fun create(event: TimelineEvent): NoticeItem? {
|
fun create(event: TimelineEvent): NoticeItem? {
|
||||||
|
|
||||||
val content: RoomTopicContent = event.root.content.toModel() ?: return null
|
val content: RoomTopicContent? = event.root.content.toModel()
|
||||||
val text = if (content.topic.isNullOrEmpty()) {
|
val roomMember = event.roomMember
|
||||||
stringProvider.getString(R.string.notice_room_topic_removed, event.senderName)
|
if (content == null || roomMember == null) {
|
||||||
} else {
|
return null
|
||||||
stringProvider.getString(R.string.notice_room_topic_changed, event.senderName, content.topic)
|
|
||||||
}
|
}
|
||||||
return NoticeItem_()
|
val text = if (content.topic.isNullOrEmpty()) {
|
||||||
.noticeText(text)
|
stringProvider.getString(R.string.notice_room_topic_removed, roomMember.displayName)
|
||||||
.avatarUrl(event.senderAvatar)
|
} else {
|
||||||
.memberName(event.senderName)
|
stringProvider.getString(R.string.notice_room_topic_changed, roomMember.displayName, content.topic)
|
||||||
|
}
|
||||||
|
return NoticeItem(text, roomMember.avatarUrl, roomMember.displayName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotredesign.features.home.room.detail.timeline.helper
|
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||||
|
|
||||||
import im.vector.riotredesign.core.resources.LocaleProvider
|
import im.vector.riotredesign.core.resources.LocaleProvider
|
||||||
import org.threeten.bp.LocalDateTime
|
import org.threeten.bp.LocalDateTime
|
||||||
@ -22,19 +22,12 @@ import org.threeten.bp.format.DateTimeFormatter
|
|||||||
|
|
||||||
class TimelineDateFormatter(private val localeProvider: LocaleProvider) {
|
class TimelineDateFormatter(private val localeProvider: LocaleProvider) {
|
||||||
|
|
||||||
private val messageHourFormatter by lazy {
|
|
||||||
DateTimeFormatter.ofPattern("H:mm", localeProvider.current())
|
|
||||||
}
|
|
||||||
private val messageDayFormatter by lazy {
|
|
||||||
DateTimeFormatter.ofPattern("EEE d MMM", localeProvider.current())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun formatMessageHour(localDateTime: LocalDateTime): String {
|
fun formatMessageHour(localDateTime: LocalDateTime): String {
|
||||||
return messageHourFormatter.format(localDateTime)
|
return DateTimeFormatter.ofPattern("H:mm", localeProvider.current()).format(localDateTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun formatMessageDay(localDateTime: LocalDateTime): String {
|
fun formatMessageDay(localDateTime: LocalDateTime): String {
|
||||||
return messageDayFormatter.format(localDateTime)
|
return DateTimeFormatter.ofPattern("EEE d MMM", localeProvider.current()).format(localDateTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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() }
|
||||||
|
}
|
||||||
|
}
|
@ -22,8 +22,8 @@ sealed class RoomListActions {
|
|||||||
|
|
||||||
data class SelectRoom(val roomSummary: RoomSummary) : RoomListActions()
|
data class SelectRoom(val roomSummary: RoomSummary) : RoomListActions()
|
||||||
|
|
||||||
|
object RoomDisplayed : RoomListActions()
|
||||||
|
|
||||||
data class FilterRooms(val roomName: CharSequence? = null) : RoomListActions()
|
data class FilterRooms(val roomName: CharSequence? = null) : RoomListActions()
|
||||||
|
|
||||||
data class ToggleCategory(val category: RoomCategory) : RoomListActions()
|
|
||||||
|
|
||||||
}
|
}
|
@ -19,27 +19,24 @@ package im.vector.riotredesign.features.home.room.list
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import com.airbnb.mvrx.Fail
|
import com.airbnb.mvrx.Fail
|
||||||
import com.airbnb.mvrx.Incomplete
|
import com.airbnb.mvrx.Incomplete
|
||||||
import com.airbnb.mvrx.Success
|
import com.airbnb.mvrx.Success
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.activityViewModel
|
||||||
import im.vector.matrix.android.api.failure.Failure
|
import im.vector.matrix.android.api.failure.Failure
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
|
|
||||||
import im.vector.riotredesign.core.extensions.observeEvent
|
|
||||||
import im.vector.riotredesign.core.extensions.setupAsSearch
|
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.core.platform.StateView
|
||||||
import im.vector.riotredesign.core.platform.VectorBaseFragment
|
|
||||||
import im.vector.riotredesign.features.home.HomeModule
|
|
||||||
import im.vector.riotredesign.features.home.HomeNavigator
|
import im.vector.riotredesign.features.home.HomeNavigator
|
||||||
import kotlinx.android.synthetic.main.fragment_room_list.*
|
import kotlinx.android.synthetic.main.fragment_room_list.*
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.android.scope.ext.android.bindScope
|
|
||||||
import org.koin.android.scope.ext.android.getOrCreateScope
|
|
||||||
|
|
||||||
class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback {
|
class RoomListFragment : RiotFragment(), RoomSummaryController.Callback {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun newInstance(): RoomListFragment {
|
fun newInstance(): RoomListFragment {
|
||||||
@ -47,44 +44,21 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val roomController by inject<RoomSummaryController>()
|
|
||||||
private val homeNavigator by inject<HomeNavigator>()
|
private val homeNavigator by inject<HomeNavigator>()
|
||||||
private val roomListViewModel: RoomListViewModel by fragmentViewModel()
|
private val roomController by inject<RoomSummaryController>()
|
||||||
|
private val homeViewModel: RoomListViewModel by activityViewModel()
|
||||||
|
|
||||||
override fun getLayoutResId() = R.layout.fragment_room_list
|
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?) {
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
bindScope(getOrCreateScope(HomeModule.ROOM_LIST_SCOPE))
|
|
||||||
setupRecyclerView()
|
|
||||||
setupFilterView()
|
|
||||||
roomListViewModel.subscribe { renderState(it) }
|
|
||||||
roomListViewModel.openRoomLiveData.observeEvent(this) {
|
|
||||||
homeNavigator.openRoomDetail(it, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupRecyclerView() {
|
|
||||||
val layoutManager = LinearLayoutManager(context)
|
|
||||||
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
|
|
||||||
epoxyRecyclerView.layoutManager = layoutManager
|
|
||||||
roomController.callback = this
|
roomController.callback = this
|
||||||
roomController.addModelBuildListener { it.dispatchTo(stateRestorer) }
|
|
||||||
stateView.contentView = epoxyRecyclerView
|
stateView.contentView = epoxyRecyclerView
|
||||||
epoxyRecyclerView.setController(roomController)
|
epoxyRecyclerView.setController(roomController)
|
||||||
}
|
setupFilterView()
|
||||||
|
homeViewModel.subscribe { renderState(it) }
|
||||||
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) {
|
|
||||||
roomListViewModel.accept(RoomListActions.FilterRooms(s))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderState(state: RoomListViewState) {
|
private fun renderState(state: RoomListViewState) {
|
||||||
@ -110,19 +84,30 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback {
|
|||||||
|
|
||||||
private fun renderFailure(error: Throwable) {
|
private fun renderFailure(error: Throwable) {
|
||||||
val message = when (error) {
|
val message = when (error) {
|
||||||
is Failure.NetworkConnection -> getString(R.string.network_error_please_check_and_retry)
|
is Failure.NetworkConnection -> getString(R.string.error_no_network)
|
||||||
else -> getString(R.string.unknown_error)
|
else -> getString(R.string.error_common)
|
||||||
}
|
}
|
||||||
stateView.state = StateView.State.Error(message)
|
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 **************************************************************
|
// RoomSummaryController.Callback **************************************************************
|
||||||
|
|
||||||
override fun onRoomSelected(room: RoomSummary) {
|
override fun onRoomSelected(room: RoomSummary) {
|
||||||
roomListViewModel.accept(RoomListActions.SelectRoom(room))
|
homeViewModel.accept(RoomListActions.SelectRoom(room))
|
||||||
|
homeNavigator.openRoomDetail(room.roomId, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onToggleRoomCategory(roomCategory: RoomCategory) {
|
|
||||||
roomListViewModel.accept(RoomListActions.ToggleCategory(roomCategory))
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -16,24 +16,22 @@
|
|||||||
|
|
||||||
package im.vector.riotredesign.features.home.room.list
|
package im.vector.riotredesign.features.home.room.list
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import arrow.core.Option
|
import arrow.core.Option
|
||||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
import com.jakewharton.rxrelay2.BehaviorRelay
|
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.Session
|
||||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||||
import im.vector.matrix.android.api.session.room.model.Membership
|
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
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.android.api.session.room.model.tag.RoomTag
|
||||||
import im.vector.matrix.rx.rx
|
import im.vector.matrix.rx.rx
|
||||||
import im.vector.riotredesign.core.platform.VectorViewModel
|
import im.vector.riotredesign.core.platform.RiotViewModel
|
||||||
import im.vector.riotredesign.core.utils.LiveEvent
|
import im.vector.riotredesign.features.home.group.SelectedGroupHolder
|
||||||
import im.vector.riotredesign.features.home.group.SelectedGroupStore
|
import im.vector.riotredesign.features.home.room.VisibleRoomHolder
|
||||||
import im.vector.riotredesign.features.home.room.VisibleRoomStore
|
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.functions.Function3
|
import io.reactivex.functions.Function3
|
||||||
|
import io.reactivex.rxkotlin.subscribeBy
|
||||||
import org.koin.android.ext.android.get
|
import org.koin.android.ext.android.get
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@ -41,20 +39,20 @@ typealias RoomListFilterName = CharSequence
|
|||||||
|
|
||||||
class RoomListViewModel(initialState: RoomListViewState,
|
class RoomListViewModel(initialState: RoomListViewState,
|
||||||
private val session: Session,
|
private val session: Session,
|
||||||
private val selectedGroupHolder: SelectedGroupStore,
|
private val selectedGroupHolder: SelectedGroupHolder,
|
||||||
private val visibleRoomHolder: VisibleRoomStore,
|
private val visibleRoomHolder: VisibleRoomHolder,
|
||||||
private val roomSelectionRepository: RoomSelectionRepository,
|
private val roomSelectionRepository: RoomSelectionRepository,
|
||||||
private val roomSummaryComparator: RoomSummaryComparator)
|
private val roomSummaryComparator: RoomSummaryComparator)
|
||||||
: VectorViewModel<RoomListViewState>(initialState) {
|
: RiotViewModel<RoomListViewState>(initialState) {
|
||||||
|
|
||||||
companion object : MvRxViewModelFactory<RoomListViewModel, RoomListViewState> {
|
companion object : MvRxViewModelFactory<RoomListViewModel, RoomListViewState> {
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
override fun create(viewModelContext: ViewModelContext, state: RoomListViewState): RoomListViewModel? {
|
override fun create(viewModelContext: ViewModelContext, state: RoomListViewState): RoomListViewModel? {
|
||||||
val currentSession = viewModelContext.activity.get<Session>()
|
val currentSession = Matrix.getInstance().currentSession
|
||||||
val roomSelectionRepository = viewModelContext.activity.get<RoomSelectionRepository>()
|
val roomSelectionRepository = viewModelContext.activity.get<RoomSelectionRepository>()
|
||||||
val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupStore>()
|
val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupHolder>()
|
||||||
val visibleRoomHolder = viewModelContext.activity.get<VisibleRoomStore>()
|
val visibleRoomHolder = viewModelContext.activity.get<VisibleRoomHolder>()
|
||||||
val roomSummaryComparator = viewModelContext.activity.get<RoomSummaryComparator>()
|
val roomSummaryComparator = viewModelContext.activity.get<RoomSummaryComparator>()
|
||||||
return RoomListViewModel(state, currentSession, selectedGroupHolder, visibleRoomHolder, roomSelectionRepository, roomSummaryComparator)
|
return RoomListViewModel(state, currentSession, selectedGroupHolder, visibleRoomHolder, roomSelectionRepository, roomSummaryComparator)
|
||||||
}
|
}
|
||||||
@ -63,10 +61,6 @@ class RoomListViewModel(initialState: RoomListViewState,
|
|||||||
|
|
||||||
private val roomListFilter = BehaviorRelay.createDefault<Option<RoomListFilterName>>(Option.empty())
|
private val roomListFilter = BehaviorRelay.createDefault<Option<RoomListFilterName>>(Option.empty())
|
||||||
|
|
||||||
private val _openRoomLiveData = MutableLiveData<LiveEvent<String>>()
|
|
||||||
val openRoomLiveData: LiveData<LiveEvent<String>>
|
|
||||||
get() = _openRoomLiveData
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
observeRoomSummaries()
|
observeRoomSummaries()
|
||||||
observeVisibleRoom()
|
observeVisibleRoom()
|
||||||
@ -74,18 +68,16 @@ class RoomListViewModel(initialState: RoomListViewState,
|
|||||||
|
|
||||||
fun accept(action: RoomListActions) {
|
fun accept(action: RoomListActions) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is RoomListActions.SelectRoom -> handleSelectRoom(action)
|
is RoomListActions.SelectRoom -> handleSelectRoom(action)
|
||||||
is RoomListActions.FilterRooms -> handleFilterRooms(action)
|
is RoomListActions.FilterRooms -> handleFilterRooms(action)
|
||||||
is RoomListActions.ToggleCategory -> handleToggleCategory(action)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PRIVATE METHODS *****************************************************************************
|
// PRIVATE METHODS *****************************************************************************
|
||||||
|
|
||||||
private fun handleSelectRoom(action: RoomListActions.SelectRoom) = withState { state ->
|
private fun handleSelectRoom(action: RoomListActions.SelectRoom) = withState { state ->
|
||||||
if (state.visibleRoomId != action.roomSummary.roomId) {
|
if (state.selectedRoomId != action.roomSummary.roomId) {
|
||||||
roomSelectionRepository.saveLastSelectedRoom(action.roomSummary.roomId)
|
roomSelectionRepository.saveLastSelectedRoom(action.roomSummary.roomId)
|
||||||
_openRoomLiveData.postValue(LiveEvent(action.roomSummary.roomId))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,23 +86,18 @@ class RoomListViewModel(initialState: RoomListViewState,
|
|||||||
roomListFilter.accept(optionalFilter)
|
roomListFilter.accept(optionalFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleToggleCategory(action: RoomListActions.ToggleCategory) = setState {
|
|
||||||
this.toggle(action.category)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun observeVisibleRoom() {
|
private fun observeVisibleRoom() {
|
||||||
visibleRoomHolder.observe()
|
visibleRoomHolder.visibleRoom()
|
||||||
.doOnNext {
|
.subscribeBy {
|
||||||
setState { copy(visibleRoomId = it) }
|
setState { copy(selectedRoomId = it) }
|
||||||
}
|
}
|
||||||
.subscribe()
|
|
||||||
.disposeOnClear()
|
.disposeOnClear()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeRoomSummaries() {
|
private fun observeRoomSummaries() {
|
||||||
Observable.combineLatest<List<RoomSummary>, Option<GroupSummary>, Option<RoomListFilterName>, RoomSummaries>(
|
Observable.combineLatest<List<RoomSummary>, Option<GroupSummary>, Option<RoomListFilterName>, RoomSummaries>(
|
||||||
session.rx().liveRoomSummaries().throttleLast(300, TimeUnit.MILLISECONDS),
|
session.rx().liveRoomSummaries().throttleLast(300, TimeUnit.MILLISECONDS),
|
||||||
selectedGroupHolder.observe(),
|
selectedGroupHolder.selectedGroup(),
|
||||||
roomListFilter.throttleLast(300, TimeUnit.MILLISECONDS),
|
roomListFilter.throttleLast(300, TimeUnit.MILLISECONDS),
|
||||||
Function3 { rooms, selectedGroupOption, filterRoomOption ->
|
Function3 { rooms, selectedGroupOption, filterRoomOption ->
|
||||||
val filteredRooms = filterRooms(rooms, filterRoomOption)
|
val filteredRooms = filterRooms(rooms, filterRoomOption)
|
||||||
@ -154,7 +141,6 @@ class RoomListViewModel(initialState: RoomListViewState,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun buildRoomSummaries(rooms: List<RoomSummary>): RoomSummaries {
|
private fun buildRoomSummaries(rooms: List<RoomSummary>): RoomSummaries {
|
||||||
val invites = ArrayList<RoomSummary>()
|
|
||||||
val favourites = ArrayList<RoomSummary>()
|
val favourites = ArrayList<RoomSummary>()
|
||||||
val directChats = ArrayList<RoomSummary>()
|
val directChats = ArrayList<RoomSummary>()
|
||||||
val groupRooms = ArrayList<RoomSummary>()
|
val groupRooms = ArrayList<RoomSummary>()
|
||||||
@ -162,10 +148,8 @@ class RoomListViewModel(initialState: RoomListViewState,
|
|||||||
val serverNotices = ArrayList<RoomSummary>()
|
val serverNotices = ArrayList<RoomSummary>()
|
||||||
|
|
||||||
for (room in rooms) {
|
for (room in rooms) {
|
||||||
if (room.membership.isLeft()) continue
|
|
||||||
val tags = room.tags.map { it.name }
|
val tags = room.tags.map { it.name }
|
||||||
when {
|
when {
|
||||||
room.membership == Membership.INVITE -> invites.add(room)
|
|
||||||
tags.contains(RoomTag.ROOM_TAG_SERVER_NOTICE) -> serverNotices.add(room)
|
tags.contains(RoomTag.ROOM_TAG_SERVER_NOTICE) -> serverNotices.add(room)
|
||||||
tags.contains(RoomTag.ROOM_TAG_FAVOURITE) -> favourites.add(room)
|
tags.contains(RoomTag.ROOM_TAG_FAVOURITE) -> favourites.add(room)
|
||||||
tags.contains(RoomTag.ROOM_TAG_LOW_PRIORITY) -> lowPriorities.add(room)
|
tags.contains(RoomTag.ROOM_TAG_LOW_PRIORITY) -> lowPriorities.add(room)
|
||||||
@ -174,14 +158,13 @@ class RoomListViewModel(initialState: RoomListViewState,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return RoomSummaries().apply {
|
return RoomSummaries(
|
||||||
put(RoomCategory.INVITE, invites.sortedWith(roomSummaryComparator))
|
favourites = favourites.sortedWith(roomSummaryComparator),
|
||||||
put(RoomCategory.FAVOURITE, favourites.sortedWith(roomSummaryComparator))
|
directRooms = directChats.sortedWith(roomSummaryComparator),
|
||||||
put(RoomCategory.DIRECT, directChats.sortedWith(roomSummaryComparator))
|
groupRooms = groupRooms.sortedWith(roomSummaryComparator),
|
||||||
put(RoomCategory.GROUP, groupRooms.sortedWith(roomSummaryComparator))
|
lowPriorities = lowPriorities.sortedWith(roomSummaryComparator),
|
||||||
put(RoomCategory.LOW_PRIORITY, lowPriorities.sortedWith(roomSummaryComparator))
|
serverNotices = serverNotices.sortedWith(roomSummaryComparator)
|
||||||
put(RoomCategory.SERVER_NOTICE, serverNotices.sortedWith(roomSummaryComparator))
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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())
|
||||||
|
}
|
@ -41,10 +41,10 @@ class RoomSummaryComparator
|
|||||||
rightTimestamp = rightRoomSummary.lastMessage?.originServerTs ?: 0
|
rightTimestamp = rightRoomSummary.lastMessage?.originServerTs ?: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rightRoomSummary?.lastMessage == null) {
|
if (leftRoomSummary?.lastMessage == null) {
|
||||||
retValue = -1
|
|
||||||
} else if (leftRoomSummary?.lastMessage == null) {
|
|
||||||
retValue = 1
|
retValue = 1
|
||||||
|
} else if (rightRoomSummary?.lastMessage == null) {
|
||||||
|
retValue = -1
|
||||||
} else if (rightHighlightCount > 0 && leftHighlightCount == 0) {
|
} else if (rightHighlightCount > 0 && leftHighlightCount == 0) {
|
||||||
retValue = 1
|
retValue = 1
|
||||||
} else if (rightHighlightCount == 0 && leftHighlightCount > 0) {
|
} else if (rightHighlightCount == 0 && leftHighlightCount > 0) {
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user