forked from GitHub-Mirror/riotX-android
Compare commits
5 Commits
feature/fi
...
feature/re
Author | SHA1 | Date | |
---|---|---|---|
4048658c56 | |||
8d13f08574 | |||
a5032920c2 | |||
68b9ba7b99 | |||
56af92d759 |
@ -1,53 +0,0 @@
|
|||||||
# Use Docker file from https://hub.docker.com/r/runmymind/docker-android-sdk
|
|
||||||
# Last docker plugin version can be found here:
|
|
||||||
# https://github.com/buildkite-plugins/docker-buildkite-plugin/releases
|
|
||||||
|
|
||||||
# Build debug version of the RiotX application, from the develop branch and the features branches
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- label: "Assemble GPlay Debug version"
|
|
||||||
agents:
|
|
||||||
# We use a medium sized instance instead of the normal small ones because
|
|
||||||
# gradle build is long
|
|
||||||
queue: "medium"
|
|
||||||
commands:
|
|
||||||
- "./gradlew clean lintGplayRelease assembleGplayDebug --stacktrace"
|
|
||||||
artifact_paths:
|
|
||||||
- "vector/build/outputs/apk/gplay/debug/*.apk"
|
|
||||||
branches: "develop feature/*"
|
|
||||||
plugins:
|
|
||||||
- docker#v3.1.0:
|
|
||||||
image: "runmymind/docker-android-sdk"
|
|
||||||
|
|
||||||
- label: "Assemble FDroid Debug version"
|
|
||||||
agents:
|
|
||||||
# We use a medium sized instance instead of the normal small ones because
|
|
||||||
# gradle build is long
|
|
||||||
queue: "medium"
|
|
||||||
commands:
|
|
||||||
- "./gradlew clean lintFdroidRelease assembleFdroidDebug --stacktrace"
|
|
||||||
artifact_paths:
|
|
||||||
- "vector/build/outputs/apk/fdroid/debug/*.apk"
|
|
||||||
branches: "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'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx
|
package im.vector.riotredesign
|
||||||
|
|
||||||
import androidx.test.InstrumentationRegistry
|
import androidx.test.InstrumentationRegistry
|
||||||
import androidx.test.runner.AndroidJUnit4
|
import androidx.test.runner.AndroidJUnit4
|
||||||
@ -35,6 +35,6 @@ class ExampleInstrumentedTest {
|
|||||||
fun useAppContext() {
|
fun useAppContext() {
|
||||||
// Context of the app under test.
|
// Context of the app under test.
|
||||||
val appContext = InstrumentationRegistry.getTargetContext()
|
val appContext = InstrumentationRegistry.getTargetContext()
|
||||||
assertEquals("im.vector.riotx", appContext.packageName)
|
assertEquals("im.vector.riotredesign", appContext.packageName)
|
||||||
}
|
}
|
||||||
}
|
}
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotredesign.core.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Context.MODE_PRIVATE
|
||||||
|
import im.vector.riotredesign.core.resources.LocaleProvider
|
||||||
|
import im.vector.riotredesign.core.resources.StringProvider
|
||||||
|
import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository
|
||||||
|
import org.koin.dsl.module.module
|
||||||
|
|
||||||
|
class AppModule(private val context: Context) {
|
||||||
|
|
||||||
|
val definition = module {
|
||||||
|
|
||||||
|
single {
|
||||||
|
LocaleProvider(context.resources)
|
||||||
|
}
|
||||||
|
|
||||||
|
single {
|
||||||
|
StringProvider(context.resources)
|
||||||
|
}
|
||||||
|
|
||||||
|
single {
|
||||||
|
context.getSharedPreferences("im.vector.riot", MODE_PRIVATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
single {
|
||||||
|
RoomSelectionRepository(get())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.core.epoxy
|
package im.vector.riotredesign.core.epoxy
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import com.airbnb.epoxy.EpoxyHolder
|
import com.airbnb.epoxy.EpoxyHolder
|
||||||
@ -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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.core.extensions
|
package im.vector.riotredesign.core.extensions
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
@ -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()
|
|
||||||
}
|
|
@ -14,10 +14,9 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.core.di
|
package im.vector.riotredesign.core.extensions
|
||||||
|
|
||||||
interface HasScreenInjector {
|
|
||||||
|
|
||||||
fun injector(): ScreenComponent
|
|
||||||
|
|
||||||
|
fun CharSequence.firstCharAsString(): String {
|
||||||
|
return if (isNotEmpty()) this[0].toString() else ""
|
||||||
}
|
}
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.core.extensions
|
package im.vector.riotredesign.core.extensions
|
||||||
|
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
@ -23,7 +23,7 @@ import android.view.MotionEvent
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import im.vector.riotx.R
|
import im.vector.riotredesign.R
|
||||||
|
|
||||||
fun EditText.setupAsSearch() {
|
fun EditText.setupAsSearch() {
|
||||||
addTextChangedListener(object : TextWatcher {
|
addTextChangedListener(object : TextWatcher {
|
@ -14,12 +14,15 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.core.extensions
|
package im.vector.riotredesign.core.extensions
|
||||||
|
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.riotx.core.resources.DateProvider
|
import org.threeten.bp.Instant
|
||||||
import org.threeten.bp.LocalDateTime
|
import org.threeten.bp.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,30 +14,30 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.core.extensions
|
package im.vector.riotredesign.core.extensions
|
||||||
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
|
||||||
fun Fragment.addFragment(fragment: Fragment, frameId: Int) {
|
fun androidx.fragment.app.Fragment.addFragment(fragment: Fragment, frameId: Int) {
|
||||||
fragmentManager?.inTransaction { add(frameId, fragment) }
|
fragmentManager?.inTransaction { add(frameId, fragment) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Fragment.replaceFragment(fragment: Fragment, frameId: Int) {
|
fun androidx.fragment.app.Fragment.replaceFragment(fragment: Fragment, frameId: Int) {
|
||||||
fragmentManager?.inTransaction { replace(frameId, fragment) }
|
fragmentManager?.inTransaction { replace(frameId, fragment) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Fragment.addFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
|
fun androidx.fragment.app.Fragment.addFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
|
||||||
fragmentManager?.inTransaction { replace(frameId, fragment).addToBackStack(tag) }
|
fragmentManager?.inTransaction { replace(frameId, fragment).addToBackStack(tag) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Fragment.addChildFragment(fragment: Fragment, frameId: Int) {
|
fun androidx.fragment.app.Fragment.addChildFragment(fragment: Fragment, frameId: Int) {
|
||||||
childFragmentManager.inTransaction { add(frameId, fragment) }
|
childFragmentManager.inTransaction { add(frameId, fragment) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Fragment.replaceChildFragment(fragment: Fragment, frameId: Int) {
|
fun androidx.fragment.app.Fragment.replaceChildFragment(fragment: Fragment, frameId: Int) {
|
||||||
childFragmentManager.inTransaction { replace(frameId, fragment) }
|
childFragmentManager.inTransaction { replace(frameId, fragment) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Fragment.addChildFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
|
fun androidx.fragment.app.Fragment.addChildFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
|
||||||
childFragmentManager.inTransaction { replace(frameId, fragment).addToBackStack(tag) }
|
childFragmentManager.inTransaction { replace(frameId, fragment).addToBackStack(tag) }
|
||||||
}
|
}
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.core.extensions
|
package im.vector.riotredesign.core.extensions
|
||||||
|
|
||||||
import androidx.fragment.app.FragmentTransaction
|
import androidx.fragment.app.FragmentTransaction
|
||||||
|
|
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * Copyright 2019 New Vector Ltd
|
||||||
|
* *
|
||||||
|
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* * you may not use this file except in compliance with the License.
|
||||||
|
* * You may obtain a copy of the License at
|
||||||
|
* *
|
||||||
|
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* *
|
||||||
|
* * Unless required by applicable law or agreed to in writing, software
|
||||||
|
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* * See the License for the specific language governing permissions and
|
||||||
|
* * limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotredesign.core.extensions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the last element yielding the smallest value of the given function or `null` if there are no elements.
|
||||||
|
*/
|
||||||
|
public inline fun <T, R : Comparable<R>> Iterable<T>.lastMinBy(selector: (T) -> R): T? {
|
||||||
|
val iterator = iterator()
|
||||||
|
if (!iterator.hasNext()) return null
|
||||||
|
var minElem = iterator.next()
|
||||||
|
var minValue = selector(minElem)
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
val e = iterator.next()
|
||||||
|
val v = selector(e)
|
||||||
|
if (minValue >= v) {
|
||||||
|
minElem = e
|
||||||
|
minValue = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return minElem
|
||||||
|
}
|
@ -14,14 +14,13 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.core.extensions
|
package im.vector.riotredesign.core.extensions
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import im.vector.riotx.core.utils.Debouncer
|
import im.vector.riotredesign.core.utils.LiveEvent
|
||||||
import im.vector.riotx.core.utils.EventObserver
|
import im.vector.riotredesign.core.utils.EventObserver
|
||||||
import im.vector.riotx.core.utils.LiveEvent
|
|
||||||
|
|
||||||
inline fun <T> LiveData<T>.observeK(owner: LifecycleOwner, crossinline observer: (T?) -> Unit) {
|
inline fun <T> LiveData<T>.observeK(owner: LifecycleOwner, crossinline observer: (T?) -> Unit) {
|
||||||
this.observe(owner, Observer { observer(it) })
|
this.observe(owner, Observer { observer(it) })
|
||||||
@ -33,14 +32,4 @@ inline fun <T> LiveData<T>.observeNotNull(owner: LifecycleOwner, crossinline obs
|
|||||||
|
|
||||||
inline fun <T> LiveData<LiveEvent<T>>.observeEvent(owner: LifecycleOwner, crossinline observer: (T) -> Unit) {
|
inline fun <T> LiveData<LiveEvent<T>>.observeEvent(owner: LifecycleOwner, crossinline observer: (T) -> Unit) {
|
||||||
this.observe(owner, EventObserver { it.run(observer) })
|
this.observe(owner, EventObserver { it.run(observer) })
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <T> LiveData<LiveEvent<T>>.observeEventDebounced(owner: LifecycleOwner, minimumInterval: Long, crossinline observer: (T) -> Unit) {
|
|
||||||
val debouncer = Debouncer(minimumInterval)
|
|
||||||
|
|
||||||
this.observe(owner, EventObserver {
|
|
||||||
if (debouncer.canHandle()) {
|
|
||||||
it.run(observer)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.core.extensions
|
package im.vector.riotredesign.core.extensions
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.View
|
import android.view.View
|
@ -14,22 +14,21 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.matrix.android.api.util
|
package im.vector.riotredesign.core.glide;
|
||||||
|
|
||||||
class CancelableBag : Cancelable {
|
import android.content.Context;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
private val cancelableList = ArrayList<Cancelable>()
|
import com.bumptech.glide.GlideBuilder;
|
||||||
|
import com.bumptech.glide.annotation.GlideModule;
|
||||||
|
import com.bumptech.glide.module.AppGlideModule;
|
||||||
|
|
||||||
fun add(cancelable: Cancelable) {
|
@GlideModule
|
||||||
cancelableList.add(cancelable)
|
public final class MyAppGlideModule extends AppGlideModule {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void applyOptions(Context context, GlideBuilder builder) {
|
||||||
|
builder.setLogLevel(Log.ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cancel() {
|
|
||||||
cancelableList.forEach { it.cancel() }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Cancelable.addTo(cancelables: CancelableBag) {
|
|
||||||
cancelables.add(this)
|
|
||||||
}
|
}
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.core.platform
|
package im.vector.riotredesign.core.platform
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.core.platform
|
package im.vector.riotredesign.core.platform
|
||||||
|
|
||||||
import androidx.recyclerview.widget.ListUpdateCallback
|
import androidx.recyclerview.widget.ListUpdateCallback
|
||||||
|
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.core.platform
|
package im.vector.riotredesign.core.platform
|
||||||
|
|
||||||
interface OnBackPressed {
|
interface OnBackPressed {
|
||||||
|
|
@ -14,19 +14,19 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.features.home.room.detail.timeline.animation
|
package im.vector.riotredesign.core.platform
|
||||||
|
|
||||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
import com.airbnb.mvrx.BaseMvRxActivity
|
||||||
|
import io.reactivex.disposables.CompositeDisposable
|
||||||
|
import io.reactivex.disposables.Disposable
|
||||||
|
|
||||||
private const val ANIM_DURATION_IN_MILLIS = 100L
|
abstract class RiotActivity : BaseMvRxActivity() {
|
||||||
|
|
||||||
class TimelineItemAnimator : DefaultItemAnimator() {
|
private val uiDisposables = CompositeDisposable()
|
||||||
|
|
||||||
init {
|
protected fun Disposable.disposeOnDestroy(): Disposable {
|
||||||
addDuration = ANIM_DURATION_IN_MILLIS
|
uiDisposables.add(this)
|
||||||
removeDuration = 0
|
return this
|
||||||
moveDuration = 0
|
|
||||||
changeDuration = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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) } }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -14,11 +14,10 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.core.platform
|
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.riotx.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)
|
@ -14,14 +14,14 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.core.platform
|
package im.vector.riotredesign.core.platform
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import im.vector.riotx.R
|
import im.vector.riotredesign.R
|
||||||
import kotlinx.android.synthetic.main.view_state.view.*
|
import kotlinx.android.synthetic.main.view_state.view.*
|
||||||
|
|
||||||
class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0)
|
class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0)
|
||||||
@ -30,7 +30,7 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
|
|||||||
sealed class State {
|
sealed class State {
|
||||||
object Content : State()
|
object Content : State()
|
||||||
object Loading : State()
|
object Loading : State()
|
||||||
data class Empty(val title: CharSequence? = null, val image: Drawable? = null, val message: CharSequence? = null) : State()
|
data class Empty(val message: CharSequence? = null) : State()
|
||||||
data class Error(val message: CharSequence? = null) : State()
|
data class Error(val message: CharSequence? = null) : State()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +52,7 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
View.inflate(context, R.layout.view_state, this)
|
View.inflate(context, R.layout.view_state, this)
|
||||||
layoutParams = LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
|
layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
||||||
errorRetryView.setOnClickListener {
|
errorRetryView.setOnClickListener {
|
||||||
eventCallback?.onRetryClicked()
|
eventCallback?.onRetryClicked()
|
||||||
}
|
}
|
||||||
@ -62,33 +62,35 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
|
|||||||
|
|
||||||
private fun update(newState: State) {
|
private fun update(newState: State) {
|
||||||
when (newState) {
|
when (newState) {
|
||||||
is State.Content -> {
|
is StateView.State.Content -> {
|
||||||
progressBar.visibility = View.INVISIBLE
|
progressBar.visibility = View.INVISIBLE
|
||||||
errorView.visibility = View.INVISIBLE
|
errorView.visibility = View.INVISIBLE
|
||||||
emptyView.visibility = View.INVISIBLE
|
emptyView.visibility = View.INVISIBLE
|
||||||
contentView?.visibility = View.VISIBLE
|
contentView?.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
is State.Loading -> {
|
is StateView.State.Loading -> {
|
||||||
progressBar.visibility = View.VISIBLE
|
progressBar.visibility = View.VISIBLE
|
||||||
errorView.visibility = View.INVISIBLE
|
errorView.visibility = View.INVISIBLE
|
||||||
emptyView.visibility = View.INVISIBLE
|
emptyView.visibility = View.INVISIBLE
|
||||||
contentView?.visibility = View.INVISIBLE
|
contentView?.visibility = View.INVISIBLE
|
||||||
}
|
}
|
||||||
is State.Empty -> {
|
is StateView.State.Empty -> {
|
||||||
progressBar.visibility = View.INVISIBLE
|
progressBar.visibility = View.INVISIBLE
|
||||||
errorView.visibility = View.INVISIBLE
|
errorView.visibility = View.INVISIBLE
|
||||||
emptyView.visibility = View.VISIBLE
|
emptyView.visibility = View.VISIBLE
|
||||||
emptyImageView.setImageDrawable(newState.image)
|
|
||||||
emptyMessageView.text = newState.message
|
emptyMessageView.text = newState.message
|
||||||
emptyTitleView.text = newState.title
|
if (contentView != null) {
|
||||||
contentView?.visibility = View.INVISIBLE
|
contentView!!.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is State.Error -> {
|
is StateView.State.Error -> {
|
||||||
progressBar.visibility = View.INVISIBLE
|
progressBar.visibility = View.INVISIBLE
|
||||||
errorView.visibility = View.VISIBLE
|
errorView.visibility = View.VISIBLE
|
||||||
emptyView.visibility = View.INVISIBLE
|
emptyView.visibility = View.INVISIBLE
|
||||||
errorMessageView.text = newState.message
|
errorMessageView.text = newState.message
|
||||||
contentView?.visibility = View.INVISIBLE
|
if (contentView != null) {
|
||||||
|
contentView!!.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.core.platform
|
package im.vector.riotredesign.core.platform
|
||||||
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
|
@ -14,16 +14,17 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.core.resources
|
package im.vector.riotredesign.core.resources
|
||||||
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import androidx.core.os.ConfigurationCompat
|
import androidx.core.os.ConfigurationCompat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class LocaleProvider @Inject constructor(private val resources: Resources) {
|
class LocaleProvider(private val resources: Resources) {
|
||||||
|
|
||||||
fun current(): Locale {
|
fun current(): Locale {
|
||||||
return ConfigurationCompat.getLocales(resources.configuration)[0]
|
return ConfigurationCompat.getLocales(resources.configuration)[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -14,18 +14,13 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.util
|
package im.vector.riotredesign.core.resources
|
||||||
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import androidx.annotation.NonNull
|
import androidx.annotation.NonNull
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import dagger.Reusable
|
|
||||||
import im.vector.matrix.android.internal.di.MatrixScope
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Reusable
|
class StringProvider(private val resources: Resources) {
|
||||||
internal class StringProvider @Inject constructor(private val resources: Resources) {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a localized string from the application's package's
|
* Returns a localized string from the application's package's
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.core.utils
|
package im.vector.riotredesign.core.utils
|
||||||
|
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotredesign.features.home
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.MenuItem
|
||||||
|
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.core.view.GravityCompat
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import com.airbnb.mvrx.viewModel
|
||||||
|
import im.vector.riotredesign.R
|
||||||
|
import im.vector.riotredesign.core.extensions.observeEvent
|
||||||
|
import im.vector.riotredesign.core.extensions.replaceFragment
|
||||||
|
import im.vector.riotredesign.core.platform.OnBackPressed
|
||||||
|
import im.vector.riotredesign.core.platform.RiotActivity
|
||||||
|
import im.vector.riotredesign.core.platform.ToolbarConfigurable
|
||||||
|
import im.vector.riotredesign.features.home.room.detail.LoadingRoomDetailFragment
|
||||||
|
import kotlinx.android.synthetic.main.activity_home.*
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
|
import org.koin.standalone.StandAloneContext.loadKoinModules
|
||||||
|
|
||||||
|
|
||||||
|
class HomeActivity : RiotActivity(), ToolbarConfigurable {
|
||||||
|
|
||||||
|
|
||||||
|
private val homeActivityViewModel: HomeActivityViewModel by viewModel()
|
||||||
|
private val homeNavigator by inject<HomeNavigator>()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
loadKoinModules(listOf(HomeModule().definition))
|
||||||
|
homeNavigator.activity = this
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_home)
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
val homeDrawerFragment = HomeDrawerFragment.newInstance()
|
||||||
|
val loadingDetail = LoadingRoomDetailFragment.newInstance()
|
||||||
|
replaceFragment(loadingDetail, R.id.homeDetailFragmentContainer)
|
||||||
|
replaceFragment(homeDrawerFragment, R.id.homeDrawerFragmentContainer)
|
||||||
|
}
|
||||||
|
homeActivityViewModel.openRoomLiveData.observeEvent(this) {
|
||||||
|
homeNavigator.openRoomDetail(it, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
homeNavigator.activity = null
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun configure(toolbar: Toolbar) {
|
||||||
|
setSupportActionBar(toolbar)
|
||||||
|
supportActionBar?.setHomeButtonEnabled(true)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
val drawerToggle = ActionBarDrawerToggle(this, drawerLayout, toolbar, 0, 0)
|
||||||
|
drawerLayout.addDrawerListener(drawerToggle)
|
||||||
|
drawerToggle.syncState()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
android.R.id.home -> {
|
||||||
|
drawerLayout.openDrawer(GravityCompat.START)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
if (drawerLayout.isDrawerOpen(Gravity.LEFT)) {
|
||||||
|
drawerLayout.closeDrawer(Gravity.LEFT)
|
||||||
|
} else {
|
||||||
|
val handled = recursivelyDispatchOnBackPressed(supportFragmentManager)
|
||||||
|
if (!handled) {
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun recursivelyDispatchOnBackPressed(fm: FragmentManager): Boolean {
|
||||||
|
if (fm.backStackEntryCount == 0)
|
||||||
|
return false
|
||||||
|
val reverseOrder = fm.fragments.filter { it is OnBackPressed }.reversed()
|
||||||
|
for (f in reverseOrder) {
|
||||||
|
val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager)
|
||||||
|
if (handledByChildFragments) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
val backPressable = f as OnBackPressed
|
||||||
|
if (backPressable.onBackPressed()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newIntent(context: Context): Intent {
|
||||||
|
return Intent(context, HomeActivity::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotredesign.features.home
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import com.airbnb.mvrx.MvRxState
|
||||||
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
|
import im.vector.matrix.android.api.Matrix
|
||||||
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
import im.vector.matrix.rx.rx
|
||||||
|
import im.vector.riotredesign.core.platform.RiotViewModel
|
||||||
|
import im.vector.riotredesign.core.utils.LiveEvent
|
||||||
|
import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository
|
||||||
|
import io.reactivex.rxkotlin.subscribeBy
|
||||||
|
import org.koin.android.ext.android.get
|
||||||
|
|
||||||
|
class EmptyState : MvRxState
|
||||||
|
|
||||||
|
class HomeActivityViewModel(state: EmptyState,
|
||||||
|
private val session: Session,
|
||||||
|
roomSelectionRepository: RoomSelectionRepository
|
||||||
|
) : RiotViewModel<EmptyState>(state) {
|
||||||
|
|
||||||
|
companion object : MvRxViewModelFactory<HomeActivityViewModel, EmptyState> {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
override fun create(viewModelContext: ViewModelContext, state: EmptyState): HomeActivityViewModel? {
|
||||||
|
val session = Matrix.getInstance().currentSession
|
||||||
|
val roomSelectionRepository = viewModelContext.activity.get<RoomSelectionRepository>()
|
||||||
|
return HomeActivityViewModel(state, session, roomSelectionRepository)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _openRoomLiveData = MutableLiveData<LiveEvent<String>>()
|
||||||
|
val openRoomLiveData: LiveData<LiveEvent<String>>
|
||||||
|
get() = _openRoomLiveData
|
||||||
|
|
||||||
|
init {
|
||||||
|
val lastSelectedRoomId = roomSelectionRepository.lastSelectedRoom()
|
||||||
|
if (lastSelectedRoomId == null || session.getRoom(lastSelectedRoomId) == null) {
|
||||||
|
getTheFirstRoomWhenAvailable()
|
||||||
|
} else {
|
||||||
|
_openRoomLiveData.postValue(LiveEvent(lastSelectedRoomId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTheFirstRoomWhenAvailable() {
|
||||||
|
session.rx().liveRoomSummaries()
|
||||||
|
.filter { it.isNotEmpty() }
|
||||||
|
.first(emptyList())
|
||||||
|
.subscribeBy {
|
||||||
|
val firstRoom = it.firstOrNull()
|
||||||
|
if (firstRoom != null) {
|
||||||
|
_openRoomLiveData.postValue(LiveEvent(firstRoom.roomId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.disposeOnClear()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotredesign.features.home
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import im.vector.riotredesign.R
|
||||||
|
import im.vector.riotredesign.core.extensions.replaceChildFragment
|
||||||
|
import im.vector.riotredesign.core.platform.RiotFragment
|
||||||
|
import im.vector.riotredesign.features.home.group.GroupListFragment
|
||||||
|
import im.vector.riotredesign.features.home.room.list.RoomListFragment
|
||||||
|
|
||||||
|
class HomeDrawerFragment : RiotFragment() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun newInstance(): HomeDrawerFragment {
|
||||||
|
return HomeDrawerFragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
return inflater.inflate(R.layout.fragment_home_drawer, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
val groupListFragment = GroupListFragment.newInstance()
|
||||||
|
replaceChildFragment(groupListFragment, R.id.groupListFragmentContainer)
|
||||||
|
val roomListFragment = RoomListFragment.newInstance()
|
||||||
|
replaceChildFragment(roomListFragment, R.id.roomListFragmentContainer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotredesign.features.home
|
||||||
|
|
||||||
|
import android.view.Gravity
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import im.vector.riotredesign.R
|
||||||
|
import im.vector.riotredesign.core.extensions.addFragmentToBackstack
|
||||||
|
import im.vector.riotredesign.core.extensions.replaceFragment
|
||||||
|
import im.vector.riotredesign.features.home.room.detail.RoomDetailArgs
|
||||||
|
import im.vector.riotredesign.features.home.room.detail.RoomDetailFragment
|
||||||
|
import kotlinx.android.synthetic.main.activity_home.*
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class HomeNavigator {
|
||||||
|
|
||||||
|
var activity: HomeActivity? = null
|
||||||
|
|
||||||
|
private var currentRoomId: String? = null
|
||||||
|
|
||||||
|
fun openRoomDetail(roomId: String,
|
||||||
|
eventId: String?,
|
||||||
|
addToBackstack: Boolean = false) {
|
||||||
|
Timber.v("Open room detail $roomId - $eventId - $addToBackstack")
|
||||||
|
if (!addToBackstack && isRoomOpened(roomId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
activity?.let {
|
||||||
|
val args = RoomDetailArgs(roomId, eventId)
|
||||||
|
val roomDetailFragment = RoomDetailFragment.newInstance(args)
|
||||||
|
it.drawerLayout?.closeDrawer(Gravity.LEFT)
|
||||||
|
if (addToBackstack) {
|
||||||
|
it.addFragmentToBackstack(roomDetailFragment, R.id.homeDetailFragmentContainer, roomId)
|
||||||
|
} else {
|
||||||
|
currentRoomId = roomId
|
||||||
|
clearBackStack(it.supportFragmentManager)
|
||||||
|
it.replaceFragment(roomDetailFragment, R.id.homeDetailFragmentContainer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openGroupDetail(groupId: String) {
|
||||||
|
Timber.v("Open group detail $groupId")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openUserDetail(userId: String) {
|
||||||
|
Timber.v("Open user detail $userId")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isRoomOpened(roomId: String): Boolean {
|
||||||
|
return currentRoomId == roomId
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearBackStack(fragmentManager: FragmentManager) {
|
||||||
|
if (fragmentManager.backStackEntryCount > 0) {
|
||||||
|
val first = fragmentManager.getBackStackEntryAt(0)
|
||||||
|
fragmentManager.popBackStack(first.id, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotredesign.features.home
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import im.vector.matrix.android.api.permalinks.PermalinkData
|
||||||
|
import im.vector.matrix.android.api.permalinks.PermalinkParser
|
||||||
|
|
||||||
|
class HomePermalinkHandler(private val navigator: HomeNavigator) {
|
||||||
|
|
||||||
|
fun launch(deepLink: String?) {
|
||||||
|
val uri = deepLink?.let { Uri.parse(it) }
|
||||||
|
launch(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launch(deepLink: Uri?) {
|
||||||
|
if (deepLink == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val permalinkData = PermalinkParser.parse(deepLink)
|
||||||
|
when (permalinkData) {
|
||||||
|
is PermalinkData.EventLink -> {
|
||||||
|
navigator.openRoomDetail(permalinkData.roomIdOrAlias, permalinkData.eventId, true)
|
||||||
|
}
|
||||||
|
is PermalinkData.RoomLink -> {
|
||||||
|
navigator.openRoomDetail(permalinkData.roomIdOrAlias, null, true)
|
||||||
|
}
|
||||||
|
is PermalinkData.GroupLink -> {
|
||||||
|
navigator.openGroupDetail(permalinkData.groupId)
|
||||||
|
}
|
||||||
|
is PermalinkData.UserLink -> {
|
||||||
|
navigator.openUserDetail(permalinkData.userId)
|
||||||
|
}
|
||||||
|
is PermalinkData.FallbackLink -> {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -14,14 +14,11 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.core.dialogs
|
package im.vector.riotredesign.features.home
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import com.airbnb.epoxy.ModelView
|
||||||
|
|
||||||
internal class DialogCallAdapter(context: Context) : DialogAdapter(context) {
|
@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
|
||||||
|
class LoadingItem(context: Context) : ProgressBar(context)
|
||||||
init {
|
|
||||||
add(DialogListItem.StartVoiceCall)
|
|
||||||
add(DialogListItem.StartVideoCall)
|
|
||||||
}
|
|
||||||
}
|
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.features.home.group
|
package im.vector.riotredesign.features.home.group
|
||||||
|
|
||||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||||
|
|
@ -14,23 +14,22 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.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.riotx.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotx.core.di.ScreenComponent
|
import im.vector.riotredesign.core.platform.RiotFragment
|
||||||
import im.vector.riotx.core.extensions.observeEvent
|
import im.vector.riotredesign.core.platform.StateView
|
||||||
import im.vector.riotx.core.platform.StateView
|
|
||||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
|
||||||
import im.vector.riotx.features.home.HomeNavigator
|
|
||||||
import kotlinx.android.synthetic.main.fragment_group_list.*
|
import kotlinx.android.synthetic.main.fragment_group_list.*
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class GroupListFragment : VectorBaseFragment(), GroupSummaryController.Callback {
|
class GroupListFragment : RiotFragment(), GroupSummaryController.Callback {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun newInstance(): GroupListFragment {
|
fun newInstance(): GroupListFragment {
|
||||||
@ -40,32 +39,24 @@ class GroupListFragment : VectorBaseFragment(), GroupSummaryController.Callback
|
|||||||
|
|
||||||
private val viewModel: GroupListViewModel by fragmentViewModel()
|
private val viewModel: GroupListViewModel by fragmentViewModel()
|
||||||
|
|
||||||
@Inject lateinit var groupListViewModelFactory: GroupListViewModel.Factory
|
private lateinit var groupController: GroupSummaryController
|
||||||
@Inject lateinit var homeNavigator: HomeNavigator
|
|
||||||
@Inject lateinit var groupController: GroupSummaryController
|
|
||||||
|
|
||||||
override fun getLayoutResId() = R.layout.fragment_group_list
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
return inflater.inflate(R.layout.fragment_group_list, container, false)
|
||||||
override fun injectWith(injector: ScreenComponent) {
|
|
||||||
injector.inject(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
groupController.callback = this
|
groupController = GroupSummaryController(this)
|
||||||
stateView.contentView = groupListEpoxyRecyclerView
|
stateView.contentView = epoxyRecyclerView
|
||||||
groupListEpoxyRecyclerView.setHasFixedSize(true)
|
epoxyRecyclerView.setController(groupController)
|
||||||
groupListEpoxyRecyclerView.setController(groupController)
|
|
||||||
viewModel.subscribe { renderState(it) }
|
viewModel.subscribe { renderState(it) }
|
||||||
viewModel.openGroupLiveData.observeEvent(this) {
|
|
||||||
homeNavigator.openSelectedGroup(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotredesign.features.home.group
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
|
import im.vector.matrix.android.api.Matrix
|
||||||
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
import im.vector.matrix.rx.rx
|
||||||
|
import im.vector.riotredesign.core.platform.RiotViewModel
|
||||||
|
import org.koin.android.ext.android.get
|
||||||
|
|
||||||
|
class GroupListViewModel(initialState: GroupListViewState,
|
||||||
|
private val selectedGroupHolder: SelectedGroupHolder,
|
||||||
|
private val session: Session
|
||||||
|
) : RiotViewModel<GroupListViewState>(initialState) {
|
||||||
|
|
||||||
|
companion object : MvRxViewModelFactory<GroupListViewModel, GroupListViewState> {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
override fun create(viewModelContext: ViewModelContext, state: GroupListViewState): GroupListViewModel? {
|
||||||
|
val currentSession = Matrix.getInstance().currentSession
|
||||||
|
val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupHolder>()
|
||||||
|
return GroupListViewModel(state, selectedGroupHolder, currentSession)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
observeGroupSummaries()
|
||||||
|
observeState()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeState() {
|
||||||
|
subscribe {
|
||||||
|
selectedGroupHolder.setSelectedGroup(it.selectedGroup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun accept(action: GroupListActions) {
|
||||||
|
when (action) {
|
||||||
|
is GroupListActions.SelectGroup -> handleSelectGroup(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PRIVATE METHODS *****************************************************************************
|
||||||
|
|
||||||
|
private fun handleSelectGroup(action: GroupListActions.SelectGroup) = withState { state ->
|
||||||
|
if (state.selectedGroup?.groupId != action.groupSummary.groupId) {
|
||||||
|
setState { copy(selectedGroup = action.groupSummary) }
|
||||||
|
} else {
|
||||||
|
setState { copy(selectedGroup = null) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun observeGroupSummaries() {
|
||||||
|
session
|
||||||
|
.rx().liveGroupSummaries()
|
||||||
|
.execute { async ->
|
||||||
|
copy(asyncGroups = async)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.features.home.group
|
package im.vector.riotredesign.features.home.group
|
||||||
|
|
||||||
import com.airbnb.mvrx.Async
|
import com.airbnb.mvrx.Async
|
||||||
import com.airbnb.mvrx.MvRxState
|
import com.airbnb.mvrx.MvRxState
|
@ -14,16 +14,13 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.features.home.group
|
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
|
||||||
import im.vector.riotx.features.home.AvatarRenderer
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class GroupSummaryController @Inject constructor(private val avatarRenderer: AvatarRenderer): 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)
|
||||||
@ -35,15 +32,14 @@ class GroupSummaryController @Inject constructor(private val avatarRenderer: Ava
|
|||||||
}
|
}
|
||||||
summaries.forEach { groupSummary ->
|
summaries.forEach { groupSummary ->
|
||||||
val isSelected = groupSummary.groupId == selected?.groupId
|
val isSelected = groupSummary.groupId == selected?.groupId
|
||||||
groupSummaryItem {
|
GroupSummaryItem(
|
||||||
avatarRenderer(avatarRenderer)
|
groupName = groupSummary.displayName,
|
||||||
id(groupSummary.groupId)
|
avatarUrl = groupSummary.avatarUrl,
|
||||||
groupId(groupSummary.groupId)
|
isSelected = isSelected,
|
||||||
groupName(groupSummary.displayName)
|
listener = { callback?.onGroupSelected(groupSummary) }
|
||||||
selected(isSelected)
|
)
|
||||||
avatarUrl(groupSummary.avatarUrl)
|
.id(groupSummary.groupId)
|
||||||
listener { callback?.onGroupSelected(groupSummary) }
|
.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)
|
||||||
|
}
|
||||||
|
}
|
@ -14,13 +14,25 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.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.riotx.core.utils.RxStore
|
import io.reactivex.Observable
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
class SelectedGroupHolder {
|
||||||
class SelectedGroupStore @Inject constructor() : RxStore<Option<GroupSummary>>(Option.empty())
|
|
||||||
|
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.riotx.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)
|
}
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotredesign.features.home.room.detail
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import im.vector.riotredesign.R
|
||||||
|
import im.vector.riotredesign.core.platform.RiotFragment
|
||||||
|
import kotlinx.android.synthetic.main.fragment_loading_room_detail.*
|
||||||
|
|
||||||
|
class LoadingRoomDetailFragment : RiotFragment() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun newInstance(): LoadingRoomDetailFragment {
|
||||||
|
return LoadingRoomDetailFragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
return inflater.inflate(R.layout.fragment_loading_room_detail, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
Glide.with(this)
|
||||||
|
.load(R.drawable.riot_splash)
|
||||||
|
.into(animatedLogoImageView)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -14,40 +14,21 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.features.home.room.detail
|
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.TimelineEvent
|
|
||||||
import im.vector.matrix.android.api.session.user.model.User
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Describes the current send mode:
|
|
||||||
* REGULAR: sends the text as a regular message
|
|
||||||
* QUOTE: User is currently quoting a message
|
|
||||||
* EDIT: User is currently editing an existing message
|
|
||||||
*
|
|
||||||
* Depending on the state the bottom toolbar will change (icons/preview/actions...)
|
|
||||||
*/
|
|
||||||
enum class SendMode {
|
|
||||||
REGULAR,
|
|
||||||
QUOTE,
|
|
||||||
EDIT,
|
|
||||||
REPLY
|
|
||||||
}
|
|
||||||
|
|
||||||
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 asyncInviter: Async<User> = Uninitialized,
|
|
||||||
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
||||||
val sendMode: SendMode = SendMode.REGULAR,
|
val asyncTimelineData: Async<TimelineData> = Uninitialized
|
||||||
val selectedEvent: TimelineEvent? = null,
|
|
||||||
val isEncrypted: Boolean = false
|
|
||||||
) : MvRxState {
|
) : MvRxState {
|
||||||
|
|
||||||
constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)
|
constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)
|
@ -14,15 +14,18 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.features.home.room.detail
|
package im.vector.riotredesign.features.home.room.detail
|
||||||
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import im.vector.riotx.core.platform.DefaultListUpdateCallback
|
import im.vector.riotredesign.core.platform.DefaultListUpdateCallback
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager) : DefaultListUpdateCallback {
|
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,13 +14,16 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.core.dialogs
|
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||||
|
|
||||||
import android.content.Context
|
import im.vector.riotredesign.R
|
||||||
|
import im.vector.riotredesign.core.epoxy.KotlinModel
|
||||||
|
|
||||||
internal class DialogSendItemAdapter(context: Context, items: MutableList<DialogListItem>) : DialogAdapter(context) {
|
class BlankItem
|
||||||
|
: KotlinModel(R.layout.item_timeline_event_blank) {
|
||||||
|
|
||||||
init {
|
override fun bind() {
|
||||||
addAll(items)
|
//no-op
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
}
|
@ -14,24 +14,19 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.core.dialogs
|
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import butterknife.BindView
|
import im.vector.riotredesign.R
|
||||||
import butterknife.ButterKnife
|
import im.vector.riotredesign.core.epoxy.KotlinModel
|
||||||
import im.vector.riotx.R
|
|
||||||
|
|
||||||
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.riotx.features.home.room.list
|
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||||
|
|
||||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
import android.widget.TextView
|
||||||
|
import im.vector.riotredesign.R
|
||||||
|
import im.vector.riotredesign.core.epoxy.KotlinModel
|
||||||
|
|
||||||
private const val ANIM_DURATION_IN_MILLIS = 200L
|
class DefaultItem(
|
||||||
|
val text: CharSequence? = null
|
||||||
|
) : KotlinModel(R.layout.item_timeline_event_default) {
|
||||||
|
|
||||||
class RoomListAnimator : DefaultItemAnimator() {
|
private val messageView by bind<TextView>(R.id.stateMessageView)
|
||||||
|
|
||||||
init {
|
override fun bind() {
|
||||||
addDuration = ANIM_DURATION_IN_MILLIS
|
messageView.text = text
|
||||||
removeDuration = ANIM_DURATION_IN_MILLIS
|
|
||||||
moveDuration = 0
|
|
||||||
changeDuration = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -14,18 +14,15 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.matrix.android.api.session.signout
|
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||||
|
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
|
|
||||||
/**
|
class DefaultItemFactory {
|
||||||
* This interface defines a method to sign out. It's implemented at the session level.
|
|
||||||
*/
|
|
||||||
interface SignOutService {
|
|
||||||
|
|
||||||
/**
|
fun create(event: TimelineEvent): DefaultItem? {
|
||||||
* Sign out
|
val text = "${event.root.type} events are not yet handled"
|
||||||
*/
|
return DefaultItem(text = text)
|
||||||
fun signOut(callback: MatrixCallback<Unit>)
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||||
|
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import im.vector.matrix.android.api.permalinks.MatrixLinkify
|
||||||
|
import im.vector.riotredesign.R
|
||||||
|
|
||||||
|
class MessageTextItem(
|
||||||
|
val message: CharSequence? = null,
|
||||||
|
informationData: MessageInformationData
|
||||||
|
) : AbsMessageItem(informationData, R.layout.item_timeline_event_text_message) {
|
||||||
|
|
||||||
|
override val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
|
||||||
|
override val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
||||||
|
override val timeView by bind<TextView>(R.id.messageTimeView)
|
||||||
|
private val messageView by bind<TextView>(R.id.messageTextView)
|
||||||
|
|
||||||
|
override fun bind() {
|
||||||
|
super.bind()
|
||||||
|
messageView.text = message
|
||||||
|
MatrixLinkify.addLinkMovementMethod(messageView)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||||
|
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import im.vector.riotredesign.R
|
||||||
|
import im.vector.riotredesign.core.epoxy.KotlinModel
|
||||||
|
import im.vector.riotredesign.features.home.AvatarRenderer
|
||||||
|
|
||||||
|
class NoticeItem(private val noticeText: CharSequence? = null,
|
||||||
|
private val avatarUrl: String?,
|
||||||
|
private val memberName: CharSequence? = null)
|
||||||
|
: KotlinModel(R.layout.item_timeline_event_notice) {
|
||||||
|
|
||||||
|
private val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView)
|
||||||
|
private val noticeTextView by bind<TextView>(R.id.itemNoticeTextView)
|
||||||
|
|
||||||
|
override fun bind() {
|
||||||
|
noticeTextView.text = noticeText
|
||||||
|
AvatarRenderer.render(avatarUrl, memberName?.toString(), avatarImageView)
|
||||||
|
}
|
||||||
|
}
|
@ -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() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||||
|
|
||||||
|
import android.text.TextUtils
|
||||||
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
|
import im.vector.matrix.android.api.session.room.model.Membership
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||||
|
import im.vector.riotredesign.R
|
||||||
|
import im.vector.riotredesign.core.resources.StringProvider
|
||||||
|
|
||||||
|
|
||||||
|
//TODO : complete with call membership events
|
||||||
|
class RoomMemberItemFactory(private val stringProvider: StringProvider) {
|
||||||
|
|
||||||
|
fun create(event: TimelineEvent): NoticeItem? {
|
||||||
|
val roomMember = event.roomMember ?: return null
|
||||||
|
val noticeText = buildRoomMemberNotice(event) ?: return null
|
||||||
|
return NoticeItem(noticeText, roomMember.avatarUrl, roomMember.displayName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildRoomMemberNotice(event: TimelineEvent): String? {
|
||||||
|
val eventContent: RoomMember? = event.root.content.toModel()
|
||||||
|
val prevEventContent: RoomMember? = event.root.prevContent.toModel()
|
||||||
|
val isMembershipEvent = prevEventContent?.membership != eventContent?.membership
|
||||||
|
return if (isMembershipEvent) {
|
||||||
|
buildMembershipNotice(event, eventContent, prevEventContent)
|
||||||
|
} else {
|
||||||
|
buildProfileNotice(event, eventContent, prevEventContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildProfileNotice(event: TimelineEvent, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
|
||||||
|
val displayText = StringBuilder()
|
||||||
|
// Check display name has been changed
|
||||||
|
if (!TextUtils.equals(eventContent?.displayName, prevEventContent?.displayName)) {
|
||||||
|
val displayNameText = when {
|
||||||
|
prevEventContent?.displayName.isNullOrEmpty() -> stringProvider.getString(R.string.notice_display_name_set, event.root.sender, eventContent?.displayName)
|
||||||
|
eventContent?.displayName.isNullOrEmpty() -> stringProvider.getString(R.string.notice_display_name_removed, event.root.sender, prevEventContent?.displayName)
|
||||||
|
else -> stringProvider.getString(R.string.notice_display_name_changed_from, event.root.sender, prevEventContent?.displayName, eventContent?.displayName)
|
||||||
|
}
|
||||||
|
displayText.append(displayNameText)
|
||||||
|
}
|
||||||
|
// Check whether the avatar has been changed
|
||||||
|
if (!TextUtils.equals(eventContent?.avatarUrl, prevEventContent?.avatarUrl)) {
|
||||||
|
val displayAvatarText = if (displayText.isNotEmpty()) {
|
||||||
|
displayText.append(" ")
|
||||||
|
stringProvider.getString(R.string.notice_avatar_changed_too)
|
||||||
|
} else {
|
||||||
|
stringProvider.getString(R.string.notice_avatar_url_changed, event.roomMember?.displayName)
|
||||||
|
}
|
||||||
|
displayText.append(displayAvatarText)
|
||||||
|
}
|
||||||
|
return displayText.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildMembershipNotice(event: TimelineEvent, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
|
||||||
|
val senderDisplayName = event.roomMember?.displayName ?: return null
|
||||||
|
val targetDisplayName = eventContent?.displayName ?: event.root.sender
|
||||||
|
return when {
|
||||||
|
Membership.INVITE == eventContent?.membership -> {
|
||||||
|
// TODO get userId
|
||||||
|
val selfUserId: String = ""
|
||||||
|
when {
|
||||||
|
eventContent.thirdPartyInvite != null -> stringProvider.getString(R.string.notice_room_third_party_registered_invite, targetDisplayName, eventContent.thirdPartyInvite?.displayName)
|
||||||
|
TextUtils.equals(event.root.stateKey, selfUserId)
|
||||||
|
-> stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName)
|
||||||
|
event.root.stateKey.isNullOrEmpty() -> stringProvider.getString(R.string.notice_room_invite_no_invitee, senderDisplayName)
|
||||||
|
else -> stringProvider.getString(R.string.notice_room_invite, senderDisplayName, targetDisplayName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Membership.JOIN == eventContent?.membership -> stringProvider.getString(R.string.notice_room_join, senderDisplayName)
|
||||||
|
Membership.LEAVE == eventContent?.membership -> // 2 cases here: this member may have left voluntarily or they may have been "left" by someone else ie. kicked
|
||||||
|
return if (TextUtils.equals(event.root.sender, event.root.stateKey)) {
|
||||||
|
if (prevEventContent?.membership == Membership.INVITE) {
|
||||||
|
stringProvider.getString(R.string.notice_room_reject, senderDisplayName)
|
||||||
|
} else {
|
||||||
|
stringProvider.getString(R.string.notice_room_leave, senderDisplayName)
|
||||||
|
}
|
||||||
|
} else if (prevEventContent?.membership == Membership.INVITE) {
|
||||||
|
stringProvider.getString(R.string.notice_room_withdraw, senderDisplayName, targetDisplayName)
|
||||||
|
} else if (prevEventContent?.membership == Membership.JOIN) {
|
||||||
|
stringProvider.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName)
|
||||||
|
} else if (prevEventContent?.membership == Membership.BAN) {
|
||||||
|
stringProvider.getString(R.string.notice_room_unban, senderDisplayName, targetDisplayName)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
Membership.BAN == eventContent?.membership -> stringProvider.getString(R.string.notice_room_ban, senderDisplayName, targetDisplayName)
|
||||||
|
Membership.KNOCK == eventContent?.membership -> stringProvider.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||||
|
|
||||||
|
import android.text.TextUtils
|
||||||
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomNameContent
|
||||||
|
import im.vector.riotredesign.R
|
||||||
|
import im.vector.riotredesign.core.resources.StringProvider
|
||||||
|
|
||||||
|
class RoomNameItemFactory(private val stringProvider: StringProvider) {
|
||||||
|
|
||||||
|
fun create(event: TimelineEvent): NoticeItem? {
|
||||||
|
|
||||||
|
val content: RoomNameContent? = event.root.content.toModel()
|
||||||
|
val roomMember = event.roomMember
|
||||||
|
if (content == null || roomMember == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val text = if (!TextUtils.isEmpty(content.name)) {
|
||||||
|
stringProvider.getString(R.string.notice_room_name_changed, roomMember.displayName, content.name)
|
||||||
|
} else {
|
||||||
|
stringProvider.getString(R.string.notice_room_name_removed, roomMember.displayName)
|
||||||
|
}
|
||||||
|
return NoticeItem(text, roomMember.avatarUrl, roomMember.displayName)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
|
||||||
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
|
import im.vector.riotredesign.R
|
||||||
|
import im.vector.riotredesign.core.resources.StringProvider
|
||||||
|
|
||||||
|
class RoomTopicItemFactory(private val stringProvider: StringProvider) {
|
||||||
|
|
||||||
|
fun create(event: TimelineEvent): NoticeItem? {
|
||||||
|
|
||||||
|
val content: RoomTopicContent? = event.root.content.toModel()
|
||||||
|
val roomMember = event.roomMember
|
||||||
|
if (content == null || roomMember == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val text = if (content.topic.isNullOrEmpty()) {
|
||||||
|
stringProvider.getString(R.string.notice_room_topic_removed, roomMember.displayName)
|
||||||
|
} else {
|
||||||
|
stringProvider.getString(R.string.notice_room_topic_changed, roomMember.displayName, content.topic)
|
||||||
|
}
|
||||||
|
return NoticeItem(text, roomMember.avatarUrl, roomMember.displayName)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -14,29 +14,20 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.features.home.room.detail.timeline.helper
|
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||||
|
|
||||||
import im.vector.riotx.core.resources.LocaleProvider
|
import im.vector.riotredesign.core.resources.LocaleProvider
|
||||||
import org.threeten.bp.LocalDateTime
|
import org.threeten.bp.LocalDateTime
|
||||||
import org.threeten.bp.format.DateTimeFormatter
|
import org.threeten.bp.format.DateTimeFormatter
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
|
class TimelineDateFormatter(private val localeProvider: LocaleProvider) {
|
||||||
class TimelineDateFormatter @Inject constructor (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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -14,14 +14,11 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.features.home.room.detail.timeline.helper
|
package im.vector.riotredesign.features.home.room.detail.timeline.helper
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import im.vector.riotx.core.di.ScreenScope
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@ScreenScope
|
class TimelineMediaSizeProvider {
|
||||||
class TimelineMediaSizeProvider @Inject constructor() {
|
|
||||||
|
|
||||||
lateinit var recyclerView: RecyclerView
|
lateinit var recyclerView: RecyclerView
|
||||||
private var cachedSize: Pair<Int, Int>? = null
|
private var cachedSize: Pair<Int, Int>? = null
|
@ -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() }
|
||||||
|
}
|
||||||
|
}
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.features.home.room.list
|
package im.vector.riotredesign.features.home.room.list
|
||||||
|
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||||
|
|
||||||
@ -22,10 +22,8 @@ sealed class RoomListActions {
|
|||||||
|
|
||||||
data class SelectRoom(val roomSummary: RoomSummary) : RoomListActions()
|
data class SelectRoom(val roomSummary: RoomSummary) : RoomListActions()
|
||||||
|
|
||||||
data class ToggleCategory(val category: RoomCategory) : RoomListActions()
|
object RoomDisplayed : RoomListActions()
|
||||||
|
|
||||||
data class AcceptInvitation(val roomSummary: RoomSummary) : RoomListActions()
|
data class FilterRooms(val roomName: CharSequence? = null) : RoomListActions()
|
||||||
|
|
||||||
data class RejectInvitation(val roomSummary: RoomSummary) : RoomListActions()
|
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotredesign.features.home.room.list
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import com.airbnb.mvrx.Fail
|
||||||
|
import com.airbnb.mvrx.Incomplete
|
||||||
|
import com.airbnb.mvrx.Success
|
||||||
|
import com.airbnb.mvrx.activityViewModel
|
||||||
|
import im.vector.matrix.android.api.failure.Failure
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||||
|
import im.vector.riotredesign.R
|
||||||
|
import im.vector.riotredesign.core.extensions.setupAsSearch
|
||||||
|
import im.vector.riotredesign.core.platform.RiotFragment
|
||||||
|
import im.vector.riotredesign.core.platform.StateView
|
||||||
|
import im.vector.riotredesign.features.home.HomeNavigator
|
||||||
|
import kotlinx.android.synthetic.main.fragment_room_list.*
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
|
class RoomListFragment : RiotFragment(), RoomSummaryController.Callback {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance(): RoomListFragment {
|
||||||
|
return RoomListFragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val homeNavigator by inject<HomeNavigator>()
|
||||||
|
private val roomController by inject<RoomSummaryController>()
|
||||||
|
private val homeViewModel: RoomListViewModel by activityViewModel()
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
return inflater.inflate(R.layout.fragment_room_list, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
roomController.callback = this
|
||||||
|
stateView.contentView = epoxyRecyclerView
|
||||||
|
epoxyRecyclerView.setController(roomController)
|
||||||
|
setupFilterView()
|
||||||
|
homeViewModel.subscribe { renderState(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderState(state: RoomListViewState) {
|
||||||
|
when (state.asyncRooms) {
|
||||||
|
is Incomplete -> renderLoading()
|
||||||
|
is Success -> renderSuccess(state)
|
||||||
|
is Fail -> renderFailure(state.asyncRooms.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderSuccess(state: RoomListViewState) {
|
||||||
|
if (state.asyncRooms().isNullOrEmpty()) {
|
||||||
|
stateView.state = StateView.State.Empty(getString(R.string.room_list_empty))
|
||||||
|
} else {
|
||||||
|
stateView.state = StateView.State.Content
|
||||||
|
}
|
||||||
|
roomController.setData(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderLoading() {
|
||||||
|
stateView.state = StateView.State.Loading
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderFailure(error: Throwable) {
|
||||||
|
val message = when (error) {
|
||||||
|
is Failure.NetworkConnection -> getString(R.string.error_no_network)
|
||||||
|
else -> getString(R.string.error_common)
|
||||||
|
}
|
||||||
|
stateView.state = StateView.State.Error(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupFilterView() {
|
||||||
|
filterRoomView.setupAsSearch()
|
||||||
|
filterRoomView.addTextChangedListener(object : TextWatcher {
|
||||||
|
override fun afterTextChanged(s: Editable?) = Unit
|
||||||
|
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
|
||||||
|
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||||
|
homeViewModel.accept(RoomListActions.FilterRooms(s))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoomSummaryController.Callback **************************************************************
|
||||||
|
|
||||||
|
override fun onRoomSelected(room: RoomSummary) {
|
||||||
|
homeViewModel.accept(RoomListActions.SelectRoom(room))
|
||||||
|
homeNavigator.openRoomDetail(room.roomId, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,171 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotredesign.features.home.room.list
|
||||||
|
|
||||||
|
import arrow.core.Option
|
||||||
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
|
import com.jakewharton.rxrelay2.BehaviorRelay
|
||||||
|
import im.vector.matrix.android.api.Matrix
|
||||||
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||||
|
import im.vector.matrix.android.api.session.room.model.tag.RoomTag
|
||||||
|
import im.vector.matrix.rx.rx
|
||||||
|
import im.vector.riotredesign.core.platform.RiotViewModel
|
||||||
|
import im.vector.riotredesign.features.home.group.SelectedGroupHolder
|
||||||
|
import im.vector.riotredesign.features.home.room.VisibleRoomHolder
|
||||||
|
import io.reactivex.Observable
|
||||||
|
import io.reactivex.functions.Function3
|
||||||
|
import io.reactivex.rxkotlin.subscribeBy
|
||||||
|
import org.koin.android.ext.android.get
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
typealias RoomListFilterName = CharSequence
|
||||||
|
|
||||||
|
class RoomListViewModel(initialState: RoomListViewState,
|
||||||
|
private val session: Session,
|
||||||
|
private val selectedGroupHolder: SelectedGroupHolder,
|
||||||
|
private val visibleRoomHolder: VisibleRoomHolder,
|
||||||
|
private val roomSelectionRepository: RoomSelectionRepository,
|
||||||
|
private val roomSummaryComparator: RoomSummaryComparator)
|
||||||
|
: RiotViewModel<RoomListViewState>(initialState) {
|
||||||
|
|
||||||
|
companion object : MvRxViewModelFactory<RoomListViewModel, RoomListViewState> {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
override fun create(viewModelContext: ViewModelContext, state: RoomListViewState): RoomListViewModel? {
|
||||||
|
val currentSession = Matrix.getInstance().currentSession
|
||||||
|
val roomSelectionRepository = viewModelContext.activity.get<RoomSelectionRepository>()
|
||||||
|
val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupHolder>()
|
||||||
|
val visibleRoomHolder = viewModelContext.activity.get<VisibleRoomHolder>()
|
||||||
|
val roomSummaryComparator = viewModelContext.activity.get<RoomSummaryComparator>()
|
||||||
|
return RoomListViewModel(state, currentSession, selectedGroupHolder, visibleRoomHolder, roomSelectionRepository, roomSummaryComparator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private val roomListFilter = BehaviorRelay.createDefault<Option<RoomListFilterName>>(Option.empty())
|
||||||
|
|
||||||
|
init {
|
||||||
|
observeRoomSummaries()
|
||||||
|
observeVisibleRoom()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun accept(action: RoomListActions) {
|
||||||
|
when (action) {
|
||||||
|
is RoomListActions.SelectRoom -> handleSelectRoom(action)
|
||||||
|
is RoomListActions.FilterRooms -> handleFilterRooms(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PRIVATE METHODS *****************************************************************************
|
||||||
|
|
||||||
|
private fun handleSelectRoom(action: RoomListActions.SelectRoom) = withState { state ->
|
||||||
|
if (state.selectedRoomId != action.roomSummary.roomId) {
|
||||||
|
roomSelectionRepository.saveLastSelectedRoom(action.roomSummary.roomId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleFilterRooms(action: RoomListActions.FilterRooms) {
|
||||||
|
val optionalFilter = Option.fromNullable(action.roomName)
|
||||||
|
roomListFilter.accept(optionalFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeVisibleRoom() {
|
||||||
|
visibleRoomHolder.visibleRoom()
|
||||||
|
.subscribeBy {
|
||||||
|
setState { copy(selectedRoomId = it) }
|
||||||
|
}
|
||||||
|
.disposeOnClear()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeRoomSummaries() {
|
||||||
|
Observable.combineLatest<List<RoomSummary>, Option<GroupSummary>, Option<RoomListFilterName>, RoomSummaries>(
|
||||||
|
session.rx().liveRoomSummaries().throttleLast(300, TimeUnit.MILLISECONDS),
|
||||||
|
selectedGroupHolder.selectedGroup(),
|
||||||
|
roomListFilter.throttleLast(300, TimeUnit.MILLISECONDS),
|
||||||
|
Function3 { rooms, selectedGroupOption, filterRoomOption ->
|
||||||
|
val filteredRooms = filterRooms(rooms, filterRoomOption)
|
||||||
|
val selectedGroup = selectedGroupOption.orNull()
|
||||||
|
val filteredDirectRooms = filteredRooms
|
||||||
|
.filter { it.isDirect }
|
||||||
|
.filter {
|
||||||
|
if (selectedGroup == null) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
it.otherMemberIds
|
||||||
|
.intersect(selectedGroup.userIds)
|
||||||
|
.isNotEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val filteredGroupRooms = filteredRooms
|
||||||
|
.filter { !it.isDirect }
|
||||||
|
.filter {
|
||||||
|
selectedGroup?.roomIds?.contains(it.roomId) ?: true
|
||||||
|
}
|
||||||
|
buildRoomSummaries(filteredDirectRooms + filteredGroupRooms)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.execute { async ->
|
||||||
|
copy(
|
||||||
|
asyncRooms = async
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filterRooms(rooms: List<RoomSummary>, filterRoomOption: Option<RoomListFilterName>): List<RoomSummary> {
|
||||||
|
val filterRoom = filterRoomOption.orNull()
|
||||||
|
return rooms.filter {
|
||||||
|
if (filterRoom.isNullOrBlank()) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
it.displayName.contains(other = filterRoom, ignoreCase = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildRoomSummaries(rooms: List<RoomSummary>): RoomSummaries {
|
||||||
|
val favourites = ArrayList<RoomSummary>()
|
||||||
|
val directChats = ArrayList<RoomSummary>()
|
||||||
|
val groupRooms = ArrayList<RoomSummary>()
|
||||||
|
val lowPriorities = ArrayList<RoomSummary>()
|
||||||
|
val serverNotices = ArrayList<RoomSummary>()
|
||||||
|
|
||||||
|
for (room in rooms) {
|
||||||
|
val tags = room.tags.map { it.name }
|
||||||
|
when {
|
||||||
|
tags.contains(RoomTag.ROOM_TAG_SERVER_NOTICE) -> serverNotices.add(room)
|
||||||
|
tags.contains(RoomTag.ROOM_TAG_FAVOURITE) -> favourites.add(room)
|
||||||
|
tags.contains(RoomTag.ROOM_TAG_LOW_PRIORITY) -> lowPriorities.add(room)
|
||||||
|
room.isDirect -> directChats.add(room)
|
||||||
|
else -> groupRooms.add(room)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return RoomSummaries(
|
||||||
|
favourites = favourites.sortedWith(roomSummaryComparator),
|
||||||
|
directRooms = directChats.sortedWith(roomSummaryComparator),
|
||||||
|
groupRooms = groupRooms.sortedWith(roomSummaryComparator),
|
||||||
|
lowPriorities = lowPriorities.sortedWith(roomSummaryComparator),
|
||||||
|
serverNotices = serverNotices.sortedWith(roomSummaryComparator)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -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())
|
||||||
|
}
|
@ -14,24 +14,23 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.matrix.android.api.session.content
|
package im.vector.riotredesign.features.home.room.list
|
||||||
|
|
||||||
interface ContentUploadStateTracker {
|
import android.content.SharedPreferences
|
||||||
|
|
||||||
fun track(key: String, updateListener: UpdateListener)
|
private const val SHARED_PREFS_SELECTED_ROOM_KEY = "SHARED_PREFS_SELECTED_ROOM_KEY"
|
||||||
|
|
||||||
fun untrack(key: String, updateListener: UpdateListener)
|
class RoomSelectionRepository(private val sharedPreferences: SharedPreferences) {
|
||||||
|
|
||||||
interface UpdateListener {
|
fun lastSelectedRoom(): String? {
|
||||||
fun onUpdate(state: State)
|
return sharedPreferences.getString(SHARED_PREFS_SELECTED_ROOM_KEY, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class State {
|
fun saveLastSelectedRoom(roomId: String) {
|
||||||
object Idle : State()
|
sharedPreferences.edit()
|
||||||
data class ProgressData(val current: Long, val total: Long) : State()
|
.putString(SHARED_PREFS_SELECTED_ROOM_KEY, roomId)
|
||||||
object Success : State()
|
.apply()
|
||||||
object Failure : State()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotredesign.features.home.room.list
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||||
|
|
||||||
|
class RoomSummaryComparator
|
||||||
|
: Comparator<RoomSummary> {
|
||||||
|
|
||||||
|
override fun compare(leftRoomSummary: RoomSummary?, rightRoomSummary: RoomSummary?): Int {
|
||||||
|
val retValue: Int
|
||||||
|
var leftHighlightCount = 0
|
||||||
|
var rightHighlightCount = 0
|
||||||
|
var leftNotificationCount = 0
|
||||||
|
var rightNotificationCount = 0
|
||||||
|
var rightTimestamp = 0L
|
||||||
|
var leftTimestamp = 0L
|
||||||
|
|
||||||
|
if (null != leftRoomSummary) {
|
||||||
|
leftHighlightCount = leftRoomSummary.highlightCount
|
||||||
|
leftNotificationCount = leftRoomSummary.notificationCount
|
||||||
|
leftTimestamp = leftRoomSummary.lastMessage?.originServerTs ?: 0
|
||||||
|
}
|
||||||
|
if (null != rightRoomSummary) {
|
||||||
|
rightHighlightCount = rightRoomSummary.highlightCount
|
||||||
|
rightNotificationCount = rightRoomSummary.notificationCount
|
||||||
|
rightTimestamp = rightRoomSummary.lastMessage?.originServerTs ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (leftRoomSummary?.lastMessage == null) {
|
||||||
|
retValue = 1
|
||||||
|
} else if (rightRoomSummary?.lastMessage == null) {
|
||||||
|
retValue = -1
|
||||||
|
} else if (rightHighlightCount > 0 && leftHighlightCount == 0) {
|
||||||
|
retValue = 1
|
||||||
|
} else if (rightHighlightCount == 0 && leftHighlightCount > 0) {
|
||||||
|
retValue = -1
|
||||||
|
} else if (rightNotificationCount > 0 && leftNotificationCount == 0) {
|
||||||
|
retValue = 1
|
||||||
|
} else if (rightNotificationCount == 0 && leftNotificationCount > 0) {
|
||||||
|
retValue = -1
|
||||||
|
} else {
|
||||||
|
val deltaTimestamp = rightTimestamp - leftTimestamp
|
||||||
|
if (deltaTimestamp > 0) {
|
||||||
|
retValue = 1
|
||||||
|
} else if (deltaTimestamp < 0) {
|
||||||
|
retValue = -1
|
||||||
|
} else {
|
||||||
|
retValue = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return retValue
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.features.home.room.list
|
package im.vector.riotredesign.features.home.room.list
|
||||||
|
|
||||||
object RoomSummaryFormatter {
|
object RoomSummaryFormatter {
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ object RoomSummaryFormatter {
|
|||||||
*/
|
*/
|
||||||
fun formatUnreadMessagesCounter(count: Int): String {
|
fun formatUnreadMessagesCounter(count: Int): String {
|
||||||
return if (count > 999) {
|
return if (count > 999) {
|
||||||
"${count / 1000}.${count % 1000 / 100}k"
|
"${count / 1000}.${count % 1000 / 100}K"
|
||||||
} else {
|
} else {
|
||||||
count.toString()
|
count.toString()
|
||||||
}
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -13,14 +13,13 @@
|
|||||||
* 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.riotx.features.home.room.list
|
package im.vector.riotredesign.features.home.room.list
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.appcompat.widget.AppCompatTextView
|
import androidx.appcompat.widget.AppCompatTextView
|
||||||
import im.vector.riotx.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotx.features.themes.ThemeUtils
|
|
||||||
|
|
||||||
class UnreadCounterBadgeView : AppCompatTextView {
|
class UnreadCounterBadgeView : AppCompatTextView {
|
||||||
|
|
||||||
@ -30,24 +29,24 @@ class UnreadCounterBadgeView : AppCompatTextView {
|
|||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||||
|
|
||||||
fun render(state: State) {
|
fun render(count: Int, highlighted: Boolean) {
|
||||||
if (state.count == 0) {
|
if (count == 0) {
|
||||||
visibility = View.INVISIBLE
|
visibility = View.INVISIBLE
|
||||||
} else {
|
} else {
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
val bgRes = if (state.highlighted) {
|
val bgRes = if (highlighted) {
|
||||||
R.drawable.bg_unread_highlight
|
R.drawable.bg_unread_highlight
|
||||||
} else {
|
} else {
|
||||||
ThemeUtils.getResourceId(context, R.drawable.bg_unread_notification_light)
|
R.drawable.bg_unread_notification
|
||||||
}
|
}
|
||||||
setBackgroundResource(bgRes)
|
setBackgroundResource(bgRes)
|
||||||
text = RoomSummaryFormatter.formatUnreadMessagesCounter(state.count)
|
text = RoomSummaryFormatter.formatUnreadMessagesCounter(count)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class State(
|
enum class Status {
|
||||||
val count: Int,
|
NOTIFICATION,
|
||||||
val highlighted: Boolean
|
HIGHLIGHT
|
||||||
)
|
}
|
||||||
|
|
||||||
}
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user