forked from GitHub-Mirror/riotX-android
Merge branch 'develop' into feature/crypto
This commit is contained in:
@ -71,10 +71,21 @@ android {
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
debug {
|
||||
keyAlias 'androiddebugkey'
|
||||
keyPassword 'android'
|
||||
storeFile file('./signature/debug.keystore')
|
||||
storePassword 'android'
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
resValue "bool", "debug_mode", "true"
|
||||
buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false"
|
||||
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
|
||||
release {
|
||||
@ -121,7 +132,7 @@ dependencies {
|
||||
def epoxy_version = "3.3.0"
|
||||
def arrow_version = "0.8.2"
|
||||
def coroutines_version = "1.0.1"
|
||||
def markwon_version = '3.0.0-SNAPSHOT'
|
||||
def markwon_version = '3.0.0'
|
||||
def big_image_viewer_version = '1.5.6'
|
||||
def glide_version = '4.9.0'
|
||||
def moshi_version = '1.8.0'
|
||||
@ -156,7 +167,7 @@ dependencies {
|
||||
|
||||
implementation("com.airbnb.android:epoxy:$epoxy_version")
|
||||
kapt "com.airbnb.android:epoxy-processor:$epoxy_version"
|
||||
implementation 'com.airbnb.android:mvrx:0.7.0'
|
||||
implementation 'com.airbnb.android:mvrx:1.0.1'
|
||||
|
||||
// Work
|
||||
implementation "android.arch.work:work-runtime-ktx:1.0.0"
|
||||
@ -173,6 +184,7 @@ dependencies {
|
||||
implementation 'me.gujun.android:span:1.7'
|
||||
implementation "ru.noties.markwon:core:$markwon_version"
|
||||
implementation "ru.noties.markwon:html:$markwon_version"
|
||||
implementation 'me.saket:better-link-movement-method:2.2.0'
|
||||
|
||||
// Passphrase strength helper
|
||||
implementation 'com.nulab-inc:zxcvbn:1.2.5'
|
||||
|
52
vector/sampledata/matrix.json
Normal file
52
vector/sampledata/matrix.json
Normal file
@ -0,0 +1,52 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"displayName": "Long display name useful to test layout with a long display name",
|
||||
"mxid": "@longmatrixidbecausesometimesuserschooselongmxid:matrix.org",
|
||||
"message": "William Shakespeare (bapt. 26 April 1564 – 23 April 1616) was an English poet, playwright and actor, widely regarded as the greatest writer in the English language and the world's greatest dramatist. He is often called England's national poet and the \"Bard of Avon\". His extant works, including collaborations, consist of approximately 39 plays, 154 sonnets, two long narrative poems, and a few other verses, some of uncertain authorship. His plays have been translated into every major living language and are performed more often than those of any other playwright.\n\nShakespeare was born and raised in Stratford-upon-Avon, Warwickshire. At the age of 18, he married Anne Hathaway, with whom he had three children: Susanna and twins Hamnet and Judith. Sometime between 1585 and 1592, he began a successful career in London as an actor, writer, and part-owner of a playing company called the Lord Chamberlain's Men, later known as the King's Men. At age 49 (around 1613), he appears to have retired to Stratford, where he died three years later. Few records of Shakespeare's private life survive; this has stimulated considerable speculation about such matters as his physical appearance, his sexuality, his religious beliefs, and whether the works attributed to him were written by others. Such theories are often criticised for failing to adequately note that few records survive of most commoners of the period.\n\nShakespeare produced most of his known works between 1589 and 1613. His early plays were primarily comedies and histories and are regarded as some of the best work produced in these genres. Until about 1608, he wrote mainly tragedies, among them Hamlet, Othello, King Lear, and Macbeth, all considered to be among the finest works in the English language. In the last phase of his life, he wrote tragicomedies (also known as romances) and collaborated with other playwrights.\n\nMany of Shakespeare's plays were published in editions of varying quality and accuracy in his lifetime. However, in 1623, two fellow actors and friends of Shakespeare's, John Heminges and Henry Condell, published a more definitive text known as the First Folio, a posthumous collected edition of Shakespeare's dramatic works that included all but two of his plays. The volume was prefaced with a poem by Ben Jonson, in which Jonson presciently hails Shakespeare in a now-famous quote as \"not of an age, but for all time\".\n\nThroughout the 20th and 21st centuries, Shakespeare's works have been continually adapted and rediscovered by new movements in scholarship and performance. His plays remain popular and are studied, performed, and reinterpreted through various cultural and political contexts around the world.",
|
||||
"roomName": "Matrix HQ",
|
||||
"roomAlias": "#matrix:matrix.org",
|
||||
"roomTopic": "Welcome to Matrix HQ! Here is the rest of the room topic…"
|
||||
},
|
||||
{
|
||||
"displayName": "benoit",
|
||||
"mxid": "@benoit:matrix.org",
|
||||
"message": "Hello!",
|
||||
"roomName": "Room name very loooooooong with some details",
|
||||
"roomAlias": "#matrix:matrix.org",
|
||||
"roomTopic": "Room topic very loooooooong with some details"
|
||||
},
|
||||
{
|
||||
"displayName": "ganfra",
|
||||
"mxid": "@ganfra:matrix.org",
|
||||
"message": "How are you?",
|
||||
"roomName": "Room name very loooooooong with some details",
|
||||
"roomAlias": "#matrix:matrix.org",
|
||||
"roomTopic": "Room topic very loooooooong with some details"
|
||||
},
|
||||
{
|
||||
"displayName": "Manu",
|
||||
"mxid": "@manu:matrix.org",
|
||||
"message": "Great weather today!",
|
||||
"roomName": "Room name very loooooooong with some details",
|
||||
"roomAlias": "#matrix:matrix.org",
|
||||
"roomTopic": "Room topic very loooooooong with some details"
|
||||
},
|
||||
{
|
||||
"displayName": "Giom",
|
||||
"mxid": "@giom:matrix.org",
|
||||
"message": "Let's do a picnic",
|
||||
"roomName": "Room name very loooooooong with some details",
|
||||
"roomAlias": "#matrix:matrix.org",
|
||||
"roomTopic": "Room topic very loooooooong with some details"
|
||||
},
|
||||
{
|
||||
"displayName": "Nad",
|
||||
"mxid": "@nadonomy:matrix.org",
|
||||
"message": "Yes, great idea",
|
||||
"roomName": "Room name very loooooooong with some details",
|
||||
"roomAlias": "#matrix:matrix.org",
|
||||
"roomTopic": "Room topic very loooooooong with some details"
|
||||
}
|
||||
]
|
||||
}
|
12
vector/signature/README.md
Normal file
12
vector/signature/README.md
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
## Debug signature
|
||||
|
||||
Buildkite CI tool uses docker images to build the Android application, and it looks like the debug signature is changed at each build.
|
||||
|
||||
So it's not possible for user to upgrade the application with the last build from buildkite without uninstalling the application.
|
||||
|
||||
This folder contains a debug signature, and the debug build will uses this signature to build the APK.
|
||||
|
||||
The validity of the signature is 30 years. So it has to be replaced before June 2049 :).
|
||||
|
||||
More info about the debug signature: https://developer.android.com/studio/publish/app-signing#debug-mode
|
BIN
vector/signature/debug.keystore
Normal file
BIN
vector/signature/debug.keystore
Normal file
Binary file not shown.
14
vector/src/debug/AndroidManifest.xml
Normal file
14
vector/src/debug/AndroidManifest.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<activity android:name="im.vector.riotredesign.features.debug.TestLinkifyActivity" />
|
||||
<activity
|
||||
android:name="im.vector.riotredesign.features.debug.DebugMaterialThemeLightActivity"
|
||||
android:theme="@style/VectorMaterialThemeDebugLight" />
|
||||
<activity
|
||||
android:name="im.vector.riotredesign.features.debug.DebugMaterialThemeDarkActivity"
|
||||
android:theme="@style/VectorMaterialThemeDebugDark" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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.debug
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.utils.toast
|
||||
import kotlinx.android.synthetic.debug.activity_test_material_theme.*
|
||||
|
||||
// Rendering is not the same with VectorBaseActivity
|
||||
abstract class DebugMaterialThemeActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_test_material_theme)
|
||||
|
||||
debugShowSnackbar.setOnClickListener {
|
||||
Snackbar.make(debugMaterialCoordinator, "Snackbar!", Snackbar.LENGTH_SHORT)
|
||||
.setAction("Action") { }
|
||||
.show()
|
||||
}
|
||||
|
||||
debugShowToast.setOnClickListener {
|
||||
toast("Toast")
|
||||
}
|
||||
|
||||
debugShowDialog.setOnClickListener {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage("Dialog content")
|
||||
.setIcon(R.drawable.ic_settings_x)
|
||||
.setPositiveButton("Positive", null)
|
||||
.setNegativeButton("Negative", null)
|
||||
.setNeutralButton("Neutral", null)
|
||||
.show()
|
||||
}
|
||||
|
||||
debugShowBottomSheet.setOnClickListener {
|
||||
BottomSheetDialogFragment().show(supportFragmentManager, "TAG")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.vector_home, menu)
|
||||
return true
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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.debug
|
||||
|
||||
class DebugMaterialThemeDarkActivity : DebugMaterialThemeActivity()
|
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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.debug
|
||||
|
||||
class DebugMaterialThemeLightActivity : DebugMaterialThemeActivity()
|
@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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.debug
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.Person
|
||||
import butterknife.OnClick
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.platform.VectorBaseActivity
|
||||
|
||||
|
||||
class DebugMenuActivity : VectorBaseActivity() {
|
||||
|
||||
override fun getLayoutRes() = R.layout.activity_debug_menu
|
||||
|
||||
@OnClick(R.id.debug_test_text_view_link)
|
||||
fun testTextViewLink() {
|
||||
startActivity(Intent(this, TestLinkifyActivity::class.java))
|
||||
}
|
||||
|
||||
@OnClick(R.id.debug_test_notification)
|
||||
fun testNotification() {
|
||||
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
// Create channel first
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel =
|
||||
NotificationChannel(
|
||||
"CHAN",
|
||||
"Channel name",
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
)
|
||||
|
||||
channel.description = "Channel description"
|
||||
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).createNotificationChannel(channel)
|
||||
|
||||
val channel2 =
|
||||
NotificationChannel(
|
||||
"CHAN2",
|
||||
"Channel name 2",
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
)
|
||||
|
||||
channel2.description = "Channel description 2"
|
||||
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).createNotificationChannel(channel2)
|
||||
}
|
||||
|
||||
|
||||
val builder = NotificationCompat.Builder(this, "CHAN")
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setContentTitle("Title")
|
||||
.setContentText("Content")
|
||||
// No effect because it's a group summary notif
|
||||
.setNumber(33)
|
||||
.setSmallIcon(R.drawable.logo_transparent)
|
||||
// This provocate the badge issue: no badge for group notification
|
||||
.setGroup("GroupKey")
|
||||
.setGroupSummary(true)
|
||||
|
||||
val messagingStyle1 = NotificationCompat.MessagingStyle(
|
||||
Person.Builder()
|
||||
.setName("User name")
|
||||
.build()
|
||||
)
|
||||
.addMessage("Message 1 - 1", System.currentTimeMillis(), Person.Builder().setName("user 1-1").build())
|
||||
.addMessage("Message 1 - 2", System.currentTimeMillis(), Person.Builder().setName("user 1-2").build())
|
||||
|
||||
val messagingStyle2 = NotificationCompat.MessagingStyle(
|
||||
Person.Builder()
|
||||
.setName("User name 2")
|
||||
.build()
|
||||
)
|
||||
.addMessage("Message 2 - 1", System.currentTimeMillis(), Person.Builder().setName("user 1-1").build())
|
||||
.addMessage("Message 2 - 2", System.currentTimeMillis(), Person.Builder().setName("user 1-2").build())
|
||||
|
||||
|
||||
notificationManager.notify(10, builder.build())
|
||||
|
||||
notificationManager.notify(
|
||||
11,
|
||||
NotificationCompat.Builder(this, "CHAN")
|
||||
.setChannelId("CHAN")
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setContentTitle("Title 1")
|
||||
.setContentText("Content 1")
|
||||
// For shortcut on long press on launcher icon
|
||||
.setBadgeIconType(NotificationCompat.BADGE_ICON_NONE)
|
||||
.setStyle(messagingStyle1)
|
||||
.setSmallIcon(R.drawable.logo_transparent)
|
||||
.setGroup("GroupKey")
|
||||
.build()
|
||||
)
|
||||
|
||||
notificationManager.notify(
|
||||
12,
|
||||
NotificationCompat.Builder(this, "CHAN2")
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setContentTitle("Title 2")
|
||||
.setContentText("Content 2")
|
||||
.setStyle(messagingStyle2)
|
||||
.setSmallIcon(R.drawable.logo_transparent)
|
||||
.setGroup("GroupKey")
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
@OnClick(R.id.debug_test_material_theme_light)
|
||||
fun testMaterialThemeLight() {
|
||||
startActivity(Intent(this, DebugMaterialThemeLightActivity::class.java))
|
||||
}
|
||||
|
||||
@OnClick(R.id.debug_test_material_theme_dark)
|
||||
fun testMaterialThemeDark() {
|
||||
startActivity(Intent(this, DebugMaterialThemeDarkActivity::class.java))
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,133 @@
|
||||
/*
|
||||
* 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.debug
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import butterknife.BindView
|
||||
import butterknife.ButterKnife
|
||||
import im.vector.riotredesign.R
|
||||
|
||||
|
||||
class TestLinkifyActivity : AppCompatActivity() {
|
||||
|
||||
@BindView(R.id.test_linkify_content_view)
|
||||
lateinit var scrollContent: LinearLayout
|
||||
|
||||
@BindView(R.id.test_linkify_coordinator)
|
||||
lateinit var coordinatorLayout: CoordinatorLayout
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_test_linkify)
|
||||
ButterKnife.bind(this)
|
||||
|
||||
scrollContent.removeAllViews()
|
||||
|
||||
listOf(
|
||||
"https://www.html5rocks.com/en/tutorials/webrtc/basics/ |",
|
||||
"https://www.html5rocks.com/en/tutorials/webrtc/basics/",
|
||||
"mailto mailto:test@toto.com test@toto.com",
|
||||
"Here is the link.www.test.com/foo/?23=35 you got it?",
|
||||
"www.lemonde.fr",
|
||||
" /www.lemonde.fr",
|
||||
"://www.lemonde.fr",
|
||||
"file:///dev/null ",
|
||||
" ansible/xoxys.matrix#2c0b65eb",
|
||||
"foo.ansible/xoxys.matrix#2c0b65eb",
|
||||
"foo.ansible.fpo/xoxys.matrix#2c0b65eb",
|
||||
"https://foo.ansible.fpo/xoxys.matrix#2c0b65eb",
|
||||
"@vf:matrix.org",
|
||||
"+44 207 123 1234",
|
||||
"+33141437940",
|
||||
"1234",
|
||||
"3456.34,089",
|
||||
"ksks9808",
|
||||
"For example: geo:48.85828,2.29449?z=16 should be clickable",
|
||||
"geo:37.786971,-122.399677;u=35",
|
||||
"37.786971,-122.399677;u=35",
|
||||
"48.107864,-1.712153",
|
||||
"synchrone peut tenir la route la",
|
||||
"that.is.some.sexy.link",
|
||||
"test overlap 48.107864,0673728392 geo + pn?",
|
||||
"test overlap 0673728392,48.107864 geo + pn?",
|
||||
"If I add a link in brackets like (help for Riot: https://about.riot.im/help), the link is usable on Riot for Desktop",
|
||||
"(help for Riot: https://about.riot.im/help)",
|
||||
"http://example.com/test(1).html",
|
||||
"http://example.com/test(1)",
|
||||
"https://about.riot.im/help)",
|
||||
"(http://example.com/test(1))",
|
||||
"http://example.com/test1)",
|
||||
"http://example.com/test1/, et ca",
|
||||
"www.example.com/, et ca",
|
||||
"foo.ansible.toplevel/xoxys.matrix#2c0b65eb",
|
||||
"foo.ansible.ninja/xoxys.matrix#2c0b65eb",
|
||||
"in brackets like (help for Riot: https://www.exemple/com/find(1)) , the link is usable ",
|
||||
"""
|
||||
In brackets like (help for Riot: https://about.riot.im/help) , the link is usable,
|
||||
But you can call +44 207 123 1234 and come to 37.786971,-122.399677;u=35 then
|
||||
see if this mail jhon@riot.im is active but this should not 12345
|
||||
""".trimIndent()
|
||||
)
|
||||
.forEach { textContent ->
|
||||
val item = LayoutInflater.from(this)
|
||||
.inflate(R.layout.item_test_linkify, scrollContent, false)
|
||||
|
||||
item.findViewById<TextView>(R.id.test_linkify_auto_text)
|
||||
?.apply {
|
||||
text = textContent
|
||||
/* TODO Use BetterLinkMovementMethod when the other PR is merged
|
||||
movementMethod = MatrixLinkMovementMethod(object : MockMessageAdapterActionListener() {
|
||||
override fun onURLClick(uri: Uri?) {
|
||||
Snackbar.make(coordinatorLayout, "URI Clicked: $uri", Snackbar.LENGTH_LONG)
|
||||
.setAction("open") {
|
||||
openUrlInExternalBrowser(this@TestLinkifyActivity, uri)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
})
|
||||
*/
|
||||
}
|
||||
|
||||
item.findViewById<TextView>(R.id.test_linkify_custom_text)
|
||||
?.apply {
|
||||
text = textContent
|
||||
/* TODO Use BetterLinkMovementMethod when the other PR is merged
|
||||
movementMethod = MatrixLinkMovementMethod(object : MockMessageAdapterActionListener() {
|
||||
override fun onURLClick(uri: Uri?) {
|
||||
Snackbar.make(coordinatorLayout, "URI Clicked: $uri", Snackbar.LENGTH_LONG)
|
||||
.setAction("open") {
|
||||
openUrlInExternalBrowser(this@TestLinkifyActivity, uri)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
})
|
||||
*/
|
||||
|
||||
// TODO Call VectorLinkify.addLinks(text)
|
||||
}
|
||||
|
||||
scrollContent.addView(item, ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<size
|
||||
android:width="8dp"
|
||||
android:height="8dp" />
|
||||
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="@color/grey_lynch" />
|
||||
</shape>
|
54
vector/src/debug/res/layout/activity_debug_menu.xml
Normal file
54
vector/src/debug/res/layout/activity_debug_menu.xml
Normal file
@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="im.vector.riotredesign.features.debug.DebugMenuActivity"
|
||||
tools:ignore="HardcodedText">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:divider="@drawable/linear_divider"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/layout_horizontal_margin"
|
||||
android:showDividers="middle">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/debug_test_text_view_link"
|
||||
style="@style/VectorButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Test linkification" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/debug_test_notification"
|
||||
style="@style/VectorButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Test Notification" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/debug_test_material_theme_light"
|
||||
style="@style/VectorButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Test Material theme Light" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/debug_test_material_theme_dark"
|
||||
style="@style/VectorButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Test Material theme Dark" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
31
vector/src/debug/res/layout/activity_test_linkify.xml
Normal file
31
vector/src/debug/res/layout/activity_test_linkify.xml
Normal file
@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/test_linkify_coordinator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/riot_secondary_text_color_status"
|
||||
tools:context="im.vector.riotredesign.features.debug.TestLinkifyActivity">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/test_linkify_content_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- Will be removed at runtime -->
|
||||
<include layout="@layout/item_test_linkify" />
|
||||
|
||||
<include layout="@layout/item_test_linkify" />
|
||||
|
||||
<include layout="@layout/item_test_linkify" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
168
vector/src/debug/res/layout/activity_test_material_theme.xml
Normal file
168
vector/src/debug/res/layout/activity_test_material_theme.xml
Normal file
@ -0,0 +1,168 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/debugMaterialCoordinator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:context=".features.debug.DebugMaterialThemeActivity"
|
||||
tools:ignore="HardcodedText">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:divider="@drawable/linear_divider"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
android:showDividers="middle">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:subtitle="Toolbar Subtitle"
|
||||
app:title="Toolbar Title" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="OutlinedBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="OutlinedBox.Dense">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="FilledBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.Dense"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="FilledBox.Dense">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
style="@style/Widget.MaterialComponents.Button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Material Button Classic"
|
||||
app:icon="@drawable/ic_settings_x" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Material Button OutlinedButton"
|
||||
app:icon="@drawable/ic_settings_x" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Material Button TextButton"
|
||||
app:icon="@drawable/ic_settings_x" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
style="@style/Widget.MaterialComponents.Button.UnelevatedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Material Button UnelevatedButton"
|
||||
app:icon="@drawable/ic_settings_x" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/debugShowSnackbar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Show Snackbar" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/debugShowToast"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Show Toast" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/debugShowDialog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Show Dialog" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/debugShowBottomSheet"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Show Bottom Sheet" />
|
||||
|
||||
<com.google.android.material.checkbox.MaterialCheckBox
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Material CheckBox" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Material Switch" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="48dp"
|
||||
android:text="TextView in MaterialCardView" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_settings_x" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
31
vector/src/debug/res/layout/demo_store_listing.xml
Normal file
31
vector/src/debug/res/layout/demo_store_listing.xml
Normal file
@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/store_title" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/store_whats_new" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/store_short_description" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/store_full_description" />
|
||||
|
||||
</LinearLayout>
|
28
vector/src/debug/res/layout/demo_theme_sample.xml
Normal file
28
vector/src/debug/res/layout/demo_theme_sample.xml
Normal file
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!--
|
||||
The aim of this file is to test the different themes of Riot
|
||||
-->
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:ignore="HardcodedText">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="20dp"
|
||||
android:background="?colorPrimaryDark"
|
||||
android:gravity="center_vertical"
|
||||
android:text="Status Bar" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:background="?colorPrimary"
|
||||
android:gravity="center_vertical"
|
||||
android:text="This is a simple text" />
|
||||
|
||||
</LinearLayout>
|
54
vector/src/debug/res/layout/demo_themes.xml
Normal file
54
vector/src/debug/res/layout/demo_themes.xml
Normal file
@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!--
|
||||
The aim of this file is to test the different themes of Riot
|
||||
Unfortunately, this does not work in the preview.
|
||||
-->
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:theme="@style/Theme.AppCompat.Light.DarkActionBar">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:theme="@style/AppTheme.Light">
|
||||
|
||||
<include layout="@layout/demo_theme_sample" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:theme="@style/AppTheme.Dark">
|
||||
|
||||
<include layout="@layout/demo_theme_sample" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:theme="@style/AppTheme.Black">
|
||||
|
||||
<include layout="@layout/demo_theme_sample" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:theme="@style/AppTheme.Status">
|
||||
|
||||
<include layout="@layout/demo_theme_sample" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
43
vector/src/debug/res/layout/item_test_linkify.xml
Normal file
43
vector/src/debug/res/layout/item_test_linkify.xml
Normal file
@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="4dp"
|
||||
tools:ignore="HardcodedText">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
style="@style/ListHeader"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="AutoLink" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/test_linkify_auto_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:autoLink="all"
|
||||
tools:text="www.example.org" />
|
||||
|
||||
<TextView
|
||||
style="@style/ListHeader"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Custom Link" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/test_linkify_custom_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
tools:text="www.example.org" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
59
vector/src/debug/res/values/styles.xml
Normal file
59
vector/src/debug/res/values/styles.xml
Normal file
@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="VectorDebug">
|
||||
<item name="android:visibility">visible</item>
|
||||
</style>
|
||||
|
||||
|
||||
<style name="VectorMaterialThemeDebugLight" parent="Theme.MaterialComponents.Light.NoActionBar">
|
||||
<item name="colorPrimary">#7F7F00</item>
|
||||
<item name="colorPrimaryVariant">#00FF00</item>
|
||||
<item name="colorOnPrimary">#0000FF</item>
|
||||
|
||||
<item name="colorSecondary">#FF00FF</item>
|
||||
<item name="colorSecondaryVariant">#00FFFF</item>
|
||||
<item name="colorOnSecondary">#FFF000</item>
|
||||
|
||||
<item name="colorError">#FF0000</item>
|
||||
<item name="colorOnError">#330033</item>
|
||||
|
||||
<item name="colorSurface">#003333</item>
|
||||
<item name="colorOnSurface">#777777</item>
|
||||
|
||||
<item name="android:colorBackground">#FF7777</item>
|
||||
<item name="colorOnBackground">#077700</item>
|
||||
|
||||
<!-- TODO is it still required? -->
|
||||
<item name="colorAccent">#03b381</item>
|
||||
|
||||
<item name="android:statusBarColor">#1188FF</item>
|
||||
<item name="android:navigationBarColor">#FF8811</item>
|
||||
</style>
|
||||
|
||||
<style name="VectorMaterialThemeDebugDark" parent="Theme.MaterialComponents.NoActionBar">
|
||||
<item name="colorPrimary">#7F7F00</item>
|
||||
<item name="colorPrimaryVariant">#00FF00</item>
|
||||
<item name="colorOnPrimary">#0000FF</item>
|
||||
|
||||
<item name="colorSecondary">#FF00FF</item>
|
||||
<item name="colorSecondaryVariant">#00FFFF</item>
|
||||
<item name="colorOnSecondary">#FFF000</item>
|
||||
|
||||
<item name="colorError">#FF0000</item>
|
||||
<item name="colorOnError">#330033</item>
|
||||
|
||||
<item name="colorSurface">#003333</item>
|
||||
<item name="colorOnSurface">#777777</item>
|
||||
|
||||
<item name="android:colorBackground">#FF7777</item>
|
||||
<item name="colorOnBackground">#077700</item>
|
||||
|
||||
<!-- TODO is it still required? -->
|
||||
<item name="colorAccent">#03b381</item>
|
||||
|
||||
<item name="android:statusBarColor">#1188FF</item>
|
||||
<item name="android:navigationBarColor">#FF8811</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
@ -55,6 +55,11 @@
|
||||
android:name="im.vector.riotredesign.features.reactions.EmojiReactionPickerActivity"
|
||||
android:label="@string/title_activity_emoji_reaction_picker" />
|
||||
|
||||
<activity android:name=".features.roomdirectory.RoomDirectoryActivity" />
|
||||
<activity android:name=".features.roomdirectory.roompreview.RoomPreviewActivity" />
|
||||
<activity android:name=".features.home.room.detail.RoomDetailActivity" />
|
||||
<activity android:name=".features.debug.DebugMenuActivity" />
|
||||
|
||||
<service
|
||||
android:name=".core.services.CallService"
|
||||
android:exported="false" />
|
||||
|
@ -18,6 +18,7 @@ package im.vector.riotredesign
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import androidx.multidex.MultiDex
|
||||
import com.airbnb.epoxy.EpoxyAsyncUtil
|
||||
import com.airbnb.epoxy.EpoxyController
|
||||
@ -27,11 +28,13 @@ import com.github.piasy.biv.loader.glide.GlideImageLoader
|
||||
import com.jakewharton.threetenabp.AndroidThreeTen
|
||||
import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.riotredesign.core.di.AppModule
|
||||
import im.vector.riotredesign.features.configuration.VectorConfiguration
|
||||
import im.vector.riotredesign.features.home.HomeModule
|
||||
import im.vector.riotredesign.features.lifecycle.VectorActivityLifecycleCallbacks
|
||||
import im.vector.riotredesign.features.rageshake.VectorFileLogger
|
||||
import im.vector.riotredesign.features.rageshake.VectorUncaughtExceptionHandler
|
||||
import org.koin.android.logger.AndroidLogger
|
||||
import im.vector.riotredesign.features.roomdirectory.RoomDirectoryModule
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.log.EmptyLogger
|
||||
import org.koin.standalone.StandAloneContext.startKoin
|
||||
import timber.log.Timber
|
||||
@ -40,10 +43,10 @@ import timber.log.Timber
|
||||
class VectorApplication : Application() {
|
||||
|
||||
lateinit var appContext: Context
|
||||
val vectorConfiguration: VectorConfiguration by inject()
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
appContext = this
|
||||
|
||||
VectorUncaughtExceptionHandler.activate(this)
|
||||
@ -62,13 +65,11 @@ class VectorApplication : Application() {
|
||||
EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
|
||||
val appModule = AppModule(applicationContext).definition
|
||||
val homeModule = HomeModule().definition
|
||||
startKoin(
|
||||
list = listOf(appModule, homeModule),
|
||||
logger = if (BuildConfig.DEBUG) AndroidLogger() else EmptyLogger())
|
||||
|
||||
val roomDirectoryModule = RoomDirectoryModule().definition
|
||||
startKoin(listOf(appModule, homeModule, roomDirectoryModule), logger = EmptyLogger())
|
||||
Matrix.getInstance().setApplicationFlavor(BuildConfig.FLAVOR_DESCRIPTION)
|
||||
|
||||
registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks())
|
||||
vectorConfiguration.initConfiguration()
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
@ -76,4 +77,9 @@ class VectorApplication : Application() {
|
||||
MultiDex.install(this)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration?) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
vectorConfiguration.onConfigurationChanged(newConfig)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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.animations
|
||||
|
||||
const val ANIMATION_DURATION_SHORT = 200L
|
@ -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.core.animations
|
||||
|
||||
import android.animation.Animator
|
||||
|
||||
open class SimpleAnimatorListener : Animator.AnimatorListener {
|
||||
override fun onAnimationRepeat(animation: Animator?) {
|
||||
// No op
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
// No op
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(animation: Animator?) {
|
||||
// No op
|
||||
}
|
||||
|
||||
override fun onAnimationStart(animation: Animator?) {
|
||||
// No op
|
||||
}
|
||||
}
|
@ -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.core.animations
|
||||
|
||||
import androidx.transition.Transition
|
||||
|
||||
open class SimpleTransitionListener : Transition.TransitionListener {
|
||||
override fun onTransitionEnd(transition: Transition) {
|
||||
// No op
|
||||
}
|
||||
|
||||
override fun onTransitionResume(transition: Transition) {
|
||||
// No op
|
||||
}
|
||||
|
||||
override fun onTransitionPause(transition: Transition) {
|
||||
// No op
|
||||
}
|
||||
|
||||
override fun onTransitionCancel(transition: Transition) {
|
||||
// No op
|
||||
}
|
||||
|
||||
override fun onTransitionStart(transition: Transition) {
|
||||
// No op
|
||||
}
|
||||
}
|
@ -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.core.animations
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.transition.ChangeBounds
|
||||
import androidx.transition.ChangeTransform
|
||||
import androidx.transition.Fade
|
||||
import androidx.transition.TransitionSet
|
||||
|
||||
class VectorFullTransitionSet : TransitionSet {
|
||||
|
||||
constructor() {
|
||||
init()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
init()
|
||||
}
|
||||
|
||||
private fun init() {
|
||||
ordering = ORDERING_TOGETHER
|
||||
addTransition(Fade(Fade.OUT))
|
||||
.addTransition(ChangeBounds())
|
||||
.addTransition(ChangeTransform())
|
||||
.addTransition(Fade(Fade.IN))
|
||||
}
|
||||
|
||||
}
|
@ -18,15 +18,20 @@ package im.vector.riotredesign.core.di
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Context.MODE_PRIVATE
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.riotredesign.core.resources.ColorProvider
|
||||
import im.vector.riotredesign.core.error.ErrorFormatter
|
||||
import im.vector.riotredesign.core.resources.LocaleProvider
|
||||
import im.vector.riotredesign.core.resources.StringArrayProvider
|
||||
import im.vector.riotredesign.core.resources.StringProvider
|
||||
import im.vector.riotredesign.features.configuration.VectorConfiguration
|
||||
import im.vector.riotredesign.features.crypto.verification.IncomingVerificationRequestHandler
|
||||
import im.vector.riotredesign.features.home.HomeRoomListObservableStore
|
||||
import im.vector.riotredesign.features.home.group.SelectedGroupStore
|
||||
import im.vector.riotredesign.features.home.room.VisibleRoomStore
|
||||
import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository
|
||||
import im.vector.riotredesign.features.home.room.list.RoomSummaryComparator
|
||||
import im.vector.riotredesign.features.home.room.list.AlphabeticalRoomComparator
|
||||
import im.vector.riotredesign.features.home.room.list.ChronologicalRoomComparator
|
||||
import im.vector.riotredesign.features.navigation.DefaultNavigator
|
||||
import im.vector.riotredesign.features.navigation.Navigator
|
||||
import im.vector.riotredesign.features.notifications.NotificationDrawerManager
|
||||
import org.koin.dsl.module.module
|
||||
|
||||
@ -34,6 +39,10 @@ class AppModule(private val context: Context) {
|
||||
|
||||
val definition = module {
|
||||
|
||||
single {
|
||||
VectorConfiguration(context)
|
||||
}
|
||||
|
||||
single {
|
||||
LocaleProvider(context.resources)
|
||||
}
|
||||
@ -43,27 +52,31 @@ class AppModule(private val context: Context) {
|
||||
}
|
||||
|
||||
single {
|
||||
ColorProvider(context)
|
||||
StringArrayProvider(context.resources)
|
||||
}
|
||||
|
||||
single {
|
||||
context.getSharedPreferences("im.vector.riot", MODE_PRIVATE)
|
||||
}
|
||||
|
||||
single {
|
||||
RoomSelectionRepository(get())
|
||||
}
|
||||
|
||||
single {
|
||||
SelectedGroupStore()
|
||||
}
|
||||
|
||||
single {
|
||||
VisibleRoomStore()
|
||||
HomeRoomListObservableStore()
|
||||
}
|
||||
|
||||
single {
|
||||
RoomSummaryComparator()
|
||||
ChronologicalRoomComparator()
|
||||
}
|
||||
|
||||
single {
|
||||
AlphabeticalRoomComparator()
|
||||
}
|
||||
|
||||
single {
|
||||
ErrorFormatter(get())
|
||||
}
|
||||
|
||||
single {
|
||||
@ -78,6 +91,8 @@ class AppModule(private val context: Context) {
|
||||
IncomingVerificationRequestHandler(context, get())
|
||||
}
|
||||
|
||||
|
||||
factory { (fragment: Fragment) ->
|
||||
DefaultNavigator(fragment) as Navigator
|
||||
}
|
||||
}
|
||||
}
|
@ -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.core.epoxy
|
||||
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotredesign.R
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_error_retry)
|
||||
abstract class ErrorWithRetryItem : VectorEpoxyModel<ErrorWithRetryItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
var text: String? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var listener: (() -> Unit)? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
holder.textView.text = text
|
||||
holder.buttonView.setOnClickListener { listener?.invoke() }
|
||||
}
|
||||
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
val textView by bind<TextView>(R.id.itemErrorRetryText)
|
||||
val buttonView by bind<Button>(R.id.itemErrorRetryButton)
|
||||
}
|
||||
}
|
@ -16,9 +16,11 @@
|
||||
|
||||
package im.vector.riotredesign.core.epoxy
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.ProgressBar
|
||||
import com.airbnb.epoxy.ModelView
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotredesign.R
|
||||
|
||||
@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
|
||||
class LoadingItem(context: Context) : ProgressBar(context)
|
||||
@EpoxyModelClass(layout = R.layout.item_loading)
|
||||
abstract class LoadingItem : VectorEpoxyModel<LoadingItem.Holder>() {
|
||||
|
||||
class Holder : VectorEpoxyHolder()
|
||||
}
|
@ -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.core.epoxy
|
||||
|
||||
import android.widget.TextView
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotredesign.R
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_no_result)
|
||||
abstract class NoResultItem : VectorEpoxyModel<NoResultItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
var text: String? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
holder.textView.text = text
|
||||
}
|
||||
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
val textView by bind<TextView>(R.id.itemNoResultText)
|
||||
}
|
||||
}
|
@ -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.core.error
|
||||
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.resources.StringProvider
|
||||
|
||||
class ErrorFormatter(val stringProvider: StringProvider) {
|
||||
|
||||
|
||||
fun toHumanReadable(failure: Failure): String {
|
||||
// Default
|
||||
return failure.localizedMessage
|
||||
}
|
||||
|
||||
fun toHumanReadable(throwable: Throwable?): String {
|
||||
|
||||
return when (throwable) {
|
||||
null -> ""
|
||||
is Failure.NetworkConnection -> stringProvider.getString(R.string.error_no_network)
|
||||
else -> throwable.localizedMessage
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -18,26 +18,26 @@ package im.vector.riotredesign.core.extensions
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
fun androidx.fragment.app.Fragment.addFragment(fragment: Fragment, frameId: Int) {
|
||||
fun Fragment.addFragment(fragment: Fragment, frameId: Int) {
|
||||
fragmentManager?.inTransaction { add(frameId, fragment) }
|
||||
}
|
||||
|
||||
fun androidx.fragment.app.Fragment.replaceFragment(fragment: Fragment, frameId: Int) {
|
||||
fun Fragment.replaceFragment(fragment: Fragment, frameId: Int) {
|
||||
fragmentManager?.inTransaction { replace(frameId, fragment) }
|
||||
}
|
||||
|
||||
fun androidx.fragment.app.Fragment.addFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
|
||||
fun Fragment.addFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
|
||||
fragmentManager?.inTransaction { replace(frameId, fragment).addToBackStack(tag) }
|
||||
}
|
||||
|
||||
fun androidx.fragment.app.Fragment.addChildFragment(fragment: Fragment, frameId: Int) {
|
||||
fun Fragment.addChildFragment(fragment: Fragment, frameId: Int) {
|
||||
childFragmentManager.inTransaction { add(frameId, fragment) }
|
||||
}
|
||||
|
||||
fun androidx.fragment.app.Fragment.replaceChildFragment(fragment: Fragment, frameId: Int) {
|
||||
fun Fragment.replaceChildFragment(fragment: Fragment, frameId: Int) {
|
||||
childFragmentManager.inTransaction { replace(frameId, fragment) }
|
||||
}
|
||||
|
||||
fun androidx.fragment.app.Fragment.addChildFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
|
||||
fun Fragment.addChildFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
|
||||
childFragmentManager.inTransaction { replace(frameId, fragment).addToBackStack(tag) }
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
|
||||
/**
|
||||
* Set a text in the TextView, or set visibility to GONE it if the text is null
|
||||
*/
|
||||
fun TextView.setTextOrHide(newText: String?, hideWhenBlank: Boolean = true) {
|
||||
if (newText == null
|
||||
|| (newText.isBlank() && hideWhenBlank)) {
|
||||
isVisible = false
|
||||
} else {
|
||||
this.text = newText
|
||||
isVisible = true
|
||||
}
|
||||
}
|
97
vector/src/main/java/im/vector/riotredesign/core/platform/ButtonStateView.kt
Executable file
97
vector/src/main/java/im/vector/riotredesign/core/platform/ButtonStateView.kt
Executable file
@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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.platform
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import im.vector.riotredesign.R
|
||||
import kotlinx.android.synthetic.main.view_button_state.view.*
|
||||
|
||||
class ButtonStateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0)
|
||||
: FrameLayout(context, attrs, defStyle) {
|
||||
|
||||
sealed class State {
|
||||
object Button : State()
|
||||
object Loading : State()
|
||||
object Loaded : State()
|
||||
object Error : State()
|
||||
}
|
||||
|
||||
var callback: Callback? = null
|
||||
|
||||
interface Callback {
|
||||
fun onButtonClicked()
|
||||
fun onRetryClicked()
|
||||
}
|
||||
|
||||
// Big or Flat button
|
||||
var button: Button
|
||||
|
||||
init {
|
||||
View.inflate(context, R.layout.view_button_state, this)
|
||||
layoutParams = LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
|
||||
buttonStateRetry.setOnClickListener {
|
||||
callback?.onRetryClicked()
|
||||
}
|
||||
|
||||
// Read attributes
|
||||
context.theme.obtainStyledAttributes(
|
||||
attrs,
|
||||
R.styleable.ButtonStateView,
|
||||
0, 0)
|
||||
.apply {
|
||||
try {
|
||||
if (getBoolean(R.styleable.ButtonStateView_bsv_use_flat_button, true)) {
|
||||
button = buttonStateButtonFlat
|
||||
buttonStateButtonBig.isVisible = false
|
||||
} else {
|
||||
button = buttonStateButtonBig
|
||||
buttonStateButtonFlat.isVisible = false
|
||||
}
|
||||
|
||||
button.text = getString(R.styleable.ButtonStateView_bsv_button_text)
|
||||
buttonStateLoaded.setImageDrawable(getDrawable(R.styleable.ButtonStateView_bsv_loaded_image_src))
|
||||
} finally {
|
||||
recycle()
|
||||
}
|
||||
}
|
||||
|
||||
button.setOnClickListener {
|
||||
callback?.onButtonClicked()
|
||||
}
|
||||
}
|
||||
|
||||
fun render(newState: State) {
|
||||
if (newState == State.Button) {
|
||||
button.isVisible = true
|
||||
} else {
|
||||
// We use isInvisible because we want to keep button space in the layout
|
||||
button.isInvisible = true
|
||||
}
|
||||
|
||||
buttonStateLoading.isVisible = newState == State.Loading
|
||||
buttonStateLoaded.isVisible = newState == State.Loaded
|
||||
buttonStateRetry.isVisible = newState == State.Error
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.platform
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.Checkable
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
||||
class CheckableConstraintLayout : ConstraintLayout, Checkable {
|
||||
|
||||
private var mChecked = false
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
|
||||
override fun isChecked(): Boolean {
|
||||
return mChecked
|
||||
}
|
||||
|
||||
override fun setChecked(b: Boolean) {
|
||||
if (b != mChecked) {
|
||||
mChecked = b
|
||||
refreshDrawableState()
|
||||
}
|
||||
}
|
||||
|
||||
override fun toggle() {
|
||||
isChecked = !mChecked
|
||||
}
|
||||
|
||||
public override fun onCreateDrawableState(extraSpace: Int): IntArray {
|
||||
val drawableState = super.onCreateDrawableState(extraSpace + 1)
|
||||
if (isChecked) {
|
||||
View.mergeDrawableStates(drawableState, CHECKED_STATE_SET)
|
||||
}
|
||||
return drawableState
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val CHECKED_STATE_SET = intArrayOf(android.R.attr.state_checked)
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.platform
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import im.vector.riotredesign.core.utils.LiveEvent
|
||||
import im.vector.riotredesign.features.configuration.VectorConfiguration
|
||||
import org.koin.standalone.KoinComponent
|
||||
import org.koin.standalone.inject
|
||||
import timber.log.Timber
|
||||
|
||||
class ConfigurationViewModel : ViewModel(), KoinComponent {
|
||||
|
||||
private val vectorConfiguration: VectorConfiguration by inject()
|
||||
|
||||
private var currentConfigurationValue: String? = null
|
||||
|
||||
private val _activityRestarter = MutableLiveData<LiveEvent<Unit>>()
|
||||
val activityRestarter: LiveData<LiveEvent<Unit>>
|
||||
get() = _activityRestarter
|
||||
|
||||
|
||||
fun onActivityResumed() {
|
||||
if (currentConfigurationValue == null) {
|
||||
currentConfigurationValue = vectorConfiguration.getHash()
|
||||
Timber.v("Configuration: init to $currentConfigurationValue")
|
||||
} else {
|
||||
val newHash = vectorConfiguration.getHash()
|
||||
Timber.v("Configuration: newHash $newHash")
|
||||
|
||||
if (newHash != currentConfigurationValue) {
|
||||
Timber.v("Configuration: recreate the Activity")
|
||||
currentConfigurationValue = newHash
|
||||
|
||||
_activityRestarter.postValue(LiveEvent(Unit))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -24,10 +24,11 @@ import butterknife.BindView
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.extensions.hideKeyboard
|
||||
import kotlinx.android.synthetic.main.activity.*
|
||||
import org.koin.android.ext.android.get
|
||||
|
||||
/**
|
||||
* Simple activity with a toolbar, a waiting overlay, and a fragment container and a mxSession.
|
||||
* Simple activity with a toolbar, a waiting overlay, and a fragment container and a session.
|
||||
*/
|
||||
abstract class SimpleFragmentActivity : VectorBaseActivity() {
|
||||
|
||||
@ -42,10 +43,10 @@ abstract class SimpleFragmentActivity : VectorBaseActivity() {
|
||||
@BindView(R.id.waiting_view_status_horizontal_progress)
|
||||
lateinit var waitingHorizontalProgress: ProgressBar
|
||||
|
||||
protected val mSession = get<Session>()
|
||||
protected val session = get<Session>()
|
||||
|
||||
override fun initUiAndData() {
|
||||
configureToolbar()
|
||||
configureToolbar(toolbar)
|
||||
waitingView = findViewById(R.id.waiting_view)
|
||||
}
|
||||
|
||||
|
@ -17,9 +17,9 @@
|
||||
package im.vector.riotredesign.core.platform
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import im.vector.riotredesign.R
|
||||
import kotlinx.android.synthetic.main.view_state.view.*
|
||||
@ -30,7 +30,7 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
|
||||
sealed class State {
|
||||
object Content : State()
|
||||
object Loading : State()
|
||||
data class Empty(val message: CharSequence? = null) : State()
|
||||
data class Empty(val title: CharSequence? = null, val image: Drawable? = null, val message: CharSequence? = null) : State()
|
||||
data class Error(val message: CharSequence? = null) : State()
|
||||
}
|
||||
|
||||
@ -52,7 +52,7 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
|
||||
|
||||
init {
|
||||
View.inflate(context, R.layout.view_state, this)
|
||||
layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
||||
layoutParams = LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
|
||||
errorRetryView.setOnClickListener {
|
||||
eventCallback?.onRetryClicked()
|
||||
}
|
||||
@ -62,35 +62,33 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
|
||||
|
||||
private fun update(newState: State) {
|
||||
when (newState) {
|
||||
is StateView.State.Content -> {
|
||||
is State.Content -> {
|
||||
progressBar.visibility = View.INVISIBLE
|
||||
errorView.visibility = View.INVISIBLE
|
||||
emptyView.visibility = View.INVISIBLE
|
||||
contentView?.visibility = View.VISIBLE
|
||||
}
|
||||
is StateView.State.Loading -> {
|
||||
is State.Loading -> {
|
||||
progressBar.visibility = View.VISIBLE
|
||||
errorView.visibility = View.INVISIBLE
|
||||
emptyView.visibility = View.INVISIBLE
|
||||
contentView?.visibility = View.INVISIBLE
|
||||
}
|
||||
is StateView.State.Empty -> {
|
||||
is State.Empty -> {
|
||||
progressBar.visibility = View.INVISIBLE
|
||||
errorView.visibility = View.INVISIBLE
|
||||
emptyView.visibility = View.VISIBLE
|
||||
emptyImageView.setImageDrawable(newState.image)
|
||||
emptyMessageView.text = newState.message
|
||||
if (contentView != null) {
|
||||
contentView!!.visibility = View.INVISIBLE
|
||||
}
|
||||
emptyTitleView.text = newState.title
|
||||
contentView?.visibility = View.INVISIBLE
|
||||
}
|
||||
is StateView.State.Error -> {
|
||||
is State.Error -> {
|
||||
progressBar.visibility = View.INVISIBLE
|
||||
errorView.visibility = View.VISIBLE
|
||||
emptyView.visibility = View.INVISIBLE
|
||||
errorMessageView.text = newState.message
|
||||
if (contentView != null) {
|
||||
contentView!!.visibility = View.INVISIBLE
|
||||
}
|
||||
contentView?.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
package im.vector.riotredesign.core.platform
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
@ -25,6 +26,8 @@ import androidx.annotation.*
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import butterknife.BindView
|
||||
import butterknife.ButterKnife
|
||||
import butterknife.Unbinder
|
||||
@ -34,14 +37,16 @@ import com.google.android.material.snackbar.Snackbar
|
||||
import im.vector.riotredesign.BuildConfig
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.utils.toast
|
||||
import im.vector.riotredesign.features.configuration.VectorConfiguration
|
||||
import im.vector.riotredesign.features.rageshake.BugReportActivity
|
||||
import im.vector.riotredesign.features.rageshake.BugReporter
|
||||
import im.vector.riotredesign.features.rageshake.RageShake
|
||||
import im.vector.riotredesign.features.themes.ActivityOtherThemes
|
||||
import im.vector.riotredesign.features.themes.ThemeUtils
|
||||
import im.vector.riotredesign.receivers.DebugReceiver
|
||||
import im.vector.ui.themes.ActivityOtherThemes
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.disposables.Disposable
|
||||
import org.koin.android.ext.android.inject
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
@ -50,11 +55,6 @@ abstract class VectorBaseActivity : BaseMvRxActivity() {
|
||||
* UI
|
||||
* ========================================================================================== */
|
||||
|
||||
@Nullable
|
||||
@JvmField
|
||||
@BindView(R.id.toolbar)
|
||||
var toolbar: Toolbar? = null
|
||||
|
||||
@Nullable
|
||||
@JvmField
|
||||
@BindView(R.id.vector_coordinator_layout)
|
||||
@ -64,6 +64,10 @@ abstract class VectorBaseActivity : BaseMvRxActivity() {
|
||||
* DATA
|
||||
* ========================================================================================== */
|
||||
|
||||
private val vectorConfiguration: VectorConfiguration by inject()
|
||||
|
||||
private lateinit var configurationViewModel: ConfigurationViewModel
|
||||
|
||||
private var unBinder: Unbinder? = null
|
||||
|
||||
private var savedInstanceState: Bundle? = null
|
||||
@ -76,6 +80,10 @@ abstract class VectorBaseActivity : BaseMvRxActivity() {
|
||||
|
||||
private var rageShake: RageShake? = null
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(vectorConfiguration.getLocalisedContext(base))
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
restorables.forEach { it.onSaveInstanceState(outState) }
|
||||
@ -101,6 +109,16 @@ abstract class VectorBaseActivity : BaseMvRxActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
configurationViewModel = ViewModelProviders.of(this).get(ConfigurationViewModel::class.java)
|
||||
|
||||
configurationViewModel.activityRestarter.observe(this, Observer {
|
||||
if (!it.hasBeenHandled) {
|
||||
// Recreate the Activity because configuration has changed
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
})
|
||||
|
||||
// Shake detector
|
||||
rageShake = RageShake(this)
|
||||
|
||||
@ -133,6 +151,8 @@ abstract class VectorBaseActivity : BaseMvRxActivity() {
|
||||
|
||||
unBinder?.unbind()
|
||||
unBinder = null
|
||||
|
||||
uiDisposables.dispose()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@ -140,6 +160,8 @@ abstract class VectorBaseActivity : BaseMvRxActivity() {
|
||||
|
||||
Timber.v("onResume Activity ${this.javaClass.simpleName}")
|
||||
|
||||
configurationViewModel.onActivityResumed()
|
||||
|
||||
if (this !is BugReportActivity) {
|
||||
rageShake?.start()
|
||||
}
|
||||
@ -244,14 +266,16 @@ abstract class VectorBaseActivity : BaseMvRxActivity() {
|
||||
protected fun isFirstCreation() = savedInstanceState == null
|
||||
|
||||
/**
|
||||
* Configure the Toolbar. It MUST be present in your layout with id "toolbar"
|
||||
* Configure the Toolbar, with default back button.
|
||||
*/
|
||||
protected fun configureToolbar() {
|
||||
protected fun configureToolbar(toolbar: Toolbar, displayBack: Boolean = true) {
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
supportActionBar?.let {
|
||||
it.setDisplayShowHomeEnabled(true)
|
||||
it.setDisplayHomeAsUpEnabled(true)
|
||||
if (displayBack) {
|
||||
supportActionBar?.let {
|
||||
it.setDisplayShowHomeEnabled(true)
|
||||
it.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -329,8 +353,12 @@ abstract class VectorBaseActivity : BaseMvRxActivity() {
|
||||
* Temporary method
|
||||
* ========================================================================================== */
|
||||
|
||||
fun notImplemented() {
|
||||
toast(getString(R.string.not_implemented))
|
||||
fun notImplemented(message: String = "") {
|
||||
if (message.isNotBlank()) {
|
||||
toast(getString(R.string.not_implemented) + ": $message")
|
||||
} else {
|
||||
toast(getString(R.string.not_implemented))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -22,11 +22,17 @@ import android.view.*
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import butterknife.ButterKnife
|
||||
import butterknife.Unbinder
|
||||
import com.airbnb.mvrx.BaseMvRxFragment
|
||||
import com.airbnb.mvrx.MvRx
|
||||
import com.bumptech.glide.util.Util.assertMainThread
|
||||
import im.vector.riotredesign.features.navigation.Navigator
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.disposables.Disposable
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import timber.log.Timber
|
||||
|
||||
abstract class VectorBaseFragment : BaseMvRxFragment(), OnBackPressed {
|
||||
@ -38,6 +44,12 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), OnBackPressed {
|
||||
activity as VectorBaseActivity
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* Navigator
|
||||
* ========================================================================================== */
|
||||
|
||||
protected val navigator: Navigator by inject { parametersOf(this) }
|
||||
|
||||
/* ==========================================================================================
|
||||
* Life cycle
|
||||
* ========================================================================================== */
|
||||
@ -78,6 +90,12 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), OnBackPressed {
|
||||
mUnBinder = null
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
uiDisposables.dispose()
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* Restorable
|
||||
* ========================================================================================== */
|
||||
@ -100,6 +118,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), OnBackPressed {
|
||||
|
||||
override fun invalidate() {
|
||||
//no-ops by default
|
||||
Timber.w("invalidate() method has not been implemented")
|
||||
}
|
||||
|
||||
protected fun setArguments(args: Parcelable? = null) {
|
||||
@ -113,6 +132,30 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), OnBackPressed {
|
||||
return this
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* Toolbar
|
||||
* ========================================================================================== */
|
||||
|
||||
/**
|
||||
* Configure the Toolbar.
|
||||
*/
|
||||
protected fun setupToolbar(toolbar: Toolbar) {
|
||||
val parentActivity = vectorBaseActivity
|
||||
if (parentActivity is ToolbarConfigurable) {
|
||||
parentActivity.configure(toolbar)
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* Disposable
|
||||
* ========================================================================================== */
|
||||
|
||||
private val uiDisposables = CompositeDisposable()
|
||||
|
||||
protected fun Disposable.disposeOnDestroy(): Disposable {
|
||||
uiDisposables.add(this)
|
||||
return this
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* MENU MANAGEMENT
|
||||
|
@ -39,7 +39,7 @@ class RoomAvatarPreference : UserAvatarPreference {
|
||||
override fun refreshAvatar() {
|
||||
if (null != mAvatarView && null != mRoom) {
|
||||
// TODO
|
||||
// VectorUtils.loadRoomAvatar(context, mSession, mAvatarView, mRoom)
|
||||
// VectorUtils.loadRoomAvatar(context, session, mAvatarView, mRoom)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -53,8 +53,8 @@ open class UserAvatarPreference : Preference {
|
||||
open fun refreshAvatar() {
|
||||
if (null != mAvatarView && null != mSession) {
|
||||
// TODO
|
||||
// val myUser = mSession!!.myUser
|
||||
// VectorUtils.loadUserAvatar(context, mSession, mAvatarView, myUser.avatarUrl, myUser.user_id, myUser.displayname)
|
||||
// val myUser = session!!.myUser
|
||||
// VectorUtils.loadUserAvatar(context, session, mAvatarView, myUser.avatarUrl, myUser.user_id, myUser.displayname)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,7 +99,7 @@ class VectorGroupPreference : SwitchPreference {
|
||||
private fun refreshAvatar() {
|
||||
if (null != mAvatarView && null != mSession && null != mGroup) {
|
||||
// TODO
|
||||
// VectorUtils.loadGroupAvatar(context, mSession, mAvatarView, mGroup)
|
||||
// VectorUtils.loadGroupAvatar(context, session, mAvatarView, mGroup)
|
||||
}
|
||||
}
|
||||
}
|
@ -19,8 +19,11 @@
|
||||
package im.vector.riotredesign.core.resources
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import im.vector.riotredesign.features.themes.ThemeUtils
|
||||
|
||||
class ColorProvider(private val context: Context) {
|
||||
|
||||
@ -28,4 +31,16 @@ class ColorProvider(private val context: Context) {
|
||||
return ContextCompat.getColor(context, colorRes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates color attributes to colors
|
||||
*
|
||||
* @param c Context
|
||||
* @param colorAttribute Color Attribute
|
||||
* @return Requested Color
|
||||
*/
|
||||
@ColorInt
|
||||
fun getColorFromAttribute(@AttrRes colorAttribute: Int): Int {
|
||||
return ThemeUtils.getColor(context, colorAttribute)
|
||||
}
|
||||
|
||||
}
|
@ -29,4 +29,9 @@ object DateProvider {
|
||||
return LocalDateTime.ofInstant(instant, zoneId)
|
||||
}
|
||||
|
||||
fun currentLocalDateTime(): LocalDateTime {
|
||||
val instant = Instant.now()
|
||||
return LocalDateTime.ofInstant(instant, zoneId)
|
||||
}
|
||||
|
||||
}
|
@ -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.resources
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.annotation.ArrayRes
|
||||
import androidx.annotation.NonNull
|
||||
|
||||
class StringArrayProvider(private val resources: Resources) {
|
||||
|
||||
/**
|
||||
* Returns a localized string array from the application's package's
|
||||
* default string array table.
|
||||
*
|
||||
* @param resId Resource id for the string array
|
||||
* @return The string array associated with the resource, stripped of styled
|
||||
* text information.
|
||||
*/
|
||||
@NonNull
|
||||
fun getStringArray(@ArrayRes resId: Int): Array<String> {
|
||||
return resources.getStringArray(resId)
|
||||
}
|
||||
|
||||
}
|
@ -106,7 +106,7 @@ class EventStreamServiceX : VectorService() {
|
||||
|
||||
// do not suspend the application if there is some active calls
|
||||
if (ServiceState.CATCHUP == serviceState) {
|
||||
val hasActiveCalls = mSession?.mCallsManager?.hasActiveCalls() == true
|
||||
val hasActiveCalls = session?.mCallsManager?.hasActiveCalls() == true
|
||||
|
||||
// if there are some active calls, the catchup should not be stopped.
|
||||
// because an user could answer to a call from another device.
|
||||
@ -351,12 +351,12 @@ class EventStreamServiceX : VectorService() {
|
||||
Timber.i("## stop(): the service is stopped")
|
||||
|
||||
/* TODO
|
||||
if (null != mSession && mSession!!.isAlive) {
|
||||
mSession!!.stopEventStream()
|
||||
mSession!!.dataHandler.removeListener(mEventsListener)
|
||||
CallsManager.getSharedInstance().removeSession(mSession)
|
||||
if (null != session && session!!.isAlive) {
|
||||
session!!.stopEventStream()
|
||||
session!!.dataHandler.removeListener(mEventsListener)
|
||||
CallsManager.getSharedInstance().removeSession(session)
|
||||
}
|
||||
mSession = null
|
||||
session = null
|
||||
*/
|
||||
|
||||
// Stop the service
|
||||
@ -389,7 +389,7 @@ class EventStreamServiceX : VectorService() {
|
||||
|
||||
if (canCatchup) {
|
||||
if (mSession != null) {
|
||||
// TODO mSession!!.catchupEventStream()
|
||||
// TODO session!!.catchupEventStream()
|
||||
} else {
|
||||
Timber.i("catchup no session")
|
||||
}
|
||||
|
@ -20,11 +20,13 @@ import android.text.Spannable
|
||||
import com.otaliastudios.autocomplete.AutocompletePolicy
|
||||
|
||||
class CommandAutocompletePolicy : AutocompletePolicy {
|
||||
|
||||
var enabled: Boolean = true
|
||||
|
||||
override fun getQuery(text: Spannable): CharSequence {
|
||||
if (text.length > 0) {
|
||||
return text.substring(1, text.length)
|
||||
}
|
||||
|
||||
// Should not happen
|
||||
return ""
|
||||
}
|
||||
@ -34,7 +36,7 @@ class CommandAutocompletePolicy : AutocompletePolicy {
|
||||
|
||||
// Only if text which starts with '/' and without space
|
||||
override fun shouldShowPopup(text: Spannable?, cursorPos: Int): Boolean {
|
||||
return text?.startsWith("/") == true
|
||||
return enabled && text?.startsWith("/") == true
|
||||
&& !text.contains(" ")
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,145 @@
|
||||
/*
|
||||
* 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.configuration
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import im.vector.riotredesign.features.settings.FontScale
|
||||
import im.vector.riotredesign.features.settings.VectorLocale
|
||||
import im.vector.riotredesign.features.themes.ThemeUtils
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Handle locale configuration change, such as theme, font size and locale chosen by the user
|
||||
*/
|
||||
class VectorConfiguration(private val context: Context) {
|
||||
|
||||
// TODO Import mLanguageReceiver From Riot?
|
||||
fun onConfigurationChanged(newConfig: Configuration?) {
|
||||
if (Locale.getDefault().toString() != VectorLocale.applicationLocale.toString()) {
|
||||
Timber.v("## onConfigurationChanged() : the locale has been updated to " + Locale.getDefault().toString()
|
||||
+ ", restore the expected value " + VectorLocale.applicationLocale.toString())
|
||||
updateApplicationSettings(VectorLocale.applicationLocale,
|
||||
FontScale.getFontScalePrefValue(context),
|
||||
ThemeUtils.getApplicationTheme(context))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun updateApplicationSettings(locale: Locale, textSize: String, theme: String) {
|
||||
VectorLocale.saveApplicationLocale(context, locale)
|
||||
FontScale.saveFontScale(context, textSize)
|
||||
Locale.setDefault(locale)
|
||||
|
||||
val config = Configuration(context.resources.configuration)
|
||||
config.locale = locale
|
||||
config.fontScale = FontScale.getFontScale(context)
|
||||
context.resources.updateConfiguration(config, context.resources.displayMetrics)
|
||||
|
||||
ThemeUtils.setApplicationTheme(context, theme)
|
||||
// TODO PhoneNumberUtils.onLocaleUpdate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the application theme
|
||||
*
|
||||
* @param theme the new theme
|
||||
*/
|
||||
fun updateApplicationTheme(theme: String) {
|
||||
ThemeUtils.setApplicationTheme(context, theme)
|
||||
updateApplicationSettings(VectorLocale.applicationLocale,
|
||||
FontScale.getFontScalePrefValue(context),
|
||||
theme)
|
||||
}
|
||||
|
||||
/**
|
||||
* Init the configuration from the saved one
|
||||
*/
|
||||
fun initConfiguration() {
|
||||
VectorLocale.init(context)
|
||||
val locale = VectorLocale.applicationLocale
|
||||
val fontScale = FontScale.getFontScale(context)
|
||||
val theme = ThemeUtils.getApplicationTheme(context)
|
||||
|
||||
Locale.setDefault(locale)
|
||||
val config = Configuration(context.resources.configuration)
|
||||
config.locale = locale
|
||||
config.fontScale = fontScale
|
||||
context.resources.updateConfiguration(config, context.resources.displayMetrics)
|
||||
|
||||
// init the theme
|
||||
ThemeUtils.setApplicationTheme(context, theme)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the application locale
|
||||
*
|
||||
* @param locale
|
||||
*/
|
||||
// TODO Call from LanguagePickerActivity
|
||||
fun updateApplicationLocale(locale: Locale) {
|
||||
updateApplicationSettings(locale, FontScale.getFontScalePrefValue(context), ThemeUtils.getApplicationTheme(context))
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a localised context
|
||||
*
|
||||
* @param context the context
|
||||
* @return the localised context
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
fun getLocalisedContext(context: Context): Context {
|
||||
try {
|
||||
val resources = context.resources
|
||||
val locale = VectorLocale.applicationLocale
|
||||
val configuration = resources.configuration
|
||||
configuration.fontScale = FontScale.getFontScale(context)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
configuration.setLocale(locale)
|
||||
configuration.setLayoutDirection(locale)
|
||||
return context.createConfigurationContext(configuration)
|
||||
} else {
|
||||
configuration.locale = locale
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
configuration.setLayoutDirection(locale)
|
||||
}
|
||||
resources.updateConfiguration(configuration, resources.displayMetrics)
|
||||
return context
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## getLocalisedContext() failed")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the locale status value
|
||||
* @param activity the activity
|
||||
* @return the local status value
|
||||
*/
|
||||
// TODO Create data class for this
|
||||
fun getHash(): String {
|
||||
return (VectorLocale.applicationLocale.toString()
|
||||
+ "_" + FontScale.getFontScalePrefValue(context)
|
||||
+ "_" + ThemeUtils.getApplicationTheme(context))
|
||||
}
|
||||
}
|
@ -43,7 +43,7 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() {
|
||||
override fun initUiAndData() {
|
||||
super.initUiAndData()
|
||||
viewModel = ViewModelProviders.of(this).get(KeysBackupRestoreSharedViewModel::class.java)
|
||||
viewModel.initSession(mSession)
|
||||
viewModel.initSession(session)
|
||||
|
||||
viewModel.keyVersionResult.observe(this, Observer { keyVersion ->
|
||||
|
||||
|
@ -44,7 +44,7 @@ class KeysBackupManageActivity : SimpleFragmentActivity() {
|
||||
override fun initUiAndData() {
|
||||
super.initUiAndData()
|
||||
viewModel = ViewModelProviders.of(this).get(KeysBackupSettingsViewModel::class.java)
|
||||
viewModel.initSession(mSession)
|
||||
viewModel.initSession(session)
|
||||
|
||||
|
||||
if (supportFragmentManager.fragments.isEmpty()) {
|
||||
@ -52,7 +52,7 @@ class KeysBackupManageActivity : SimpleFragmentActivity() {
|
||||
.replace(R.id.container, KeysBackupSettingsFragment.newInstance())
|
||||
.commitNow()
|
||||
|
||||
mSession.getKeysBackupService()
|
||||
session.getKeysBackupService()
|
||||
.forceUsingLastVersion(object : MatrixCallback<Boolean> {})
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,7 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
|
||||
|
||||
viewModel = ViewModelProviders.of(this).get(KeysBackupSetupSharedViewModel::class.java)
|
||||
viewModel.showManualExport.value = intent.getBooleanExtra(EXTRA_SHOW_MANUAL_EXPORT, false)
|
||||
viewModel.initSession(mSession)
|
||||
viewModel.initSession(session)
|
||||
|
||||
|
||||
viewModel.isCreatingBackupVersion.observe(this, Observer {
|
||||
@ -124,7 +124,7 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
|
||||
/*
|
||||
showWaitingView()
|
||||
|
||||
CommonActivityUtils.exportKeys(mSession, passphrase, object : SimpleApiCallback<String>(this@KeysBackupSetupActivity) {
|
||||
CommonActivityUtils.exportKeys(session, passphrase, object : SimpleApiCallback<String>(this@KeysBackupSetupActivity) {
|
||||
override fun onSuccess(filename: String) {
|
||||
hideWaitingView()
|
||||
|
||||
|
@ -90,9 +90,9 @@ class SASVerificationActivity : SimpleFragmentActivity() {
|
||||
val isIncoming = intent.getBooleanExtra(EXTRA_IS_INCOMING, false)
|
||||
if (isIncoming) {
|
||||
//incoming always have a transaction id
|
||||
viewModel.initIncoming(mSession, intent.getStringExtra(EXTRA_OTHER_USER_ID), transactionID)
|
||||
viewModel.initIncoming(session, intent.getStringExtra(EXTRA_OTHER_USER_ID), transactionID)
|
||||
} else {
|
||||
viewModel.initOutgoing(mSession, intent.getStringExtra(EXTRA_OTHER_USER_ID), intent.getStringExtra(EXTRA_OTHER_DEVICE_ID))
|
||||
viewModel.initOutgoing(session, intent.getStringExtra(EXTRA_OTHER_USER_ID), intent.getStringExtra(EXTRA_OTHER_DEVICE_ID))
|
||||
}
|
||||
|
||||
if (isIncoming) {
|
||||
|
@ -46,9 +46,9 @@ object AvatarRenderer {
|
||||
private const val THUMBNAIL_SIZE = 250
|
||||
|
||||
private val AVATAR_COLOR_LIST = listOf(
|
||||
R.color.avatar_color_1,
|
||||
R.color.avatar_color_2,
|
||||
R.color.avatar_color_3
|
||||
R.color.riotx_avatar_fill_1,
|
||||
R.color.riotx_avatar_fill_2,
|
||||
R.color.riotx_avatar_fill_3
|
||||
)
|
||||
|
||||
@UiThread
|
||||
@ -73,10 +73,12 @@ object AvatarRenderer {
|
||||
identifier: String,
|
||||
name: String?,
|
||||
target: Target<Drawable>) {
|
||||
if (name.isNullOrEmpty()) {
|
||||
return
|
||||
val displayName = if (name.isNullOrBlank()) {
|
||||
identifier
|
||||
} else {
|
||||
name
|
||||
}
|
||||
val placeholder = getPlaceholderDrawable(context, identifier, name)
|
||||
val placeholder = getPlaceholderDrawable(context, identifier, displayName)
|
||||
buildGlideRequest(glideRequest, avatarUrl)
|
||||
.placeholder(placeholder)
|
||||
.into(target)
|
||||
@ -122,33 +124,4 @@ object AvatarRenderer {
|
||||
.load(resolvedUrl)
|
||||
.apply(RequestOptions.circleCropTransform())
|
||||
}
|
||||
|
||||
|
||||
//Based on riot-web implementation
|
||||
@ColorRes
|
||||
fun getColorFromUserId(sender: String): Int {
|
||||
var hash = 0
|
||||
var i = 0
|
||||
var chr: Char
|
||||
if (sender.isEmpty()) {
|
||||
return R.color.username_1
|
||||
}
|
||||
while (i < sender.length) {
|
||||
chr = sender[i]
|
||||
hash = (hash shl 5) - hash + chr.toInt()
|
||||
hash = hash or 0
|
||||
i++
|
||||
}
|
||||
val cI = Math.abs(hash) % 8 + 1
|
||||
return when (cI) {
|
||||
1 -> R.color.username_1
|
||||
2 -> R.color.username_2
|
||||
3 -> R.color.username_3
|
||||
4 -> R.color.username_4
|
||||
5 -> R.color.username_5
|
||||
6 -> R.color.username_6
|
||||
7 -> R.color.username_7
|
||||
else -> R.color.username_8
|
||||
}
|
||||
}
|
||||
}
|
@ -21,7 +21,6 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.GravityCompat
|
||||
@ -32,16 +31,13 @@ import com.airbnb.mvrx.viewModel
|
||||
import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.extensions.hideKeyboard
|
||||
import im.vector.riotredesign.core.extensions.observeEvent
|
||||
import im.vector.riotredesign.core.extensions.replaceFragment
|
||||
import im.vector.riotredesign.core.platform.OnBackPressed
|
||||
import im.vector.riotredesign.core.platform.ToolbarConfigurable
|
||||
import im.vector.riotredesign.core.platform.VectorBaseActivity
|
||||
import im.vector.riotredesign.features.crypto.verification.IncomingVerificationRequestHandler
|
||||
import im.vector.riotredesign.features.home.room.detail.LoadingRoomDetailFragment
|
||||
import im.vector.riotredesign.features.rageshake.BugReporter
|
||||
import im.vector.riotredesign.features.rageshake.VectorUncaughtExceptionHandler
|
||||
import im.vector.riotredesign.features.settings.VectorSettingsActivity
|
||||
import im.vector.riotredesign.features.workers.signout.SignOutUiWorker
|
||||
import kotlinx.android.synthetic.main.activity_home.*
|
||||
import org.koin.android.ext.android.inject
|
||||
@ -72,15 +68,13 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||
bindScope(getOrCreateScope(HomeModule.HOME_SCOPE))
|
||||
homeNavigator.activity = this
|
||||
drawerLayout.addDrawerListener(drawerListener)
|
||||
if (savedInstanceState == null) {
|
||||
if (isFirstCreation()) {
|
||||
val homeDrawerFragment = HomeDrawerFragment.newInstance()
|
||||
val loadingDetail = LoadingRoomDetailFragment.newInstance()
|
||||
val loadingDetail = LoadingFragment.newInstance()
|
||||
replaceFragment(loadingDetail, R.id.homeDetailFragmentContainer)
|
||||
replaceFragment(homeDrawerFragment, R.id.homeDrawerFragmentContainer)
|
||||
}
|
||||
homeActivityViewModel.openRoomLiveData.observeEvent(this) {
|
||||
homeNavigator.openRoomDetail(it, null)
|
||||
}
|
||||
|
||||
homeActivityViewModel.isLoading.observe(this, Observer<Boolean> {
|
||||
// TODO better UI
|
||||
if (it) {
|
||||
@ -118,12 +112,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||
}
|
||||
|
||||
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()
|
||||
configureToolbar(toolbar, false)
|
||||
}
|
||||
|
||||
override fun getMenuRes() = R.menu.home
|
||||
@ -134,19 +123,10 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||
drawerLayout.openDrawer(GravityCompat.START)
|
||||
return true
|
||||
}
|
||||
R.id.sliding_menu_settings -> {
|
||||
startActivity(VectorSettingsActivity.getIntent(this, "TODO"))
|
||||
return true
|
||||
}
|
||||
R.id.sliding_menu_sign_out -> {
|
||||
SignOutUiWorker(this).perform(Matrix.getInstance().currentSession!!)
|
||||
return true
|
||||
}
|
||||
// TODO Temporary code here to create a room
|
||||
R.id.tmp_menu_create_room -> {
|
||||
homeActivityViewModel.createRoom()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
@ -164,8 +144,9 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||
}
|
||||
|
||||
private fun recursivelyDispatchOnBackPressed(fm: FragmentManager): Boolean {
|
||||
if (fm.backStackEntryCount == 0)
|
||||
return false
|
||||
// if (fm.backStackEntryCount == 0)
|
||||
// return false
|
||||
|
||||
val reverseOrder = fm.fragments.filter { it is OnBackPressed }.reversed()
|
||||
for (f in reverseOrder) {
|
||||
val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager)
|
||||
|
@ -18,26 +18,31 @@ package im.vector.riotredesign.features.home
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import arrow.core.Option
|
||||
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.MatrixCallback
|
||||
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.create.CreateRoomParams
|
||||
import im.vector.matrix.android.api.session.sync.FilterService
|
||||
import im.vector.matrix.rx.rx
|
||||
import im.vector.riotredesign.core.platform.VectorViewModel
|
||||
import im.vector.riotredesign.core.utils.LiveEvent
|
||||
import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository
|
||||
import io.reactivex.rxkotlin.subscribeBy
|
||||
import im.vector.riotredesign.features.home.group.ALL_COMMUNITIES_GROUP_ID
|
||||
import im.vector.riotredesign.features.home.group.SelectedGroupStore
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.functions.BiFunction
|
||||
import org.koin.android.ext.android.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
data class EmptyState(val isEmpty: Boolean = true) : MvRxState
|
||||
|
||||
class HomeActivityViewModel(state: EmptyState,
|
||||
private val session: Session,
|
||||
roomSelectionRepository: RoomSelectionRepository
|
||||
private val selectedGroupStore: SelectedGroupStore,
|
||||
private val homeRoomListStore: HomeRoomListObservableStore
|
||||
) : VectorViewModel<EmptyState>(state), Session.Listener {
|
||||
|
||||
companion object : MvRxViewModelFactory<HomeActivityViewModel, EmptyState> {
|
||||
@ -45,8 +50,9 @@ class HomeActivityViewModel(state: 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)
|
||||
val selectedGroupStore = viewModelContext.activity.get<SelectedGroupStore>()
|
||||
val homeRoomListObservableSource = viewModelContext.activity.get<HomeRoomListObservableStore>()
|
||||
return HomeActivityViewModel(state, session, selectedGroupStore, homeRoomListObservableSource)
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,29 +60,41 @@ class HomeActivityViewModel(state: EmptyState,
|
||||
val isLoading: LiveData<Boolean>
|
||||
get() = _isLoading
|
||||
|
||||
private val _openRoomLiveData = MutableLiveData<LiveEvent<String>>()
|
||||
val openRoomLiveData: LiveData<LiveEvent<String>>
|
||||
get() = _openRoomLiveData
|
||||
|
||||
init {
|
||||
session.addListener(this)
|
||||
val lastSelectedRoomId = roomSelectionRepository.lastSelectedRoom()
|
||||
if (lastSelectedRoomId == null || session.getRoom(lastSelectedRoomId) == null) {
|
||||
getTheFirstRoomWhenAvailable()
|
||||
} else {
|
||||
_openRoomLiveData.postValue(LiveEvent(lastSelectedRoomId))
|
||||
}
|
||||
observeRoomAndGroup()
|
||||
}
|
||||
|
||||
private fun getTheFirstRoomWhenAvailable() {
|
||||
session.rx().liveRoomSummaries()
|
||||
.filter { it.isNotEmpty() }
|
||||
.first(emptyList())
|
||||
.subscribeBy {
|
||||
val firstRoom = it.firstOrNull()
|
||||
if (firstRoom != null) {
|
||||
_openRoomLiveData.postValue(LiveEvent(firstRoom.roomId))
|
||||
}
|
||||
private fun observeRoomAndGroup() {
|
||||
Observable
|
||||
.combineLatest<List<RoomSummary>, Option<GroupSummary>, List<RoomSummary>>(
|
||||
session.rx().liveRoomSummaries().throttleLast(300, TimeUnit.MILLISECONDS),
|
||||
selectedGroupStore.observe(),
|
||||
BiFunction { rooms, selectedGroupOption ->
|
||||
val selectedGroup = selectedGroupOption.orNull()
|
||||
val filteredDirectRooms = rooms
|
||||
.filter { it.isDirect }
|
||||
.filter {
|
||||
if (selectedGroup == null || selectedGroup.groupId == ALL_COMMUNITIES_GROUP_ID) {
|
||||
true
|
||||
} else {
|
||||
it.otherMemberIds
|
||||
.intersect(selectedGroup.userIds)
|
||||
.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
val filteredGroupRooms = rooms
|
||||
.filter { !it.isDirect }
|
||||
.filter {
|
||||
selectedGroup?.groupId == ALL_COMMUNITIES_GROUP_ID
|
||||
|| selectedGroup?.roomIds?.contains(it.roomId) ?: true
|
||||
}
|
||||
filteredDirectRooms + filteredGroupRooms
|
||||
}
|
||||
)
|
||||
.subscribe {
|
||||
homeRoomListStore.post(it)
|
||||
}
|
||||
.disposeOnClear()
|
||||
}
|
||||
@ -87,8 +105,6 @@ class HomeActivityViewModel(state: EmptyState,
|
||||
session.createRoom(createRoomParams, object : MatrixCallback<String> {
|
||||
override fun onSuccess(data: String) {
|
||||
_isLoading.value = false
|
||||
// Open room id
|
||||
_openRoomLiveData.postValue(LiveEvent(data))
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
|
@ -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
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
import androidx.core.view.forEachIndexed
|
||||
import com.airbnb.mvrx.args
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationItemView
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationMenuView
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.platform.ToolbarConfigurable
|
||||
import im.vector.riotredesign.core.platform.VectorBaseFragment
|
||||
import im.vector.riotredesign.features.home.room.list.RoomListFragment
|
||||
import im.vector.riotredesign.features.home.room.list.RoomListParams
|
||||
import im.vector.riotredesign.features.home.room.list.UnreadCounterBadgeView
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.fragment_home_detail.*
|
||||
|
||||
|
||||
@Parcelize
|
||||
data class HomeDetailParams(
|
||||
val groupId: String,
|
||||
val groupName: String,
|
||||
val groupAvatar: String
|
||||
) : Parcelable
|
||||
|
||||
|
||||
private const val CURRENT_DISPLAY_MODE = "CURRENT_DISPLAY_MODE"
|
||||
|
||||
private const val INDEX_CATCHUP = 0
|
||||
private const val INDEX_PEOPLE = 1
|
||||
private const val INDEX_ROOMS = 2
|
||||
|
||||
class HomeDetailFragment : VectorBaseFragment() {
|
||||
|
||||
private val params: HomeDetailParams by args()
|
||||
private val unreadCounterBadgeViews = arrayListOf<UnreadCounterBadgeView>()
|
||||
private lateinit var currentDisplayMode: RoomListFragment.DisplayMode
|
||||
|
||||
private val viewModel: HomeDetailViewModel by fragmentViewModel()
|
||||
|
||||
override fun getLayoutResId(): Int {
|
||||
return R.layout.fragment_home_detail
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
currentDisplayMode = savedInstanceState?.getSerializable(CURRENT_DISPLAY_MODE) as? RoomListFragment.DisplayMode
|
||||
?: RoomListFragment.DisplayMode.HOME
|
||||
switchDisplayMode(currentDisplayMode)
|
||||
setupBottomNavigationView()
|
||||
setupToolbar()
|
||||
}
|
||||
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putSerializable(CURRENT_DISPLAY_MODE, currentDisplayMode)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
private fun setupToolbar() {
|
||||
val parentActivity = vectorBaseActivity
|
||||
if (parentActivity is ToolbarConfigurable) {
|
||||
parentActivity.configure(groupToolbar)
|
||||
}
|
||||
groupToolbar.title = ""
|
||||
AvatarRenderer.render(
|
||||
params.groupAvatar,
|
||||
params.groupId,
|
||||
params.groupName,
|
||||
groupToolbarAvatarImageView
|
||||
)
|
||||
groupToolbarAvatarImageView.setOnClickListener {
|
||||
vectorBaseActivity.notImplemented("Group click in toolbar")
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupBottomNavigationView() {
|
||||
bottomNavigationView.setOnNavigationItemSelectedListener {
|
||||
val displayMode = when (it.itemId) {
|
||||
R.id.bottom_action_home -> RoomListFragment.DisplayMode.HOME
|
||||
R.id.bottom_action_people -> RoomListFragment.DisplayMode.PEOPLE
|
||||
R.id.bottom_action_rooms -> RoomListFragment.DisplayMode.ROOMS
|
||||
else -> RoomListFragment.DisplayMode.HOME
|
||||
}
|
||||
if (currentDisplayMode != displayMode) {
|
||||
currentDisplayMode = displayMode
|
||||
switchDisplayMode(displayMode)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
val menuView = bottomNavigationView.getChildAt(0) as BottomNavigationMenuView
|
||||
menuView.forEachIndexed { index, view ->
|
||||
val itemView = view as BottomNavigationItemView
|
||||
val badgeLayout = LayoutInflater.from(requireContext()).inflate(R.layout.vector_home_badge_unread_layout, menuView, false)
|
||||
val unreadCounterBadgeView: UnreadCounterBadgeView = badgeLayout.findViewById(R.id.actionUnreadCounterBadgeView)
|
||||
itemView.addView(badgeLayout)
|
||||
unreadCounterBadgeViews.add(index, unreadCounterBadgeView)
|
||||
}
|
||||
}
|
||||
|
||||
private fun switchDisplayMode(displayMode: RoomListFragment.DisplayMode) {
|
||||
groupToolbarTitleView.setText(displayMode.titleRes)
|
||||
updateSelectedFragment(displayMode)
|
||||
}
|
||||
|
||||
private fun updateSelectedFragment(displayMode: RoomListFragment.DisplayMode) {
|
||||
val fragmentTag = "FRAGMENT_TAG_${displayMode.name}"
|
||||
var fragment = childFragmentManager.findFragmentByTag(fragmentTag)
|
||||
if (fragment == null) {
|
||||
fragment = RoomListFragment.newInstance(RoomListParams(displayMode))
|
||||
}
|
||||
childFragmentManager.beginTransaction()
|
||||
.replace(R.id.roomListContainer, fragment, fragmentTag)
|
||||
.addToBackStack(fragmentTag)
|
||||
.commit()
|
||||
}
|
||||
|
||||
|
||||
override fun invalidate() = withState(viewModel) {
|
||||
unreadCounterBadgeViews[INDEX_CATCHUP].render(UnreadCounterBadgeView.State(it.notificationCountCatchup, it.notificationHighlightCatchup))
|
||||
unreadCounterBadgeViews[INDEX_PEOPLE].render(UnreadCounterBadgeView.State(it.notificationCountPeople, it.notificationHighlightPeople))
|
||||
unreadCounterBadgeViews[INDEX_ROOMS].render(UnreadCounterBadgeView.State(it.notificationCountRooms, it.notificationHighlightRooms))
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance(args: HomeDetailParams): HomeDetailFragment {
|
||||
return HomeDetailFragment().apply {
|
||||
setArguments(args)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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 com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import im.vector.riotredesign.core.platform.VectorViewModel
|
||||
import org.koin.android.ext.android.get
|
||||
|
||||
/**
|
||||
* View model used to update the home bottom bar notification counts
|
||||
*/
|
||||
class HomeDetailViewModel(initialState: HomeDetailViewState,
|
||||
private val homeRoomListStore: HomeRoomListObservableStore)
|
||||
: VectorViewModel<HomeDetailViewState>(initialState) {
|
||||
|
||||
companion object : MvRxViewModelFactory<HomeDetailViewModel, HomeDetailViewState> {
|
||||
|
||||
@JvmStatic
|
||||
override fun create(viewModelContext: ViewModelContext, state: HomeDetailViewState): HomeDetailViewModel? {
|
||||
val homeRoomListStore = viewModelContext.activity.get<HomeRoomListObservableStore>()
|
||||
return HomeDetailViewModel(state, homeRoomListStore)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
observeRoomSummaries()
|
||||
}
|
||||
|
||||
// PRIVATE METHODS *****************************************************************************
|
||||
|
||||
private fun observeRoomSummaries() {
|
||||
homeRoomListStore
|
||||
.observe()
|
||||
.subscribe { list ->
|
||||
list.let { summaries ->
|
||||
val peopleNotifications = summaries
|
||||
.filter { it.isDirect }
|
||||
.map { it.notificationCount }
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.reduce { acc, i -> acc + i }
|
||||
?: 0
|
||||
val peopleHasHighlight = summaries
|
||||
.filter { it.isDirect }
|
||||
.any { it.highlightCount > 0 }
|
||||
|
||||
val roomsNotifications = summaries
|
||||
.filter { !it.isDirect }
|
||||
.map { it.notificationCount }
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.reduce { acc, i -> acc + i }
|
||||
?: 0
|
||||
val roomsHasHighlight = summaries
|
||||
.filter { !it.isDirect }
|
||||
.any { it.highlightCount > 0 }
|
||||
|
||||
setState {
|
||||
copy(
|
||||
notificationCountCatchup = peopleNotifications + roomsNotifications,
|
||||
notificationHighlightCatchup = peopleHasHighlight || roomsHasHighlight,
|
||||
notificationCountPeople = peopleNotifications,
|
||||
notificationHighlightPeople = peopleHasHighlight,
|
||||
notificationCountRooms = roomsNotifications,
|
||||
notificationHighlightRooms = roomsHasHighlight
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.disposeOnClear()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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 com.airbnb.mvrx.MvRxState
|
||||
|
||||
data class HomeDetailViewState(
|
||||
val notificationCountCatchup: Int = 0,
|
||||
val notificationHighlightCatchup: Boolean = false,
|
||||
val notificationCountPeople: Int = 0,
|
||||
val notificationHighlightPeople: Boolean = false,
|
||||
val notificationCountRooms: Int = 0,
|
||||
val notificationHighlightRooms: Boolean = false
|
||||
) : MvRxState
|
@ -17,11 +17,14 @@
|
||||
package im.vector.riotredesign.features.home
|
||||
|
||||
import android.os.Bundle
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.extensions.observeK
|
||||
import im.vector.riotredesign.core.extensions.replaceChildFragment
|
||||
import im.vector.riotredesign.core.platform.VectorBaseFragment
|
||||
import im.vector.riotredesign.features.home.group.GroupListFragment
|
||||
import im.vector.riotredesign.features.home.room.list.RoomListFragment
|
||||
import kotlinx.android.synthetic.main.fragment_home_drawer.*
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class HomeDrawerFragment : VectorBaseFragment() {
|
||||
|
||||
@ -32,16 +35,31 @@ class HomeDrawerFragment : VectorBaseFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
val session by inject<Session>()
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_home_drawer
|
||||
|
||||
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)
|
||||
replaceChildFragment(groupListFragment, R.id.homeDrawerGroupListContainer)
|
||||
}
|
||||
|
||||
session.observeUser(session.sessionParams.credentials.userId).observeK(this) { user ->
|
||||
if (user != null) {
|
||||
AvatarRenderer.render(user.avatarUrl, user.userId, user.displayName, homeDrawerHeaderAvatarView)
|
||||
homeDrawerUsernameView.text = user.displayName
|
||||
homeDrawerUserIdView.text = user.userId
|
||||
}
|
||||
}
|
||||
homeDrawerHeaderSettingsView.setOnClickListener {
|
||||
navigator.openSettings()
|
||||
}
|
||||
|
||||
// Debug menu
|
||||
homeDrawerHeaderDebugView.setOnClickListener {
|
||||
navigator.openDebug()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -18,6 +18,7 @@ package im.vector.riotredesign.features.home
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.vector.riotredesign.core.glide.GlideApp
|
||||
import im.vector.riotredesign.core.resources.ColorProvider
|
||||
import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandController
|
||||
import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandPresenter
|
||||
import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserController
|
||||
@ -25,10 +26,12 @@ import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresent
|
||||
import im.vector.riotredesign.features.home.group.GroupSummaryController
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.factory.*
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.format.NoticeEventFormatter
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||
import im.vector.riotredesign.features.home.room.list.RoomSummaryController
|
||||
import im.vector.riotredesign.features.html.EventHtmlRenderer
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.koin.dsl.module.module
|
||||
|
||||
class HomeModule {
|
||||
@ -36,8 +39,6 @@ class HomeModule {
|
||||
companion object {
|
||||
const val HOME_SCOPE = "HOME_SCOPE"
|
||||
const val ROOM_DETAIL_SCOPE = "ROOM_DETAIL_SCOPE"
|
||||
const val ROOM_LIST_SCOPE = "ROOM_LIST_SCOPE"
|
||||
const val GROUP_LIST_SCOPE = "GROUP_LIST_SCOPE"
|
||||
}
|
||||
|
||||
val definition = module {
|
||||
@ -49,32 +50,38 @@ class HomeModule {
|
||||
}
|
||||
|
||||
scope(HOME_SCOPE) {
|
||||
HomePermalinkHandler(get())
|
||||
HomePermalinkHandler(get(), get())
|
||||
}
|
||||
|
||||
// Fragment scopes
|
||||
|
||||
factory { (fragment: Fragment) ->
|
||||
val eventHtmlRenderer = EventHtmlRenderer(GlideApp.with(fragment), fragment.requireContext(), get())
|
||||
val timelineDateFormatter = TimelineDateFormatter(get())
|
||||
val timelineMediaSizeProvider = TimelineMediaSizeProvider()
|
||||
val messageItemFactory = MessageItemFactory(get(), timelineMediaSizeProvider, timelineDateFormatter, eventHtmlRenderer)
|
||||
factory {
|
||||
TimelineDateFormatter(get())
|
||||
}
|
||||
|
||||
val timelineItemFactory = TimelineItemFactory(messageItemFactory = messageItemFactory,
|
||||
roomNameItemFactory = RoomNameItemFactory(get()),
|
||||
roomTopicItemFactory = RoomTopicItemFactory(get()),
|
||||
roomMemberItemFactory = RoomMemberItemFactory(get()),
|
||||
roomHistoryVisibilityItemFactory = RoomHistoryVisibilityItemFactory(get()),
|
||||
callItemFactory = CallItemFactory(get()),
|
||||
factory {
|
||||
NoticeEventFormatter(get())
|
||||
}
|
||||
|
||||
factory { (fragment: Fragment) ->
|
||||
val colorProvider = ColorProvider(fragment.requireContext())
|
||||
val timelineDateFormatter = get<TimelineDateFormatter>()
|
||||
val eventHtmlRenderer = EventHtmlRenderer(GlideApp.with(fragment), fragment.requireContext(), get())
|
||||
val noticeEventFormatter = get<NoticeEventFormatter>(parameters = { parametersOf(fragment) })
|
||||
val timelineMediaSizeProvider = TimelineMediaSizeProvider()
|
||||
val messageItemFactory = MessageItemFactory(colorProvider, timelineMediaSizeProvider, timelineDateFormatter, eventHtmlRenderer, get())
|
||||
val timelineItemFactory = TimelineItemFactory(
|
||||
messageItemFactory = messageItemFactory,
|
||||
noticeItemFactory = NoticeItemFactory(noticeEventFormatter),
|
||||
defaultItemFactory = DefaultItemFactory(),
|
||||
encryptionItemFactory = EncryptionItemFactory(get()),
|
||||
encryptedItemFactory = EncryptedItemFactory(get()),
|
||||
defaultItemFactory = DefaultItemFactory()
|
||||
encryptedItemFactory = EncryptedItemFactory(get())
|
||||
)
|
||||
TimelineEventController(timelineDateFormatter, timelineItemFactory, timelineMediaSizeProvider)
|
||||
}
|
||||
|
||||
factory {
|
||||
RoomSummaryController(get())
|
||||
RoomSummaryController(get(), get(), get())
|
||||
}
|
||||
|
||||
factory {
|
||||
|
@ -18,11 +18,10 @@ package im.vector.riotredesign.features.home
|
||||
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
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 im.vector.riotredesign.features.navigation.Navigator
|
||||
import kotlinx.android.synthetic.main.activity_home.*
|
||||
import timber.log.Timber
|
||||
|
||||
@ -32,22 +31,24 @@ class HomeNavigator {
|
||||
|
||||
private var rootRoomId: String? = null
|
||||
|
||||
fun openSelectedGroup(groupSummary: GroupSummary) {
|
||||
Timber.v("Open selected group ${groupSummary.groupId}")
|
||||
activity?.let {
|
||||
val args = HomeDetailParams(groupSummary.groupId, groupSummary.displayName, groupSummary.avatarUrl)
|
||||
val homeDetailFragment = HomeDetailFragment.newInstance(args)
|
||||
it.drawerLayout?.closeDrawer(GravityCompat.START)
|
||||
it.replaceFragment(homeDetailFragment, R.id.homeDetailFragmentContainer)
|
||||
}
|
||||
}
|
||||
|
||||
fun openRoomDetail(roomId: String,
|
||||
eventId: String?,
|
||||
addToBackstack: Boolean = false) {
|
||||
Timber.v("Open room detail $roomId - $eventId - $addToBackstack")
|
||||
navigator: Navigator) {
|
||||
Timber.v("Open room detail $roomId - $eventId")
|
||||
activity?.let {
|
||||
//TODO enable eventId permalink. It doesn't work enough at the moment.
|
||||
val args = RoomDetailArgs(roomId)
|
||||
val roomDetailFragment = RoomDetailFragment.newInstance(args)
|
||||
it.drawerLayout?.closeDrawer(GravityCompat.START)
|
||||
if (addToBackstack) {
|
||||
it.addFragmentToBackstack(roomDetailFragment, R.id.homeDetailFragmentContainer, roomId)
|
||||
} else {
|
||||
rootRoomId = roomId
|
||||
clearBackStack(it.supportFragmentManager)
|
||||
it.replaceFragment(roomDetailFragment, R.id.homeDetailFragmentContainer)
|
||||
}
|
||||
navigator.openRoom(roomId)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,8 +19,10 @@ 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
|
||||
import im.vector.riotredesign.features.navigation.Navigator
|
||||
|
||||
class HomePermalinkHandler(private val navigator: HomeNavigator) {
|
||||
class HomePermalinkHandler(private val homeNavigator: HomeNavigator,
|
||||
private val navigator: Navigator) {
|
||||
|
||||
fun launch(deepLink: String?) {
|
||||
val uri = deepLink?.let { Uri.parse(it) }
|
||||
@ -34,16 +36,16 @@ class HomePermalinkHandler(private val navigator: HomeNavigator) {
|
||||
val permalinkData = PermalinkParser.parse(deepLink)
|
||||
when (permalinkData) {
|
||||
is PermalinkData.EventLink -> {
|
||||
navigator.openRoomDetail(permalinkData.roomIdOrAlias, permalinkData.eventId, true)
|
||||
homeNavigator.openRoomDetail(permalinkData.roomIdOrAlias, permalinkData.eventId, navigator)
|
||||
}
|
||||
is PermalinkData.RoomLink -> {
|
||||
navigator.openRoomDetail(permalinkData.roomIdOrAlias, null, true)
|
||||
homeNavigator.openRoomDetail(permalinkData.roomIdOrAlias, null, navigator)
|
||||
}
|
||||
is PermalinkData.GroupLink -> {
|
||||
navigator.openGroupDetail(permalinkData.groupId)
|
||||
homeNavigator.openGroupDetail(permalinkData.groupId)
|
||||
}
|
||||
is PermalinkData.UserLink -> {
|
||||
navigator.openUserDetail(permalinkData.userId)
|
||||
homeNavigator.openUserDetail(permalinkData.userId)
|
||||
}
|
||||
is PermalinkData.FallbackLink -> {
|
||||
|
||||
|
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home
|
||||
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.riotredesign.core.utils.RxStore
|
||||
import im.vector.riotredesign.features.home.room.list.RoomListDisplayModeFilter
|
||||
import im.vector.riotredesign.features.home.room.list.RoomListFragment
|
||||
import io.reactivex.Observable
|
||||
|
||||
class HomeRoomListObservableStore : RxStore<List<RoomSummary>>() {
|
||||
|
||||
fun observeFilteredBy(displayMode: RoomListFragment.DisplayMode): Observable<List<RoomSummary>> {
|
||||
return observe()
|
||||
.flatMapSingle {
|
||||
Observable.fromIterable(it).filter(RoomListDisplayModeFilter(displayMode)).toList()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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
|
||||
|
||||
import android.graphics.drawable.AnimationDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.platform.VectorBaseFragment
|
||||
import kotlinx.android.synthetic.main.fragment_loading.*
|
||||
|
||||
class LoadingFragment : VectorBaseFragment() {
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance(): LoadingFragment {
|
||||
return LoadingFragment()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_loading
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val background = animatedLogoImageView.background
|
||||
if (background is AnimationDrawable) {
|
||||
background.start()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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
|
||||
|
||||
import androidx.annotation.ColorRes
|
||||
import im.vector.riotredesign.R
|
||||
|
||||
|
||||
@ColorRes
|
||||
fun getColorFromUserId(userId: String?): Int {
|
||||
if (userId.isNullOrBlank()) {
|
||||
return R.color.riotx_username_1
|
||||
}
|
||||
|
||||
var hash = 0
|
||||
var i = 0
|
||||
var chr: Char
|
||||
|
||||
while (i < userId.length) {
|
||||
chr = userId[i]
|
||||
hash = (hash shl 5) - hash + chr.toInt()
|
||||
i++
|
||||
}
|
||||
|
||||
return when (Math.abs(hash) % 8 + 1) {
|
||||
1 -> R.color.riotx_username_1
|
||||
2 -> R.color.riotx_username_2
|
||||
3 -> R.color.riotx_username_3
|
||||
4 -> R.color.riotx_username_4
|
||||
5 -> R.color.riotx_username_5
|
||||
6 -> R.color.riotx_username_6
|
||||
7 -> R.color.riotx_username_7
|
||||
else -> R.color.riotx_username_8
|
||||
}
|
||||
}
|
@ -22,13 +22,12 @@ import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.extensions.observeEvent
|
||||
import im.vector.riotredesign.core.platform.StateView
|
||||
import im.vector.riotredesign.core.platform.VectorBaseFragment
|
||||
import im.vector.riotredesign.features.home.HomeModule
|
||||
import im.vector.riotredesign.features.home.HomeNavigator
|
||||
import kotlinx.android.synthetic.main.fragment_group_list.*
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.android.scope.ext.android.bindScope
|
||||
import org.koin.android.scope.ext.android.getOrCreateScope
|
||||
|
||||
class GroupListFragment : VectorBaseFragment(), GroupSummaryController.Callback {
|
||||
|
||||
@ -39,17 +38,20 @@ class GroupListFragment : VectorBaseFragment(), GroupSummaryController.Callback
|
||||
}
|
||||
|
||||
private val viewModel: GroupListViewModel by fragmentViewModel()
|
||||
private val homeNavigator by inject<HomeNavigator>()
|
||||
private val groupController by inject<GroupSummaryController>()
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_group_list
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
bindScope(getOrCreateScope(HomeModule.GROUP_LIST_SCOPE))
|
||||
groupController.callback = this
|
||||
stateView.contentView = epoxyRecyclerView
|
||||
epoxyRecyclerView.setController(groupController)
|
||||
viewModel.subscribe { renderState(it) }
|
||||
viewModel.openGroupLiveData.observeEvent(this) {
|
||||
homeNavigator.openSelectedGroup(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderState(state: GroupListViewState) {
|
||||
|
@ -16,17 +16,26 @@
|
||||
|
||||
package im.vector.riotredesign.features.home.group
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import arrow.core.Option
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
import im.vector.matrix.rx.rx
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.platform.VectorViewModel
|
||||
import im.vector.riotredesign.core.resources.StringProvider
|
||||
import im.vector.riotredesign.core.utils.LiveEvent
|
||||
import org.koin.android.ext.android.get
|
||||
|
||||
const val ALL_COMMUNITIES_GROUP_ID = "ALL_COMMUNITIES_GROUP_ID"
|
||||
|
||||
class GroupListViewModel(initialState: GroupListViewState,
|
||||
private val selectedGroupHolder: SelectedGroupStore,
|
||||
private val session: Session
|
||||
private val session: Session,
|
||||
private val stringProvider: StringProvider
|
||||
) : VectorViewModel<GroupListViewState>(initialState) {
|
||||
|
||||
companion object : MvRxViewModelFactory<GroupListViewModel, GroupListViewState> {
|
||||
@ -35,19 +44,27 @@ class GroupListViewModel(initialState: GroupListViewState,
|
||||
override fun create(viewModelContext: ViewModelContext, state: GroupListViewState): GroupListViewModel? {
|
||||
val currentSession = viewModelContext.activity.get<Session>()
|
||||
val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupStore>()
|
||||
return GroupListViewModel(state, selectedGroupHolder, currentSession)
|
||||
val stringProvider = viewModelContext.activity.get<StringProvider>()
|
||||
return GroupListViewModel(state, selectedGroupHolder, currentSession, stringProvider)
|
||||
}
|
||||
}
|
||||
|
||||
private val _openGroupLiveData = MutableLiveData<LiveEvent<GroupSummary>>()
|
||||
val openGroupLiveData: LiveData<LiveEvent<GroupSummary>>
|
||||
get() = _openGroupLiveData
|
||||
|
||||
init {
|
||||
observeGroupSummaries()
|
||||
observeState()
|
||||
observeSelectionState()
|
||||
}
|
||||
|
||||
private fun observeState() {
|
||||
subscribe {
|
||||
val selectedGroup = Option.fromNullable(it.selectedGroup)
|
||||
selectedGroupHolder.post(selectedGroup)
|
||||
private fun observeSelectionState() {
|
||||
selectSubscribe(GroupListViewState::selectedGroup) {
|
||||
if (it != null) {
|
||||
_openGroupLiveData.postValue(LiveEvent(it))
|
||||
val optionGroup = Option.fromNullable(it)
|
||||
selectedGroupHolder.post(optionGroup)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,17 +79,23 @@ class GroupListViewModel(initialState: GroupListViewState,
|
||||
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()
|
||||
.map {
|
||||
val myUser = session.getUser(session.sessionParams.credentials.userId)
|
||||
val allCommunityGroup = GroupSummary(
|
||||
groupId = ALL_COMMUNITIES_GROUP_ID,
|
||||
displayName = stringProvider.getString(R.string.group_all_communities),
|
||||
avatarUrl = myUser?.avatarUrl ?: "")
|
||||
listOf(allCommunityGroup) + it
|
||||
}
|
||||
.execute { async ->
|
||||
copy(asyncGroups = async)
|
||||
val newSelectedGroup = selectedGroup ?: async()?.firstOrNull()
|
||||
copy(asyncGroups = async, selectedGroup = newSelectedGroup)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,12 +17,13 @@
|
||||
package im.vector.riotredesign.features.home.group
|
||||
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.riotredesign.core.platform.CheckableFrameLayout
|
||||
import im.vector.riotredesign.core.platform.CheckableConstraintLayout
|
||||
import im.vector.riotredesign.features.home.AvatarRenderer
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_group)
|
||||
@ -36,14 +37,16 @@ abstract class GroupSummaryItem : VectorEpoxyModel<GroupSummaryItem.Holder>() {
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
holder.rootView.isSelected = selected
|
||||
holder.rootView.setOnClickListener { listener?.invoke() }
|
||||
holder.groupNameView.text = groupName
|
||||
holder.rootView.isChecked = selected
|
||||
AvatarRenderer.render(avatarUrl, groupId, groupName.toString(), holder.avatarImageView)
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
val avatarImageView by bind<ImageView>(R.id.groupAvatarImageView)
|
||||
val rootView by bind<CheckableFrameLayout>(R.id.itemGroupLayout)
|
||||
val groupNameView by bind<TextView>(R.id.groupNameView)
|
||||
val rootView by bind<CheckableConstraintLayout>(R.id.itemGroupLayout)
|
||||
}
|
||||
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail
|
||||
|
||||
import android.graphics.drawable.AnimationDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.platform.VectorBaseFragment
|
||||
import kotlinx.android.synthetic.main.fragment_loading_room_detail.*
|
||||
|
||||
class LoadingRoomDetailFragment : VectorBaseFragment() {
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance(): LoadingRoomDetailFragment {
|
||||
return LoadingRoomDetailFragment()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_loading_room_detail
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val background = animatedLogoImageView.background
|
||||
if (background is AnimationDrawable) {
|
||||
background.start()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -17,15 +17,27 @@
|
||||
package im.vector.riotredesign.features.home.room.detail
|
||||
|
||||
import com.jaiselrahman.filepicker.model.MediaFile
|
||||
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
|
||||
sealed class RoomDetailActions {
|
||||
|
||||
data class SendMessage(val text: String) : RoomDetailActions()
|
||||
data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions()
|
||||
data class SendMedia(val mediaFiles: List<MediaFile>) : RoomDetailActions()
|
||||
object IsDisplayed : RoomDetailActions()
|
||||
data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions()
|
||||
data class LoadMore(val direction: Timeline.Direction) : RoomDetailActions()
|
||||
data class SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions()
|
||||
data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions()
|
||||
data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions()
|
||||
data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val opposite: String) : RoomDetailActions()
|
||||
data class ShowEditHistoryAction(val event: String, val editAggregatedSummary: EditAggregatedSummary) : RoomDetailActions()
|
||||
object AcceptInvite : RoomDetailActions()
|
||||
object RejectInvite : RoomDetailActions()
|
||||
|
||||
data class EnterEditMode(val eventId: String) : RoomDetailActions()
|
||||
data class EnterQuoteMode(val eventId: String) : RoomDetailActions()
|
||||
data class EnterReplyMode(val eventId: String) : RoomDetailActions()
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
/*
|
||||
*
|
||||
* * 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.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.extensions.replaceFragment
|
||||
import im.vector.riotredesign.core.platform.ToolbarConfigurable
|
||||
import im.vector.riotredesign.core.platform.VectorBaseActivity
|
||||
|
||||
class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.activity_room_detail
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (isFirstCreation()) {
|
||||
val roomDetailArgs: RoomDetailArgs = intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS)
|
||||
?: return
|
||||
val roomDetailFragment = RoomDetailFragment.newInstance(roomDetailArgs)
|
||||
replaceFragment(roomDetailFragment, R.id.roomDetailContainer)
|
||||
}
|
||||
}
|
||||
|
||||
override fun configure(toolbar: Toolbar) {
|
||||
configureToolbar(toolbar)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val EXTRA_ROOM_DETAIL_ARGS = "EXTRA_ROOM_DETAIL_ARGS"
|
||||
|
||||
fun newIntent(context: Context, roomDetailArgs: RoomDetailArgs): Intent {
|
||||
return Intent(context, RoomDetailActivity::class.java).apply {
|
||||
putExtra(EXTRA_ROOM_DETAIL_ARGS, roomDetailArgs)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -40,8 +40,9 @@ import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import butterknife.BindView
|
||||
import com.airbnb.epoxy.EpoxyVisibilityTracker
|
||||
import com.airbnb.mvrx.MvRx
|
||||
import com.airbnb.mvrx.args
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.github.piasy.biv.BigImageViewer
|
||||
import com.github.piasy.biv.loader.ImageLoader
|
||||
@ -53,15 +54,18 @@ import com.otaliastudios.autocomplete.Autocomplete
|
||||
import com.otaliastudios.autocomplete.AutocompleteCallback
|
||||
import com.otaliastudios.autocomplete.CharPolicy
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.api.session.room.model.message.*
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.dialogs.DialogListItem
|
||||
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
|
||||
import im.vector.riotredesign.core.extensions.hideKeyboard
|
||||
import im.vector.riotredesign.core.extensions.observeEvent
|
||||
import im.vector.riotredesign.core.glide.GlideApp
|
||||
import im.vector.riotredesign.core.platform.ToolbarConfigurable
|
||||
import im.vector.riotredesign.core.platform.VectorBaseFragment
|
||||
import im.vector.riotredesign.core.utils.*
|
||||
import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandPresenter
|
||||
@ -71,7 +75,9 @@ import im.vector.riotredesign.features.command.Command
|
||||
import im.vector.riotredesign.features.home.AvatarRenderer
|
||||
import im.vector.riotredesign.features.home.HomeModule
|
||||
import im.vector.riotredesign.features.home.HomePermalinkHandler
|
||||
import im.vector.riotredesign.features.home.getColorFromUserId
|
||||
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerActions
|
||||
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerView
|
||||
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewModel
|
||||
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewState
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||
@ -81,17 +87,23 @@ import im.vector.riotredesign.features.home.room.detail.timeline.action.MessageM
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import im.vector.riotredesign.features.html.PillImageSpan
|
||||
import im.vector.riotredesign.features.invite.VectorInviteView
|
||||
import im.vector.riotredesign.features.media.ImageContentRenderer
|
||||
import im.vector.riotredesign.features.media.ImageMediaViewerActivity
|
||||
import im.vector.riotredesign.features.media.VideoContentRenderer
|
||||
import im.vector.riotredesign.features.media.VideoMediaViewerActivity
|
||||
import im.vector.riotredesign.features.reactions.EmojiReactionPickerActivity
|
||||
import im.vector.riotredesign.features.settings.PreferencesManager
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.fragment_room_detail.*
|
||||
import kotlinx.android.synthetic.main.merge_composer_layout.view.*
|
||||
import org.commonmark.parser.Parser
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.android.scope.ext.android.bindScope
|
||||
import org.koin.android.scope.ext.android.getOrCreateScope
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import ru.noties.markwon.Markwon
|
||||
import ru.noties.markwon.html.HtmlPlugin
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
@ -106,11 +118,13 @@ data class RoomDetailArgs(
|
||||
private const val CAMERA_VALUE_TITLE = "attachment"
|
||||
private const val REQUEST_FILES_REQUEST_CODE = 0
|
||||
private const val TAKE_IMAGE_REQUEST_CODE = 1
|
||||
private const val REACTION_SELECT_REQUEST_CODE = 2
|
||||
|
||||
class RoomDetailFragment :
|
||||
VectorBaseFragment(),
|
||||
TimelineEventController.Callback,
|
||||
AutocompleteUserPresenter.Callback {
|
||||
AutocompleteUserPresenter.Callback,
|
||||
VectorInviteView.Callback {
|
||||
|
||||
companion object {
|
||||
|
||||
@ -127,13 +141,12 @@ class RoomDetailFragment :
|
||||
* @return the sanitized display name
|
||||
*/
|
||||
fun sanitizeDisplayname(displayName: String): String? {
|
||||
var displayName = displayName
|
||||
// sanity checks
|
||||
if (!TextUtils.isEmpty(displayName)) {
|
||||
val ircPattern = " (IRC)"
|
||||
|
||||
if (displayName.endsWith(ircPattern)) {
|
||||
displayName = displayName.substring(0, displayName.length - ircPattern.length)
|
||||
return displayName.substring(0, displayName.length - ircPattern.length)
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,6 +154,7 @@ class RoomDetailFragment :
|
||||
}
|
||||
}
|
||||
|
||||
private val roomDetailArgs: RoomDetailArgs by args()
|
||||
private val session by inject<Session>()
|
||||
private val glideRequests by lazy {
|
||||
GlideApp.with(this)
|
||||
@ -149,6 +163,7 @@ class RoomDetailFragment :
|
||||
private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
|
||||
private val textComposerViewModel: TextComposerViewModel by fragmentViewModel()
|
||||
private val timelineEventController: TimelineEventController by inject { parametersOf(this) }
|
||||
private val commandAutocompletePolicy = CommandAutocompletePolicy()
|
||||
private val autocompleteCommandPresenter: AutocompleteCommandPresenter by inject { parametersOf(this) }
|
||||
private val autocompleteUserPresenter: AutocompleteUserPresenter by inject { parametersOf(this) }
|
||||
private val homePermalinkHandler: HomePermalinkHandler by inject()
|
||||
@ -159,21 +174,101 @@ class RoomDetailFragment :
|
||||
|
||||
private lateinit var actionViewModel: ActionsHandler
|
||||
|
||||
@BindView(R.id.composerLayout)
|
||||
lateinit var composerLayout: TextComposerView
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
|
||||
bindScope(getOrCreateScope(HomeModule.ROOM_DETAIL_SCOPE))
|
||||
setupToolbar(roomToolbar)
|
||||
setupRecyclerView()
|
||||
setupToolbar()
|
||||
setupComposer()
|
||||
setupAttachmentButton()
|
||||
setupInviteView()
|
||||
roomDetailViewModel.subscribe { renderState(it) }
|
||||
textComposerViewModel.subscribe { renderTextComposerState(it) }
|
||||
roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) }
|
||||
|
||||
roomDetailViewModel.nonBlockingPopAlert.observe(this, Observer { liveEvent ->
|
||||
liveEvent.getContentIfNotHandled()?.let {
|
||||
val message = requireContext().getString(it.first, *it.second.toTypedArray())
|
||||
showSnackWithMessage(message, Snackbar.LENGTH_LONG)
|
||||
}
|
||||
})
|
||||
actionViewModel.actionCommandEvent.observe(this, Observer {
|
||||
handleActions(it)
|
||||
})
|
||||
|
||||
roomDetailViewModel.selectSubscribe(
|
||||
RoomDetailViewState::sendMode,
|
||||
RoomDetailViewState::selectedEvent,
|
||||
RoomDetailViewState::roomId) { mode, event, roomId ->
|
||||
when (mode) {
|
||||
SendMode.REGULAR -> {
|
||||
commandAutocompletePolicy.enabled = true
|
||||
val uid = session.sessionParams.credentials.userId
|
||||
val meMember = session.getRoom(roomId)?.getRoomMember(uid)
|
||||
AvatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composerLayout.composerAvatarImageView)
|
||||
composerLayout.collapse()
|
||||
}
|
||||
SendMode.EDIT,
|
||||
SendMode.QUOTE,
|
||||
SendMode.REPLY -> {
|
||||
commandAutocompletePolicy.enabled = false
|
||||
if (event == null) {
|
||||
//we should ignore? can this happen?
|
||||
Timber.e("Enter edit mode with no event selected")
|
||||
return@selectSubscribe
|
||||
}
|
||||
//switch to expanded bar
|
||||
composerLayout.composerRelatedMessageTitle.apply {
|
||||
text = event.senderName
|
||||
setTextColor(ContextCompat.getColor(requireContext(), getColorFromUserId(event.root.sender)))
|
||||
}
|
||||
|
||||
//TODO this is used at several places, find way to refactor?
|
||||
val messageContent: MessageContent? =
|
||||
event.annotations?.editSummary?.aggregatedContent?.toModel()
|
||||
?: event.root.content.toModel()
|
||||
val nonFormattedBody = messageContent?.body ?: ""
|
||||
var formattedBody: CharSequence? = null
|
||||
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
|
||||
val parser = Parser.builder().build()
|
||||
val document = parser.parse(messageContent.formattedBody ?: messageContent.body)
|
||||
formattedBody = Markwon.builder(requireContext())
|
||||
.usePlugin(HtmlPlugin.create()).build().render(document)
|
||||
}
|
||||
composerLayout.composerRelatedMessageContent.text = formattedBody ?: nonFormattedBody
|
||||
|
||||
|
||||
if (mode == SendMode.EDIT) {
|
||||
//TODO if it's a reply we should trim the top part of message
|
||||
composerLayout.composerEditText.setText(nonFormattedBody)
|
||||
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_edit))
|
||||
} else if (mode == SendMode.QUOTE) {
|
||||
composerLayout.composerEditText.setText("")
|
||||
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_quote))
|
||||
} else if (mode == SendMode.REPLY) {
|
||||
composerLayout.composerEditText.setText("")
|
||||
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_reply))
|
||||
}
|
||||
|
||||
AvatarRenderer.render(event.senderAvatar, event.root.sender
|
||||
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
|
||||
|
||||
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
|
||||
composerLayout.expand {
|
||||
focusComposerAndShowKeyboard()
|
||||
}
|
||||
composerLayout.composerRelatedMessageCloseButton.setOnClickListener {
|
||||
composerLayout.composerEditText.setText("")
|
||||
roomDetailViewModel.resetSendMode()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
@ -181,24 +276,20 @@ class RoomDetailFragment :
|
||||
if (resultCode == RESULT_OK && data != null) {
|
||||
when (requestCode) {
|
||||
REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data)
|
||||
REACTION_SELECT_REQUEST_CODE -> {
|
||||
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
|
||||
?: return
|
||||
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
|
||||
?: return
|
||||
//TODO check if already reacted with that?
|
||||
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
roomDetailViewModel.process(RoomDetailActions.IsDisplayed)
|
||||
}
|
||||
|
||||
// PRIVATE METHODS *****************************************************************************
|
||||
|
||||
private fun setupToolbar() {
|
||||
val parentActivity = vectorBaseActivity
|
||||
if (parentActivity is ToolbarConfigurable) {
|
||||
parentActivity.configure(toolbar)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
val epoxyVisibilityTracker = EpoxyVisibilityTracker()
|
||||
epoxyVisibilityTracker.attach(recyclerView)
|
||||
@ -224,8 +315,8 @@ class RoomDetailFragment :
|
||||
private fun setupComposer() {
|
||||
val elevation = 6f
|
||||
val backgroundDrawable = ColorDrawable(Color.WHITE)
|
||||
Autocomplete.on<Command>(composerEditText)
|
||||
.with(CommandAutocompletePolicy())
|
||||
Autocomplete.on<Command>(composerLayout.composerEditText)
|
||||
.with(commandAutocompletePolicy)
|
||||
.with(autocompleteCommandPresenter)
|
||||
.with(elevation)
|
||||
.with(backgroundDrawable)
|
||||
@ -244,7 +335,7 @@ class RoomDetailFragment :
|
||||
.build()
|
||||
|
||||
autocompleteUserPresenter.callback = this
|
||||
Autocomplete.on<User>(composerEditText)
|
||||
Autocomplete.on<User>(composerLayout.composerEditText)
|
||||
.with(CharPolicy('@', true))
|
||||
.with(autocompleteUserPresenter)
|
||||
.with(elevation)
|
||||
@ -272,7 +363,7 @@ class RoomDetailFragment :
|
||||
// Add the span
|
||||
val user = session.getUser(item.userId)
|
||||
val span = PillImageSpan(glideRequests, context!!, item.userId, user)
|
||||
span.bind(composerEditText)
|
||||
span.bind(composerLayout.composerEditText)
|
||||
|
||||
editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
|
||||
@ -284,16 +375,16 @@ class RoomDetailFragment :
|
||||
})
|
||||
.build()
|
||||
|
||||
sendButton.setOnClickListener {
|
||||
val textMessage = composerEditText.text.toString()
|
||||
composerLayout.sendButton.setOnClickListener {
|
||||
val textMessage = composerLayout.composerEditText.text.toString()
|
||||
if (textMessage.isNotBlank()) {
|
||||
roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage))
|
||||
roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage, PreferencesManager.isMarkdownEnabled(requireContext())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupAttachmentButton() {
|
||||
attachmentButton.setOnClickListener {
|
||||
composerLayout.attachmentButton.setOnClickListener {
|
||||
val intent = Intent(requireContext(), FilePickerActivity::class.java)
|
||||
intent.putExtra(FilePickerActivity.CONFIGS, Configurations.Builder()
|
||||
.setCheckPermission(true)
|
||||
@ -334,27 +425,31 @@ class RoomDetailFragment :
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupInviteView() {
|
||||
inviteView.callback = this
|
||||
}
|
||||
|
||||
private fun onSendChoiceClicked(dialogListItem: DialogListItem) {
|
||||
Timber.v("On send choice clicked: $dialogListItem")
|
||||
when (dialogListItem) {
|
||||
is DialogListItem.SendFile -> {
|
||||
is DialogListItem.SendFile -> {
|
||||
// launchFileIntent
|
||||
}
|
||||
is DialogListItem.SendVoice -> {
|
||||
is DialogListItem.SendVoice -> {
|
||||
//launchAudioRecorderIntent()
|
||||
}
|
||||
is DialogListItem.SendSticker -> {
|
||||
is DialogListItem.SendSticker -> {
|
||||
//startStickerPickerActivity()
|
||||
}
|
||||
is DialogListItem.TakePhotoVideo ->
|
||||
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) {
|
||||
// launchCamera()
|
||||
}
|
||||
is DialogListItem.TakePhoto ->
|
||||
is DialogListItem.TakePhoto ->
|
||||
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA)) {
|
||||
openCamera(requireActivity(), CAMERA_VALUE_TITLE, TAKE_IMAGE_REQUEST_CODE)
|
||||
}
|
||||
is DialogListItem.TakeVideo ->
|
||||
is DialogListItem.TakeVideo ->
|
||||
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA)) {
|
||||
// launchNativeVideoRecorder()
|
||||
}
|
||||
@ -368,18 +463,33 @@ class RoomDetailFragment :
|
||||
|
||||
private fun renderState(state: RoomDetailViewState) {
|
||||
renderRoomSummary(state)
|
||||
timelineEventController.setTimeline(state.timeline)
|
||||
val summary = state.asyncRoomSummary()
|
||||
val inviter = state.asyncInviter()
|
||||
if (summary?.membership == Membership.JOIN) {
|
||||
timelineEventController.setTimeline(state.timeline)
|
||||
inviteView.visibility = View.GONE
|
||||
|
||||
val uid = session.sessionParams.credentials.userId
|
||||
val meMember = session.getRoom(state.roomId)?.getRoomMember(uid)
|
||||
AvatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composerLayout.composerAvatarImageView)
|
||||
|
||||
} else if (summary?.membership == Membership.INVITE && inviter != null) {
|
||||
inviteView.visibility = View.VISIBLE
|
||||
inviteView.render(inviter, VectorInviteView.Mode.LARGE)
|
||||
} else if (state.asyncInviter.complete) {
|
||||
vectorBaseActivity.finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderRoomSummary(state: RoomDetailViewState) {
|
||||
state.asyncRoomSummary()?.let {
|
||||
toolbarTitleView.text = it.displayName
|
||||
AvatarRenderer.render(it, toolbarAvatarImageView)
|
||||
roomToolbarTitleView.text = it.displayName
|
||||
AvatarRenderer.render(it, roomToolbarAvatarImageView)
|
||||
if (it.topic.isNotEmpty()) {
|
||||
toolbarSubtitleView.visibility = View.VISIBLE
|
||||
toolbarSubtitleView.text = it.topic
|
||||
roomToolbarSubtitleView.visibility = View.VISIBLE
|
||||
roomToolbarSubtitleView.text = it.topic
|
||||
} else {
|
||||
toolbarSubtitleView.visibility = View.GONE
|
||||
roomToolbarSubtitleView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -391,20 +501,20 @@ class RoomDetailFragment :
|
||||
private fun renderSendMessageResult(sendMessageResult: SendMessageResult) {
|
||||
when (sendMessageResult) {
|
||||
is SendMessageResult.MessageSent,
|
||||
is SendMessageResult.SlashCommandHandled -> {
|
||||
is SendMessageResult.SlashCommandHandled -> {
|
||||
// Clear composer
|
||||
composerEditText.text = null
|
||||
composerLayout.composerEditText.text = null
|
||||
}
|
||||
is SendMessageResult.SlashCommandError -> {
|
||||
is SendMessageResult.SlashCommandError -> {
|
||||
displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command))
|
||||
}
|
||||
is SendMessageResult.SlashCommandUnknown -> {
|
||||
is SendMessageResult.SlashCommandUnknown -> {
|
||||
displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command))
|
||||
}
|
||||
is SendMessageResult.SlashCommandResultOk -> {
|
||||
is SendMessageResult.SlashCommandResultOk -> {
|
||||
// Ignore
|
||||
}
|
||||
is SendMessageResult.SlashCommandResultError -> {
|
||||
is SendMessageResult.SlashCommandResultError -> {
|
||||
displayCommandError(sendMessageResult.throwable.localizedMessage)
|
||||
}
|
||||
is SendMessageResult.SlashCommandNotImplemented -> {
|
||||
@ -455,11 +565,9 @@ class RoomDetailFragment :
|
||||
|
||||
override fun onEventLongClicked(informationData: MessageInformationData, messageContent: MessageContent, view: View): Boolean {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
val roomId = (arguments?.get(MvRx.KEY_ARG) as? RoomDetailArgs)?.roomId
|
||||
if (roomId.isNullOrBlank()) {
|
||||
Timber.e("Missing RoomId, cannot open bottomsheet")
|
||||
return false
|
||||
}
|
||||
val roomId = roomDetailArgs.roomId
|
||||
|
||||
this.view?.hideKeyboard()
|
||||
MessageActionsBottomSheet
|
||||
.newInstance(roomId, informationData)
|
||||
.show(requireActivity().supportFragmentManager, "MESSAGE_CONTEXTUAL_ACTIONS")
|
||||
@ -474,6 +582,23 @@ class RoomDetailFragment :
|
||||
override fun onMemberNameClicked(informationData: MessageInformationData) {
|
||||
insertUserDisplayNameInTextEditor(informationData.memberName?.toString())
|
||||
}
|
||||
|
||||
override fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) {
|
||||
if (on) {
|
||||
//we should test the current real state of reaction on this event
|
||||
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, informationData.eventId))
|
||||
} else {
|
||||
//I need to redact a reaction
|
||||
roomDetailViewModel.process(RoomDetailActions.UndoReaction(informationData.eventId, reaction))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEditedDecorationClicked(informationData: MessageInformationData, editAggregatedSummary: EditAggregatedSummary?) {
|
||||
editAggregatedSummary?.also {
|
||||
roomDetailViewModel.process(RoomDetailActions.ShowEditHistoryAction(informationData.eventId, it))
|
||||
}
|
||||
|
||||
}
|
||||
// AutocompleteUserPresenter.Callback
|
||||
|
||||
override fun onQueryUsers(query: CharSequence?) {
|
||||
@ -484,17 +609,22 @@ class RoomDetailFragment :
|
||||
it?.getContentIfNotHandled()?.let { actionData ->
|
||||
|
||||
when (actionData.actionId) {
|
||||
MessageMenuViewModel.ACTION_ADD_REACTION -> {
|
||||
startActivityForResult(EmojiReactionPickerActivity.intent(requireContext()), 0)
|
||||
MessageMenuViewModel.ACTION_ADD_REACTION -> {
|
||||
val eventId = actionData.data?.toString() ?: return
|
||||
startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), eventId), REACTION_SELECT_REQUEST_CODE)
|
||||
}
|
||||
MessageMenuViewModel.ACTION_COPY -> {
|
||||
MessageMenuViewModel.ACTION_COPY -> {
|
||||
//I need info about the current selected message :/
|
||||
copyToClipboard(requireContext(), actionData.data?.toString() ?: "", false)
|
||||
val snack = Snackbar.make(view!!, requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT)
|
||||
snack.view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.notification_accent_color))
|
||||
snack.show()
|
||||
}
|
||||
MessageMenuViewModel.ACTION_SHARE -> {
|
||||
MessageMenuViewModel.ACTION_DELETE -> {
|
||||
val eventId = actionData.data?.toString() ?: return
|
||||
roomDetailViewModel.process(RoomDetailActions.RedactAction(eventId, context?.getString(R.string.event_redacted_by_user_reason)))
|
||||
}
|
||||
MessageMenuViewModel.ACTION_SHARE -> {
|
||||
//TODO current data communication is too limited
|
||||
//Need to now the media type
|
||||
actionData.data?.toString()?.let {
|
||||
@ -537,7 +667,25 @@ class RoomDetailFragment :
|
||||
.setPositiveButton(R.string.ok) { dialog, id -> dialog.cancel() }
|
||||
.show()
|
||||
}
|
||||
else -> {
|
||||
MessageMenuViewModel.ACTION_QUICK_REACT -> {
|
||||
//eventId,ClickedOn,Opposite
|
||||
(actionData.data as? Triple<String, String, String>)?.let { (eventId, clickedOn, opposite) ->
|
||||
roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(eventId, clickedOn, opposite))
|
||||
}
|
||||
}
|
||||
MessageMenuViewModel.ACTION_EDIT -> {
|
||||
val eventId = actionData.data.toString()
|
||||
roomDetailViewModel.process(RoomDetailActions.EnterEditMode(eventId))
|
||||
}
|
||||
MessageMenuViewModel.ACTION_QUOTE -> {
|
||||
val eventId = actionData.data.toString()
|
||||
roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(eventId))
|
||||
}
|
||||
MessageMenuViewModel.ACTION_REPLY -> {
|
||||
val eventId = actionData.data.toString()
|
||||
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
|
||||
}
|
||||
else -> {
|
||||
Toast.makeText(context, "Action ${actionData.actionId} not implemented", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
@ -550,28 +698,30 @@ class RoomDetailFragment :
|
||||
*
|
||||
* @param text the text to insert.
|
||||
*/
|
||||
//TODO legacy, refactor
|
||||
private fun insertUserDisplayNameInTextEditor(text: String?) {
|
||||
//TODO move logic outside of fragment
|
||||
if (null != text) {
|
||||
// var vibrate = false
|
||||
|
||||
val myDisplayName = session.getUser(session.sessionParams.credentials.userId)?.displayName
|
||||
if (TextUtils.equals(myDisplayName, text)) {
|
||||
// current user
|
||||
if (TextUtils.isEmpty(composerEditText.text)) {
|
||||
composerEditText.append(Command.EMOTE.command + " ")
|
||||
composerEditText.setSelection(composerEditText.text.length)
|
||||
if (TextUtils.isEmpty(composerLayout.composerEditText.text)) {
|
||||
composerLayout.composerEditText.append(Command.EMOTE.command + " ")
|
||||
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
|
||||
// vibrate = true
|
||||
}
|
||||
} else {
|
||||
// another user
|
||||
if (TextUtils.isEmpty(composerEditText.text)) {
|
||||
if (TextUtils.isEmpty(composerLayout.composerEditText.text)) {
|
||||
// Ensure displayName will not be interpreted as a Slash command
|
||||
if (text.startsWith("/")) {
|
||||
composerEditText.append("\\")
|
||||
composerLayout.composerEditText.append("\\")
|
||||
}
|
||||
composerEditText.append(sanitizeDisplayname(text)!! + ": ")
|
||||
composerLayout.composerEditText.append(sanitizeDisplayname(text)!! + ": ")
|
||||
} else {
|
||||
composerEditText.text.insert(composerEditText.selectionStart, sanitizeDisplayname(text)!! + " ")
|
||||
composerLayout.composerEditText.text.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayname(text)!! + " ")
|
||||
}
|
||||
|
||||
// vibrate = true
|
||||
@ -583,10 +733,29 @@ class RoomDetailFragment :
|
||||
// v.vibrate(100)
|
||||
// }
|
||||
// }
|
||||
composerEditText.requestFocus()
|
||||
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
imm?.showSoftInput(composerEditText, InputMethodManager.SHOW_FORCED)
|
||||
focusComposerAndShowKeyboard()
|
||||
}
|
||||
}
|
||||
|
||||
private fun focusComposerAndShowKeyboard() {
|
||||
composerLayout.composerEditText.requestFocus()
|
||||
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
imm?.showSoftInput(composerLayout.composerEditText, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
|
||||
fun showSnackWithMessage(message: String, duration: Int = Snackbar.LENGTH_SHORT) {
|
||||
val snack = Snackbar.make(view!!, message, duration)
|
||||
snack.view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.notification_accent_color))
|
||||
snack.show()
|
||||
}
|
||||
|
||||
// VectorInviteView.Callback
|
||||
|
||||
override fun onAcceptInvite() {
|
||||
roomDetailViewModel.process(RoomDetailActions.AcceptInvite)
|
||||
}
|
||||
|
||||
override fun onRejectInvite() {
|
||||
roomDetailViewModel.process(RoomDetailActions.RejectInvite)
|
||||
}
|
||||
}
|
||||
|
@ -16,29 +16,39 @@
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail
|
||||
|
||||
import android.text.TextUtils
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.jakewharton.rxrelay2.BehaviorRelay
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||
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.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.rx.rx
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.platform.VectorViewModel
|
||||
import im.vector.riotredesign.core.utils.LiveEvent
|
||||
import im.vector.riotredesign.features.command.CommandParser
|
||||
import im.vector.riotredesign.features.command.ParsedCommand
|
||||
import im.vector.riotredesign.features.home.room.VisibleRoomStore
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
|
||||
import io.reactivex.rxkotlin.subscribeBy
|
||||
import org.commonmark.parser.Parser
|
||||
import org.commonmark.renderer.html.HtmlRenderer
|
||||
import org.koin.android.ext.android.get
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
class RoomDetailViewModel(initialState: RoomDetailViewState,
|
||||
private val session: Session,
|
||||
private val visibleRoomHolder: VisibleRoomStore
|
||||
private val session: Session
|
||||
) : VectorViewModel<RoomDetailViewState>(initialState) {
|
||||
|
||||
private val room = session.getRoom(initialState.roomId)!!
|
||||
@ -54,14 +64,14 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
||||
@JvmStatic
|
||||
override fun create(viewModelContext: ViewModelContext, state: RoomDetailViewState): RoomDetailViewModel? {
|
||||
val currentSession = viewModelContext.activity.get<Session>()
|
||||
val visibleRoomHolder = viewModelContext.activity.get<VisibleRoomStore>()
|
||||
return RoomDetailViewModel(state, currentSession, visibleRoomHolder)
|
||||
return RoomDetailViewModel(state, currentSession)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
observeRoomSummary()
|
||||
observeEventDisplayedActions()
|
||||
observeInvitationState()
|
||||
room.loadRoomMembersIfNeeded()
|
||||
timeline.start()
|
||||
setState { copy(timeline = this@RoomDetailViewModel.timeline) }
|
||||
@ -69,14 +79,46 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
||||
|
||||
fun process(action: RoomDetailActions) {
|
||||
when (action) {
|
||||
is RoomDetailActions.SendMessage -> handleSendMessage(action)
|
||||
is RoomDetailActions.IsDisplayed -> handleIsDisplayed()
|
||||
is RoomDetailActions.SendMedia -> handleSendMedia(action)
|
||||
is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action)
|
||||
is RoomDetailActions.LoadMore -> handleLoadMore(action)
|
||||
is RoomDetailActions.SendMessage -> handleSendMessage(action)
|
||||
is RoomDetailActions.SendMedia -> handleSendMedia(action)
|
||||
is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action)
|
||||
is RoomDetailActions.LoadMore -> handleLoadMore(action)
|
||||
is RoomDetailActions.SendReaction -> handleSendReaction(action)
|
||||
is RoomDetailActions.AcceptInvite -> handleAcceptInvite()
|
||||
is RoomDetailActions.RejectInvite -> handleRejectInvite()
|
||||
is RoomDetailActions.RedactAction -> handleRedactEvent(action)
|
||||
is RoomDetailActions.UndoReaction -> handleUndoReact(action)
|
||||
is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
|
||||
is RoomDetailActions.ShowEditHistoryAction -> handleShowEditHistoryReaction(action)
|
||||
is RoomDetailActions.EnterEditMode -> handleEditAction(action)
|
||||
is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action)
|
||||
is RoomDetailActions.EnterReplyMode -> handleReplyAction(action)
|
||||
}
|
||||
}
|
||||
|
||||
fun enterEditMode(event: TimelineEvent) {
|
||||
setState {
|
||||
copy(
|
||||
sendMode = SendMode.EDIT,
|
||||
selectedEvent = event
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun resetSendMode() {
|
||||
setState {
|
||||
copy(
|
||||
sendMode = SendMode.REGULAR,
|
||||
selectedEvent = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val _nonBlockingPopAlert = MutableLiveData<LiveEvent<Pair<Int, List<Any>>>>()
|
||||
val nonBlockingPopAlert: LiveData<LiveEvent<Pair<Int, List<Any>>>>
|
||||
get() = _nonBlockingPopAlert
|
||||
|
||||
|
||||
private val _sendMessageResultLiveData = MutableLiveData<LiveEvent<SendMessageResult>>()
|
||||
val sendMessageResultLiveData: LiveData<LiveEvent<SendMessageResult>>
|
||||
get() = _sendMessageResultLiveData
|
||||
@ -84,73 +126,163 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
||||
// PRIVATE METHODS *****************************************************************************
|
||||
|
||||
private fun handleSendMessage(action: RoomDetailActions.SendMessage) {
|
||||
// Handle slash command
|
||||
val slashCommandResult = CommandParser.parseSplashCommand(action.text)
|
||||
withState { state ->
|
||||
when (state.sendMode) {
|
||||
SendMode.REGULAR -> {
|
||||
val slashCommandResult = CommandParser.parseSplashCommand(action.text)
|
||||
|
||||
when (slashCommandResult) {
|
||||
is ParsedCommand.ErrorNotACommand -> {
|
||||
// Send the text message to the room
|
||||
room.sendTextMessage(action.text)
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
|
||||
}
|
||||
is ParsedCommand.ErrorSyntax -> {
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command)))
|
||||
}
|
||||
is ParsedCommand.ErrorEmptySlashCommand -> {
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown("/")))
|
||||
}
|
||||
is ParsedCommand.ErrorUnknownSlashCommand -> {
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown(slashCommandResult.slashCommand)))
|
||||
}
|
||||
is ParsedCommand.Invite -> {
|
||||
handleInviteSlashCommand(slashCommandResult)
|
||||
}
|
||||
is ParsedCommand.SetUserPowerLevel -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.ClearScalarToken -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.SetMarkdown -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.UnbanUser -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.BanUser -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.KickUser -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.JoinRoom -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.PartRoom -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.SendEmote -> {
|
||||
room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE)
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled))
|
||||
}
|
||||
is ParsedCommand.ChangeTopic -> {
|
||||
handleChangeTopicSlashCommand(slashCommandResult)
|
||||
}
|
||||
is ParsedCommand.ChangeDisplayName -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
when (slashCommandResult) {
|
||||
is ParsedCommand.ErrorNotACommand -> {
|
||||
// Send the text message to the room
|
||||
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
|
||||
}
|
||||
is ParsedCommand.ErrorSyntax -> {
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command)))
|
||||
}
|
||||
is ParsedCommand.ErrorEmptySlashCommand -> {
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown("/")))
|
||||
}
|
||||
is ParsedCommand.ErrorUnknownSlashCommand -> {
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown(slashCommandResult.slashCommand)))
|
||||
}
|
||||
is ParsedCommand.Invite -> {
|
||||
handleInviteSlashCommand(slashCommandResult)
|
||||
}
|
||||
is ParsedCommand.SetUserPowerLevel -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.ClearScalarToken -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.SetMarkdown -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.UnbanUser -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.BanUser -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.KickUser -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.JoinRoom -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.PartRoom -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
is ParsedCommand.SendEmote -> {
|
||||
room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE)
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled))
|
||||
}
|
||||
is ParsedCommand.ChangeTopic -> {
|
||||
handleChangeTopicSlashCommand(slashCommandResult)
|
||||
}
|
||||
is ParsedCommand.ChangeDisplayName -> {
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
|
||||
}
|
||||
}
|
||||
}
|
||||
SendMode.EDIT -> {
|
||||
room.editTextMessage(state.selectedEvent?.root?.eventId ?: "", action.text, action.autoMarkdown)
|
||||
setState {
|
||||
copy(
|
||||
sendMode = SendMode.REGULAR,
|
||||
selectedEvent = null
|
||||
)
|
||||
}
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
|
||||
}
|
||||
SendMode.QUOTE -> {
|
||||
val messageContent: MessageContent? =
|
||||
state.selectedEvent?.annotations?.editSummary?.aggregatedContent?.toModel()
|
||||
?: state.selectedEvent?.root?.content.toModel()
|
||||
val textMsg = messageContent?.body
|
||||
|
||||
val finalText = legacyRiotQuoteText(textMsg, action.text)
|
||||
|
||||
//TODO Refactor this, just temporary for quotes
|
||||
val parser = Parser.builder().build()
|
||||
val document = parser.parse(finalText)
|
||||
val renderer = HtmlRenderer.builder().build()
|
||||
val htmlText = renderer.render(document)
|
||||
if (TextUtils.equals(finalText, htmlText)) {
|
||||
room.sendTextMessage(finalText)
|
||||
} else {
|
||||
room.sendFormattedTextMessage(finalText, htmlText)
|
||||
}
|
||||
setState {
|
||||
copy(
|
||||
sendMode = SendMode.REGULAR,
|
||||
selectedEvent = null
|
||||
)
|
||||
}
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
|
||||
}
|
||||
SendMode.REPLY -> {
|
||||
state.selectedEvent?.let {
|
||||
room.replyToMessage(it.root, action.text)
|
||||
setState {
|
||||
copy(
|
||||
sendMode = SendMode.REGULAR,
|
||||
selectedEvent = null
|
||||
)
|
||||
}
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle slash command
|
||||
|
||||
}
|
||||
|
||||
private fun legacyRiotQuoteText(quotedText: String?, myText: String): String {
|
||||
val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray()
|
||||
var quotedTextMsg = StringBuilder()
|
||||
if (messageParagraphs != null) {
|
||||
for (i in messageParagraphs.indices) {
|
||||
if (messageParagraphs[i].trim({ it <= ' ' }) != "") {
|
||||
quotedTextMsg.append("> ").append(messageParagraphs[i])
|
||||
}
|
||||
|
||||
if (i + 1 != messageParagraphs.size) {
|
||||
quotedTextMsg.append("\n\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
val finalText = "$quotedTextMsg\n\n$myText"
|
||||
return finalText
|
||||
}
|
||||
|
||||
private fun handleShowEditHistoryReaction(action: RoomDetailActions.ShowEditHistoryAction) {
|
||||
//TODO temporary implementation
|
||||
val lastReplace = action.editAggregatedSummary.sourceEvents.lastOrNull()?.let {
|
||||
room.getTimeLineEvent(it)
|
||||
} ?: return
|
||||
|
||||
val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault())
|
||||
_nonBlockingPopAlert.postValue(LiveEvent(
|
||||
Pair(R.string.last_edited_info_message, listOf(
|
||||
lastReplace.senderName ?: "?",
|
||||
dateFormat.format(Date(lastReplace.root.originServerTs ?: 0)))
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) {
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled))
|
||||
|
||||
@ -179,6 +311,26 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
private fun handleSendReaction(action: RoomDetailActions.SendReaction) {
|
||||
room.sendReaction(action.reaction, action.targetEventId)
|
||||
}
|
||||
|
||||
private fun handleRedactEvent(action: RoomDetailActions.RedactAction) {
|
||||
val event = room.getTimeLineEvent(action.targetEventId) ?: return
|
||||
room.redactEvent(event.root, action.reason)
|
||||
}
|
||||
|
||||
private fun handleUndoReact(action: RoomDetailActions.UndoReaction) {
|
||||
room.undoReaction(action.key, action.targetEventId, session.sessionParams.credentials.userId)
|
||||
}
|
||||
|
||||
|
||||
private fun handleUpdateQuickReaction(action: RoomDetailActions.UpdateQuickReactAction) {
|
||||
room.updateQuickReaction(action.selectedReaction, action.opposite, action.targetEventId, session.sessionParams.credentials.userId)
|
||||
}
|
||||
|
||||
|
||||
private fun handleSendMedia(action: RoomDetailActions.SendMedia) {
|
||||
val attachments = action.mediaFiles.map {
|
||||
ContentAttachmentData(
|
||||
@ -200,14 +352,47 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
||||
displayedEventsObservable.accept(action)
|
||||
}
|
||||
|
||||
private fun handleIsDisplayed() {
|
||||
visibleRoomHolder.post(roomId)
|
||||
}
|
||||
|
||||
private fun handleLoadMore(action: RoomDetailActions.LoadMore) {
|
||||
timeline.paginate(action.direction, PAGINATION_COUNT)
|
||||
}
|
||||
|
||||
private fun handleRejectInvite() {
|
||||
room.leave(object : MatrixCallback<Unit> {})
|
||||
}
|
||||
|
||||
private fun handleAcceptInvite() {
|
||||
room.join(object : MatrixCallback<Unit> {})
|
||||
}
|
||||
|
||||
private fun handleEditAction(action: RoomDetailActions.EnterEditMode) {
|
||||
room.getTimeLineEvent(action.eventId)?.let {
|
||||
enterEditMode(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleQuoteAction(action: RoomDetailActions.EnterQuoteMode) {
|
||||
room.getTimeLineEvent(action.eventId)?.let {
|
||||
setState {
|
||||
copy(
|
||||
sendMode = SendMode.QUOTE,
|
||||
selectedEvent = it
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleReplyAction(action: RoomDetailActions.EnterReplyMode) {
|
||||
room.getTimeLineEvent(action.eventId)?.let {
|
||||
setState {
|
||||
copy(
|
||||
sendMode = SendMode.REPLY,
|
||||
selectedEvent = it
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun observeEventDisplayedActions() {
|
||||
// We are buffering scroll events for one second
|
||||
// and keep the most recent one to set the read receipt on.
|
||||
@ -230,6 +415,18 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeInvitationState() {
|
||||
asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary ->
|
||||
if (summary.membership == Membership.INVITE) {
|
||||
summary.lastMessage?.sender?.let { senderId ->
|
||||
session.getUser(senderId)
|
||||
}?.also {
|
||||
setState { copy(asyncInviter = Success(it)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
timeline.dispose()
|
||||
super.onCleared()
|
||||
|
@ -22,13 +22,33 @@ import com.airbnb.mvrx.Uninitialized
|
||||
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(
|
||||
val roomId: String,
|
||||
val eventId: String?,
|
||||
val timeline: Timeline? = null,
|
||||
val asyncInviter: Async<User> = Uninitialized,
|
||||
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
||||
val asyncTimelineData: Async<TimelineData> = Uninitialized
|
||||
val asyncTimelineData: Async<TimelineData> = Uninitialized,
|
||||
val sendMode: SendMode = SendMode.REGULAR,
|
||||
val selectedEvent: TimelineEvent? = null
|
||||
) : MvRxState {
|
||||
|
||||
constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)
|
||||
|
@ -0,0 +1,116 @@
|
||||
package im.vector.riotredesign.features.home.room.detail.composer
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.transition.AutoTransition
|
||||
import androidx.transition.Transition
|
||||
import androidx.transition.TransitionManager
|
||||
import butterknife.BindView
|
||||
import butterknife.ButterKnife
|
||||
import im.vector.riotredesign.R
|
||||
|
||||
|
||||
/**
|
||||
* Encapsulate the timeline composer UX.
|
||||
*
|
||||
*/
|
||||
class TextComposerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
@BindView(R.id.composer_related_message_sender)
|
||||
lateinit var composerRelatedMessageTitle: TextView
|
||||
@BindView(R.id.composer_related_message_preview)
|
||||
lateinit var composerRelatedMessageContent: TextView
|
||||
@BindView(R.id.composer_related_message_avatar_view)
|
||||
lateinit var composerRelatedMessageAvatar: ImageView
|
||||
@BindView(R.id.composer_related_message_action_image)
|
||||
lateinit var composerRelatedMessageActionIcon: ImageView
|
||||
@BindView(R.id.composer_related_message_close)
|
||||
lateinit var composerRelatedMessageCloseButton: ImageButton
|
||||
@BindView(R.id.composerEditText)
|
||||
lateinit var composerEditText: EditText
|
||||
@BindView(R.id.composer_avatar_view)
|
||||
lateinit var composerAvatarImageView: ImageView
|
||||
|
||||
var currentConstraintSetId: Int = -1
|
||||
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.merge_composer_layout, this)
|
||||
ButterKnife.bind(this)
|
||||
collapse(false)
|
||||
}
|
||||
|
||||
|
||||
fun collapse(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) {
|
||||
if (currentConstraintSetId == R.layout.constraint_set_composer_layout_compact) {
|
||||
//ignore we good
|
||||
return
|
||||
}
|
||||
currentConstraintSetId = R.layout.constraint_set_composer_layout_compact
|
||||
if (animate) {
|
||||
val transition = AutoTransition()
|
||||
// transition.duration = 5000
|
||||
transition.addListener(object : Transition.TransitionListener {
|
||||
|
||||
override fun onTransitionEnd(transition: Transition) {
|
||||
transitionComplete?.invoke()
|
||||
}
|
||||
|
||||
override fun onTransitionResume(transition: Transition) {}
|
||||
|
||||
override fun onTransitionPause(transition: Transition) {}
|
||||
|
||||
override fun onTransitionCancel(transition: Transition) {}
|
||||
|
||||
override fun onTransitionStart(transition: Transition) {}
|
||||
}
|
||||
)
|
||||
TransitionManager.beginDelayedTransition((parent as? ViewGroup ?: this), transition)
|
||||
}
|
||||
ConstraintSet().also {
|
||||
it.clone(context, currentConstraintSetId)
|
||||
it.applyTo(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) {
|
||||
if (currentConstraintSetId == R.layout.constraint_set_composer_layout_expanded) {
|
||||
//ignore we good
|
||||
return
|
||||
}
|
||||
currentConstraintSetId = R.layout.constraint_set_composer_layout_expanded
|
||||
if (animate) {
|
||||
val transition = AutoTransition()
|
||||
// transition.duration = 5000
|
||||
transition.addListener(object : Transition.TransitionListener {
|
||||
|
||||
override fun onTransitionEnd(transition: Transition) {
|
||||
transitionComplete?.invoke()
|
||||
}
|
||||
|
||||
override fun onTransitionResume(transition: Transition) {}
|
||||
|
||||
override fun onTransitionPause(transition: Transition) {}
|
||||
|
||||
override fun onTransitionCancel(transition: Transition) {}
|
||||
|
||||
override fun onTransitionStart(transition: Transition) {}
|
||||
}
|
||||
)
|
||||
TransitionManager.beginDelayedTransition((parent as? ViewGroup ?: this), transition)
|
||||
}
|
||||
ConstraintSet().also {
|
||||
it.clone(context, currentConstraintSetId)
|
||||
it.applyTo(this)
|
||||
}
|
||||
}
|
||||
}
|
@ -24,13 +24,11 @@ import androidx.recyclerview.widget.ListUpdateCallback
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.airbnb.epoxy.EpoxyController
|
||||
import com.airbnb.epoxy.EpoxyModel
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
|
||||
import im.vector.matrix.android.api.session.room.model.message.*
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotredesign.core.epoxy.LoadingItemModel_
|
||||
import im.vector.riotredesign.core.epoxy.loadingItem
|
||||
import im.vector.riotredesign.core.epoxy.LoadingItem_
|
||||
import im.vector.riotredesign.core.extensions.localDateTime
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.factory.TimelineItemFactory
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.*
|
||||
@ -48,7 +46,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
||||
private val backgroundHandler: Handler = TimelineAsyncHelper.getBackgroundHandler()
|
||||
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener {
|
||||
|
||||
interface Callback {
|
||||
interface Callback : ReactionPillCallback {
|
||||
fun onEventVisible(event: TimelineEvent)
|
||||
fun onUrlClicked(url: String)
|
||||
fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View)
|
||||
@ -59,6 +57,11 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
||||
fun onEventLongClicked(informationData: MessageInformationData, messageContent: MessageContent, view: View): Boolean
|
||||
fun onAvatarClicked(informationData: MessageInformationData)
|
||||
fun onMemberNameClicked(informationData: MessageInformationData)
|
||||
fun onEditedDecorationClicked(informationData: MessageInformationData, editAggregatedSummary: EditAggregatedSummary?)
|
||||
}
|
||||
|
||||
interface ReactionPillCallback {
|
||||
fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean)
|
||||
}
|
||||
|
||||
private val collapsedEventIds = linkedSetOf<String>()
|
||||
@ -126,14 +129,14 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
||||
}
|
||||
|
||||
override fun buildModels() {
|
||||
LoadingItemModel_()
|
||||
LoadingItem_()
|
||||
.id("forward_loading_item")
|
||||
.addWhen(Timeline.Direction.FORWARDS)
|
||||
|
||||
val timelineModels = getModels()
|
||||
add(timelineModels)
|
||||
|
||||
LoadingItemModel_()
|
||||
LoadingItem_()
|
||||
.id("backward_loading_item")
|
||||
.addWhen(Timeline.Direction.BACKWARDS)
|
||||
}
|
||||
@ -224,10 +227,8 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
||||
} else {
|
||||
val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed()
|
||||
val mergedData = mergedEvents.map { mergedEvent ->
|
||||
val eventContent: RoomMember? = mergedEvent.root.content.toModel()
|
||||
val prevEventContent: RoomMember? = mergedEvent.root.prevContent.toModel()
|
||||
val senderAvatar = RoomMemberEventHelper.senderAvatar(eventContent, prevEventContent, mergedEvent)
|
||||
val senderName = RoomMemberEventHelper.senderName(eventContent, prevEventContent, mergedEvent)
|
||||
val senderAvatar = mergedEvent.senderAvatar()
|
||||
val senderName = mergedEvent.senderName()
|
||||
MergedHeaderItem.Data(
|
||||
userId = mergedEvent.root.sender ?: "",
|
||||
avatarUrl = senderAvatar,
|
||||
@ -256,7 +257,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
||||
}
|
||||
}
|
||||
|
||||
private fun LoadingItemModel_.addWhen(direction: Timeline.Direction) {
|
||||
private fun LoadingItem_.addWhen(direction: Timeline.Direction) {
|
||||
val shouldAdd = timeline?.hasMoreToLoad(direction) ?: false
|
||||
addIf(shouldAdd, this@TimelineEventController)
|
||||
}
|
||||
|
@ -16,24 +16,35 @@
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline.action
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import com.airbnb.mvrx.MvRx
|
||||
import com.airbnb.mvrx.MvRxView
|
||||
import com.airbnb.mvrx.MvRxViewModelStore
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Add MvRx capabilities to bottomsheetdialog (like BaseMvRxFragment)
|
||||
*/
|
||||
abstract class BaseMvRxBottomSheetDialog() : BottomSheetDialogFragment(), MvRxView {
|
||||
abstract class BaseMvRxBottomSheetDialog : BottomSheetDialogFragment(), MvRxView {
|
||||
|
||||
override val mvrxViewModelStore by lazy { MvRxViewModelStore(viewModelStore) }
|
||||
private lateinit var mvrxPersistedViewId: String
|
||||
|
||||
final override val mvrxViewId: String by lazy { mvrxPersistedViewId }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
mvrxViewModelStore.restoreViewModels(this, savedInstanceState)
|
||||
mvrxPersistedViewId = savedInstanceState?.getString(PERSISTED_VIEW_ID_KEY)
|
||||
?: this::class.java.simpleName + "_" + UUID.randomUUID().toString()
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
mvrxViewModelStore.saveViewModels(outState)
|
||||
outState.putString(PERSISTED_VIEW_ID_KEY, mvrxViewId)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
@ -42,4 +53,10 @@ abstract class BaseMvRxBottomSheetDialog() : BottomSheetDialogFragment(), MvRxVi
|
||||
// subscribe to a ViewModel.
|
||||
postInvalidate()
|
||||
}
|
||||
}
|
||||
|
||||
protected fun setArguments(args: Parcelable? = null) {
|
||||
arguments = args?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } }
|
||||
}
|
||||
}
|
||||
|
||||
private const val PERSISTED_VIEW_ID_KEY = "mvrx:bottomsheet_persisted_view_id"
|
@ -89,14 +89,14 @@ class MessageActionsBottomSheet : BaseMvRxBottomSheetDialog() {
|
||||
|
||||
var quickReactionFragment = cfm.findFragmentByTag("QuickReaction") as? QuickReactionFragment
|
||||
if (quickReactionFragment == null) {
|
||||
quickReactionFragment = QuickReactionFragment.newInstance()
|
||||
quickReactionFragment = QuickReactionFragment.newInstance(arguments!!.get(MvRx.KEY_ARG) as ParcelableArgs)
|
||||
cfm.beginTransaction()
|
||||
.replace(R.id.bottom_sheet_quick_reaction_container, quickReactionFragment, "QuickReaction")
|
||||
.commit()
|
||||
}
|
||||
quickReactionFragment.interactionListener = object : QuickReactionFragment.InteractionListener {
|
||||
override fun didQuickReactWith(reactions: List<String>) {
|
||||
actionHandlerModel.fireAction("Quick React", reactions)
|
||||
override fun didQuickReactWith(clikedOn: String, opposite: String, reactions: List<String>, eventId: String) {
|
||||
actionHandlerModel.fireAction(MessageMenuViewModel.ACTION_QUICK_REACT, Triple(eventId, clikedOn, opposite))
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
@ -144,15 +144,15 @@ class MessageActionsBottomSheet : BaseMvRxBottomSheetDialog() {
|
||||
|
||||
companion object {
|
||||
fun newInstance(roomId: String, informationData: MessageInformationData): MessageActionsBottomSheet {
|
||||
val args = Bundle()
|
||||
val parcelableArgs = ParcelableArgs(
|
||||
informationData.eventId,
|
||||
roomId,
|
||||
informationData
|
||||
)
|
||||
args.putParcelable(MvRx.KEY_ARG, parcelableArgs)
|
||||
return MessageActionsBottomSheet().apply { arguments = args }
|
||||
|
||||
return MessageActionsBottomSheet().apply {
|
||||
setArguments(
|
||||
ParcelableArgs(
|
||||
informationData.eventId,
|
||||
roomId,
|
||||
informationData
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -21,8 +21,14 @@ import com.airbnb.mvrx.ViewModelContext
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
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.MessageTextContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.riotredesign.core.platform.VectorViewModel
|
||||
import org.commonmark.parser.Parser
|
||||
import org.commonmark.renderer.html.HtmlRenderer
|
||||
import org.koin.android.ext.android.get
|
||||
import ru.noties.markwon.Markwon
|
||||
import ru.noties.markwon.html.HtmlPlugin
|
||||
import timber.log.Timber
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
@ -31,7 +37,7 @@ import java.util.*
|
||||
data class MessageActionState(
|
||||
val userId: String,
|
||||
val senderName: String,
|
||||
val messageBody: String,
|
||||
val messageBody: CharSequence,
|
||||
val ts: String?,
|
||||
val senderAvatarPath: String? = null)
|
||||
: MvRxState
|
||||
@ -51,12 +57,22 @@ class MessageActionsViewModel(initialState: MessageActionState) : VectorViewMode
|
||||
|
||||
val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId)
|
||||
return if (event != null) {
|
||||
val messageContent: MessageContent? = event.root.content.toModel()
|
||||
val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent?.toModel()
|
||||
?: event.root.content.toModel()
|
||||
val originTs = event.root.originServerTs
|
||||
var body: CharSequence = messageContent?.body ?: ""
|
||||
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
|
||||
val parser = Parser.builder().build()
|
||||
val document = parser.parse(messageContent.formattedBody?.trim() ?: messageContent.body)
|
||||
// val renderer = HtmlRenderer.builder().build()
|
||||
body = Markwon.builder(viewModelContext.activity)
|
||||
.usePlugin(HtmlPlugin.create()).build().render(document)
|
||||
// body = renderer.render(document)
|
||||
}
|
||||
MessageActionState(
|
||||
event.root.sender ?: "",
|
||||
parcel.informationData.memberName.toString(),
|
||||
messageContent?.body ?: "",
|
||||
body,
|
||||
dateFormat.format(Date(originTs ?: 0)),
|
||||
currentSession.contentUrlResolver().resolveFullSize(parcel.informationData.avatarUrl)
|
||||
)
|
||||
|
@ -50,16 +50,17 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
|
||||
val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId)
|
||||
?: return null
|
||||
|
||||
val messageContent: MessageContent = event.root.content.toModel() ?: return null
|
||||
val messageContent: MessageContent = event.annotations?.editSummary?.aggregatedContent?.toModel()
|
||||
?: event.root.content.toModel() ?: return null
|
||||
val type = messageContent.type
|
||||
|
||||
if (event.sendState == SendState.UNSENT) {
|
||||
//Resend and Delete
|
||||
return MessageMenuState(
|
||||
listOf(
|
||||
SimpleAction(ACTION_RESEND, R.string.resend, R.drawable.ic_corner_down_right, event.root.eventId),
|
||||
SimpleAction(ACTION_RESEND, R.string.resend, R.drawable.ic_send, event.root.eventId),
|
||||
//TODO delete icon
|
||||
SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_material_delete, event.root.eventId)
|
||||
SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_delete, event.root.eventId)
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -67,20 +68,36 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
|
||||
|
||||
//TODO determine if can copy, forward, reply, quote, report?
|
||||
val actions = ArrayList<SimpleAction>().apply {
|
||||
this.add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_smile))
|
||||
|
||||
if (event.sendState == SendState.SENDING) {
|
||||
//TODO add cancel?
|
||||
return@apply
|
||||
}
|
||||
//TODO is downloading attachement?
|
||||
|
||||
this.add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_add_reaction, event.root.eventId))
|
||||
if (canCopy(type)) {
|
||||
//TODO copy images? html? see ClipBoard
|
||||
this.add(SimpleAction(ACTION_COPY, R.string.copy, R.drawable.ic_copy, messageContent.body))
|
||||
}
|
||||
|
||||
if (canReply(event, messageContent)) {
|
||||
this.add(SimpleAction(ACTION_REPLY, R.string.reply, R.drawable.ic_reply, event.root.eventId))
|
||||
}
|
||||
|
||||
if (canEdit(event, currentSession.sessionParams.credentials.userId)) {
|
||||
this.add(SimpleAction(ACTION_EDIT, R.string.edit, R.drawable.ic_edit, event.root.eventId))
|
||||
}
|
||||
|
||||
if (canRedact(event, currentSession.sessionParams.credentials.userId)) {
|
||||
this.add(SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_delete, event.root.eventId))
|
||||
}
|
||||
|
||||
if (canQuote(event, messageContent)) {
|
||||
//TODO quote icon
|
||||
this.add(SimpleAction(ACTION_QUOTE, R.string.quote, R.drawable.ic_quote, parcel.eventId))
|
||||
}
|
||||
|
||||
if (canReply(event, messageContent)) {
|
||||
this.add(SimpleAction(ACTION_REPLY, R.string.reply, R.drawable.ic_corner_down_right))
|
||||
}
|
||||
if (canShare(type)) {
|
||||
if (messageContent is MessageImageContent) {
|
||||
this.add(
|
||||
@ -92,8 +109,6 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
|
||||
//TODO
|
||||
}
|
||||
|
||||
//TODO is uploading
|
||||
//TODO is downloading
|
||||
|
||||
if (event.sendState == SendState.SENT) {
|
||||
|
||||
@ -148,6 +163,25 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
|
||||
}
|
||||
}
|
||||
|
||||
private fun canRedact(event: TimelineEvent, myUserId: String): Boolean {
|
||||
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
||||
if (event.root.type != EventType.MESSAGE) return false
|
||||
//TODO if user is admin or moderator
|
||||
return event.root.sender == myUserId
|
||||
}
|
||||
|
||||
private fun canEdit(event: TimelineEvent, myUserId: String): Boolean {
|
||||
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
||||
if (event.root.type != EventType.MESSAGE) return false
|
||||
//TODO if user is admin or moderator
|
||||
val messageContent = event.root.content.toModel<MessageContent>()
|
||||
return event.root.sender == myUserId && (
|
||||
messageContent?.type == MessageType.MSGTYPE_TEXT
|
||||
|| messageContent?.type == MessageType.MSGTYPE_EMOTE
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun canCopy(type: String): Boolean {
|
||||
return when (type) {
|
||||
MessageType.MSGTYPE_TEXT,
|
||||
@ -175,6 +209,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
|
||||
|
||||
const val ACTION_ADD_REACTION = "add_reaction"
|
||||
const val ACTION_COPY = "copy"
|
||||
const val ACTION_EDIT = "edit"
|
||||
const val ACTION_QUOTE = "quote"
|
||||
const val ACTION_REPLY = "reply"
|
||||
const val ACTION_SHARE = "share"
|
||||
@ -184,6 +219,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
|
||||
const val VIEW_DECRYPTED_SOURCE = "VIEW_DECRYPTED_SOURCE"
|
||||
const val PERMALINK = "PERMALINK"
|
||||
const val ACTION_FLAG = "ACTION_FLAG"
|
||||
const val ACTION_QUICK_REACT = "ACTION_QUICK_REACT"
|
||||
|
||||
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import androidx.transition.TransitionManager
|
||||
import butterknife.BindView
|
||||
import butterknife.ButterKnife
|
||||
import com.airbnb.mvrx.BaseMvRxFragment
|
||||
import com.airbnb.mvrx.MvRx
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.riotredesign.R
|
||||
@ -62,10 +63,10 @@ class QuickReactionFragment : BaseMvRxFragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
quickReact1Text.text = viewModel.agreePositive
|
||||
quickReact2Text.text = viewModel.agreeNegative
|
||||
quickReact3Text.text = viewModel.likePositive
|
||||
quickReact4Text.text = viewModel.likeNegative
|
||||
quickReact1Text.text = QuickReactionViewModel.agreePositive
|
||||
quickReact2Text.text = QuickReactionViewModel.agreeNegative
|
||||
quickReact3Text.text = QuickReactionViewModel.likePositive
|
||||
quickReact4Text.text = QuickReactionViewModel.likeNegative
|
||||
|
||||
//configure click listeners
|
||||
quickReact1Text.setOnClickListener {
|
||||
@ -118,17 +119,23 @@ class QuickReactionFragment : BaseMvRxFragment() {
|
||||
}
|
||||
|
||||
if (it.selectionResult != null) {
|
||||
interactionListener?.didQuickReactWith(it.selectionResult)
|
||||
val clikedOn = it.selectionResult.first
|
||||
interactionListener?.didQuickReactWith(clikedOn, QuickReactionViewModel.getOpposite(clikedOn)
|
||||
?: "", it.selectionResult.second, it.eventId)
|
||||
}
|
||||
}
|
||||
|
||||
interface InteractionListener {
|
||||
fun didQuickReactWith(reactions: List<String>)
|
||||
fun didQuickReactWith(clikedOn: String, opposite: String, reactions: List<String>, eventId: String)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance(): QuickReactionFragment {
|
||||
return QuickReactionFragment()
|
||||
fun newInstance(pa: MessageActionsBottomSheet.ParcelableArgs): QuickReactionFragment {
|
||||
val args = Bundle()
|
||||
args.putParcelable(MvRx.KEY_ARG, pa)
|
||||
val fragment = QuickReactionFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
@ -18,7 +18,9 @@ package im.vector.riotredesign.features.home.room.detail.timeline.action
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.riotredesign.core.platform.VectorViewModel
|
||||
import org.koin.android.ext.android.get
|
||||
|
||||
/**
|
||||
* Quick reactions state, it's a toggle with 3rd state
|
||||
@ -29,7 +31,12 @@ enum class TriggleState {
|
||||
SECOND
|
||||
}
|
||||
|
||||
data class QuickReactionState(val agreeTrigleState: TriggleState, val likeTriggleState: TriggleState, val selectionResult: List<String>? = null) : MvRxState
|
||||
data class QuickReactionState(
|
||||
val agreeTrigleState: TriggleState,
|
||||
val likeTriggleState: TriggleState,
|
||||
/** Pair of 'clickedOn' and current toggles state*/
|
||||
val selectionResult: Pair<String, List<String>>? = null,
|
||||
val eventId: String) : MvRxState
|
||||
|
||||
/**
|
||||
* Quick reaction view model
|
||||
@ -37,25 +44,22 @@ data class QuickReactionState(val agreeTrigleState: TriggleState, val likeTriggl
|
||||
*/
|
||||
class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel<QuickReactionState>(initialState) {
|
||||
|
||||
val agreePositive = "👍"
|
||||
val agreeNegative = "👎"
|
||||
val likePositive = "😀"
|
||||
val likeNegative = "😞"
|
||||
|
||||
|
||||
fun toggleAgree(isFirst: Boolean) = withState {
|
||||
if (isFirst) {
|
||||
setState {
|
||||
val newTriggle = if (it.agreeTrigleState == TriggleState.FIRST) TriggleState.NONE else TriggleState.FIRST
|
||||
copy(
|
||||
agreeTrigleState = if (it.agreeTrigleState == TriggleState.FIRST) TriggleState.NONE else TriggleState.FIRST,
|
||||
selectionResult = getReactions(this)
|
||||
agreeTrigleState = newTriggle,
|
||||
selectionResult = Pair(agreePositive, getReactions(this, newTriggle, null))
|
||||
)
|
||||
}
|
||||
} else {
|
||||
setState {
|
||||
val newTriggle = if (it.agreeTrigleState == TriggleState.SECOND) TriggleState.NONE else TriggleState.SECOND
|
||||
copy(
|
||||
agreeTrigleState = if (it.agreeTrigleState == TriggleState.SECOND) TriggleState.NONE else TriggleState.SECOND,
|
||||
selectionResult = getReactions(this)
|
||||
agreeTrigleState = agreeTrigleState,
|
||||
selectionResult = Pair(agreeNegative, getReactions(this, newTriggle, null))
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -64,30 +68,32 @@ class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel
|
||||
fun toggleLike(isFirst: Boolean) = withState {
|
||||
if (isFirst) {
|
||||
setState {
|
||||
val newTriggle = if (it.likeTriggleState == TriggleState.FIRST) TriggleState.NONE else TriggleState.FIRST
|
||||
copy(
|
||||
likeTriggleState = if (it.likeTriggleState == TriggleState.FIRST) TriggleState.NONE else TriggleState.FIRST,
|
||||
selectionResult = getReactions(this)
|
||||
likeTriggleState = newTriggle,
|
||||
selectionResult = Pair(likePositive, getReactions(this, null, newTriggle))
|
||||
)
|
||||
}
|
||||
} else {
|
||||
setState {
|
||||
val newTriggle = if (it.likeTriggleState == TriggleState.SECOND) TriggleState.NONE else TriggleState.SECOND
|
||||
copy(
|
||||
likeTriggleState = if (it.likeTriggleState == TriggleState.SECOND) TriggleState.NONE else TriggleState.SECOND,
|
||||
selectionResult = getReactions(this)
|
||||
likeTriggleState = newTriggle,
|
||||
selectionResult = Pair(likeNegative, getReactions(this, null, newTriggle))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getReactions(state: QuickReactionState): List<String> {
|
||||
private fun getReactions(state: QuickReactionState, newState1: TriggleState?, newState2: TriggleState?): List<String> {
|
||||
return ArrayList<String>(4).apply {
|
||||
when (state.likeTriggleState) {
|
||||
when (newState2 ?: state.likeTriggleState) {
|
||||
TriggleState.FIRST -> add(likePositive)
|
||||
TriggleState.SECOND -> add(likeNegative)
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
when (state.agreeTrigleState) {
|
||||
when (newState1 ?: state.agreeTrigleState) {
|
||||
TriggleState.FIRST -> add(agreePositive)
|
||||
TriggleState.SECOND -> add(agreeNegative)
|
||||
else -> {
|
||||
@ -99,10 +105,47 @@ class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel
|
||||
|
||||
companion object : MvRxViewModelFactory<QuickReactionViewModel, QuickReactionState> {
|
||||
|
||||
val agreePositive = "👍"
|
||||
val agreeNegative = "👎"
|
||||
val likePositive = "🙂"
|
||||
val likeNegative = "😔"
|
||||
|
||||
fun getOpposite(reaction: String): String? {
|
||||
return when (reaction) {
|
||||
agreePositive -> agreeNegative
|
||||
agreeNegative -> agreePositive
|
||||
likePositive -> likeNegative
|
||||
likeNegative -> likePositive
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
override fun initialState(viewModelContext: ViewModelContext): QuickReactionState? {
|
||||
// Args are accessible from the context.
|
||||
// val foo = vieWModelContext.args<MyArgs>.foo
|
||||
return QuickReactionState(TriggleState.NONE, TriggleState.NONE)
|
||||
val currentSession = viewModelContext.activity.get<Session>()
|
||||
val parcel = viewModelContext.args as MessageActionsBottomSheet.ParcelableArgs
|
||||
val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId)
|
||||
?: return null
|
||||
var agreeTriggle: TriggleState = TriggleState.NONE
|
||||
var likeTriggle: TriggleState = TriggleState.NONE
|
||||
event.annotations?.reactionsSummary?.forEach {
|
||||
//it.addedByMe
|
||||
if (it.addedByMe) {
|
||||
if (agreePositive == it.key) {
|
||||
agreeTriggle = TriggleState.FIRST
|
||||
} else if (agreeNegative == it.key) {
|
||||
agreeTriggle = TriggleState.SECOND
|
||||
}
|
||||
|
||||
if (likePositive == it.key) {
|
||||
likeTriggle = TriggleState.FIRST
|
||||
} else if (likeNegative == it.key) {
|
||||
likeTriggle = TriggleState.SECOND
|
||||
}
|
||||
}
|
||||
}
|
||||
return QuickReactionState(agreeTriggle, likeTriggle, null, event.root.eventId ?: "")
|
||||
}
|
||||
}
|
||||
}
|
@ -19,7 +19,6 @@ package im.vector.riotredesign.features.home.room.detail.timeline.factory
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotredesign.R
|
||||
@ -50,7 +49,7 @@ class CallItemFactory(private val stringProvider: StringProvider) {
|
||||
}
|
||||
EventType.CALL_ANSWER == event.getClearType() -> stringProvider.getString(R.string.notice_answered_call, senderName)
|
||||
EventType.CALL_HANGUP == event.getClearType() -> stringProvider.getString(R.string.notice_ended_call, senderName)
|
||||
else -> null
|
||||
else -> null
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -17,12 +17,19 @@
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline.factory
|
||||
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.TextPaint
|
||||
import android.text.style.ClickableSpan
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.RelativeSizeSpan
|
||||
import android.view.View
|
||||
import androidx.annotation.ColorRes
|
||||
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.RelationType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
|
||||
import im.vector.matrix.android.api.session.room.model.message.*
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
@ -31,8 +38,10 @@ import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.riotredesign.core.extensions.localDateTime
|
||||
import im.vector.riotredesign.core.linkify.VectorLinkify
|
||||
import im.vector.riotredesign.core.resources.ColorProvider
|
||||
import im.vector.riotredesign.core.resources.StringProvider
|
||||
import im.vector.riotredesign.core.utils.DebouncedClickListener
|
||||
import im.vector.riotredesign.features.home.AvatarRenderer
|
||||
import im.vector.riotredesign.features.home.getColorFromUserId
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||
@ -45,7 +54,8 @@ import me.gujun.android.span.span
|
||||
class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
|
||||
private val timelineDateFormatter: TimelineDateFormatter,
|
||||
private val htmlRenderer: EventHtmlRenderer) {
|
||||
private val htmlRenderer: EventHtmlRenderer,
|
||||
private val stringProvider: StringProvider) {
|
||||
|
||||
fun create(event: TimelineEvent,
|
||||
nextEvent: TimelineEvent?,
|
||||
@ -66,29 +76,57 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
|| nextEvent?.root?.getClearType() != EventType.MESSAGE
|
||||
|| isNextMessageReceivedMoreThanOneHourAgo
|
||||
|
||||
val messageContent: MessageContent = event.root.getClearContent().toModel() ?: return null
|
||||
val time = timelineDateFormatter.formatMessageHour(date)
|
||||
val avatarUrl = event.senderAvatar
|
||||
val memberName = event.senderName ?: event.root.sender ?: ""
|
||||
val formattedMemberName = span(memberName) {
|
||||
textColor = colorProvider.getColor(AvatarRenderer.getColorFromUserId(event.root.sender ?: ""))
|
||||
textColor = colorProvider.getColor(getColorFromUserId(event.root.sender
|
||||
?: ""))
|
||||
}
|
||||
val hasBeenEdited = event.annotations?.editSummary != null
|
||||
val informationData = MessageInformationData(eventId = eventId,
|
||||
senderId = event.root.sender ?: "",
|
||||
sendState = event.sendState,
|
||||
time = time,
|
||||
avatarUrl = avatarUrl,
|
||||
memberName = formattedMemberName,
|
||||
showInformation = showInformation)
|
||||
showInformation = showInformation,
|
||||
orderedReactionList = event.annotations?.reactionsSummary?.map {
|
||||
ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty())
|
||||
},
|
||||
hasBeenEdited = hasBeenEdited
|
||||
)
|
||||
|
||||
//Test for reactions UX
|
||||
//informationData.orderedReactionList = listOf( Triple("👍",1,false), Triple("👎",2,false))
|
||||
if (event.root.unsignedData?.redactedEvent != null) {
|
||||
//message is redacted
|
||||
return buildRedactedItem(informationData, callback)
|
||||
}
|
||||
|
||||
val messageContent: MessageContent =
|
||||
event.annotations?.editSummary?.aggregatedContent?.toModel()
|
||||
?: event.root.getClearContent().toModel()
|
||||
?: //Malformed content, we should echo something on screen
|
||||
return DefaultItem_().text(stringProvider.getString(R.string.malformed_message))
|
||||
|
||||
if (messageContent.relatesTo?.type == RelationType.REPLACE) {
|
||||
// ignore replace event, the targeted id is already edited
|
||||
return BlankItem_()
|
||||
}
|
||||
// val all = event.root.toContent()
|
||||
// val ev = all.toModel<Event>()
|
||||
return when (messageContent) {
|
||||
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, callback)
|
||||
is MessageTextContent -> buildTextMessageItem(event.sendState, messageContent, informationData, callback)
|
||||
is MessageEmoteContent -> buildEmoteMessageItem(messageContent,
|
||||
informationData,
|
||||
hasBeenEdited,
|
||||
event.annotations?.editSummary,
|
||||
callback)
|
||||
is MessageTextContent -> buildTextMessageItem(event.sendState,
|
||||
messageContent,
|
||||
informationData,
|
||||
hasBeenEdited,
|
||||
event.annotations?.editSummary,
|
||||
callback
|
||||
)
|
||||
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, callback)
|
||||
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, callback)
|
||||
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, callback)
|
||||
@ -98,12 +136,14 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildAudioMessageItem(messageContent: MessageAudioContent, informationData: MessageInformationData,
|
||||
private fun buildAudioMessageItem(messageContent: MessageAudioContent,
|
||||
informationData: MessageInformationData,
|
||||
callback: TimelineEventController.Callback?): MessageFileItem? {
|
||||
return MessageFileItem_()
|
||||
.informationData(informationData)
|
||||
.filename(messageContent.body)
|
||||
.iconRes(R.drawable.filetype_audio)
|
||||
.reactionPillCallback(callback)
|
||||
.avatarClickListener(
|
||||
DebouncedClickListener(View.OnClickListener { view ->
|
||||
callback?.onAvatarClicked(informationData)
|
||||
@ -126,11 +166,13 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildFileMessageItem(messageContent: MessageFileContent, informationData: MessageInformationData,
|
||||
private fun buildFileMessageItem(messageContent: MessageFileContent,
|
||||
informationData: MessageInformationData,
|
||||
callback: TimelineEventController.Callback?): MessageFileItem? {
|
||||
return MessageFileItem_()
|
||||
.informationData(informationData)
|
||||
.filename(messageContent.body)
|
||||
.reactionPillCallback(callback)
|
||||
.iconRes(R.drawable.filetype_attachment)
|
||||
.avatarClickListener(
|
||||
DebouncedClickListener(View.OnClickListener { view ->
|
||||
@ -159,7 +201,8 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
return DefaultItem_().text(text)
|
||||
}
|
||||
|
||||
private fun buildImageMessageItem(messageContent: MessageImageContent, informationData: MessageInformationData,
|
||||
private fun buildImageMessageItem(messageContent: MessageImageContent,
|
||||
informationData: MessageInformationData,
|
||||
callback: TimelineEventController.Callback?): MessageImageVideoItem? {
|
||||
|
||||
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
|
||||
@ -177,6 +220,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
.playable(messageContent.info?.mimeType == "image/gif")
|
||||
.informationData(informationData)
|
||||
.mediaData(data)
|
||||
.reactionPillCallback(callback)
|
||||
.avatarClickListener(
|
||||
DebouncedClickListener(View.OnClickListener { view ->
|
||||
callback?.onAvatarClicked(informationData)
|
||||
@ -193,14 +237,14 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
DebouncedClickListener(View.OnClickListener { view ->
|
||||
callback?.onEventCellClicked(informationData, messageContent, view)
|
||||
}))
|
||||
|
||||
.longClickListener { view ->
|
||||
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
||||
?: false
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildVideoMessageItem(messageContent: MessageVideoContent, informationData: MessageInformationData,
|
||||
private fun buildVideoMessageItem(messageContent: MessageVideoContent,
|
||||
informationData: MessageInformationData,
|
||||
callback: TimelineEventController.Callback?): MessageImageVideoItem? {
|
||||
|
||||
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
|
||||
@ -223,6 +267,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
.playable(true)
|
||||
.informationData(informationData)
|
||||
.mediaData(thumbnailData)
|
||||
.reactionPillCallback(callback)
|
||||
.avatarClickListener(
|
||||
DebouncedClickListener(View.OnClickListener { view ->
|
||||
callback?.onAvatarClicked(informationData)
|
||||
@ -242,18 +287,30 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildTextMessageItem(sendState: SendState, messageContent: MessageTextContent,
|
||||
private fun buildTextMessageItem(sendState: SendState,
|
||||
messageContent: MessageTextContent,
|
||||
informationData: MessageInformationData,
|
||||
hasBeenEdited: Boolean,
|
||||
editSummary: EditAggregatedSummary?,
|
||||
callback: TimelineEventController.Callback?): MessageTextItem? {
|
||||
|
||||
val bodyToUse = messageContent.formattedBody?.let {
|
||||
htmlRenderer.render(it)
|
||||
htmlRenderer.render(it.trim())
|
||||
} ?: messageContent.body
|
||||
|
||||
val linkifiedBody = linkifyBody(bodyToUse, callback)
|
||||
|
||||
return MessageTextItem_()
|
||||
.message(linkifiedBody)
|
||||
.apply {
|
||||
if (hasBeenEdited) {
|
||||
val spannable = annotateWithEdited(linkifiedBody, callback, informationData, editSummary)
|
||||
message(spannable)
|
||||
} else {
|
||||
message(linkifiedBody)
|
||||
}
|
||||
}
|
||||
.informationData(informationData)
|
||||
.reactionPillCallback(callback)
|
||||
.avatarClickListener(
|
||||
DebouncedClickListener(View.OnClickListener { view ->
|
||||
callback?.onAvatarClicked(informationData)
|
||||
@ -262,11 +319,6 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
DebouncedClickListener(View.OnClickListener { view ->
|
||||
callback?.onMemberNameClicked(informationData)
|
||||
}))
|
||||
//click on the text
|
||||
.clickListener(
|
||||
DebouncedClickListener(View.OnClickListener { view ->
|
||||
callback?.onEventCellClicked(informationData, messageContent, view)
|
||||
}))
|
||||
.cellClickListener(
|
||||
DebouncedClickListener(View.OnClickListener { view ->
|
||||
callback?.onEventCellClicked(informationData, messageContent, view)
|
||||
@ -277,13 +329,48 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNoticeMessageItem(messageContent: MessageNoticeContent, informationData: MessageInformationData,
|
||||
private fun annotateWithEdited(linkifiedBody: CharSequence,
|
||||
callback: TimelineEventController.Callback?,
|
||||
informationData: MessageInformationData,
|
||||
editSummary: EditAggregatedSummary?): SpannableStringBuilder {
|
||||
val spannable = SpannableStringBuilder()
|
||||
spannable.append(linkifiedBody)
|
||||
// TODO i18n
|
||||
val editedSuffix = "(edited)"
|
||||
spannable.append(" ").append(editedSuffix)
|
||||
val color = colorProvider.getColorFromAttribute(R.attr.vctr_list_header_secondary_text_color)
|
||||
val editStart = spannable.indexOf(editedSuffix)
|
||||
val editEnd = editStart + editedSuffix.length
|
||||
spannable.setSpan(
|
||||
ForegroundColorSpan(color),
|
||||
editStart,
|
||||
editEnd,
|
||||
Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||
|
||||
spannable.setSpan(RelativeSizeSpan(.9f), editStart, editEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||
spannable.setSpan(object : ClickableSpan() {
|
||||
override fun onClick(widget: View?) {
|
||||
callback?.onEditedDecorationClicked(informationData, editSummary)
|
||||
}
|
||||
|
||||
override fun updateDrawState(ds: TextPaint?) {
|
||||
//nop
|
||||
}
|
||||
},
|
||||
editStart,
|
||||
editEnd,
|
||||
Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||
return spannable
|
||||
}
|
||||
|
||||
private fun buildNoticeMessageItem(messageContent: MessageNoticeContent,
|
||||
informationData: MessageInformationData,
|
||||
callback: TimelineEventController.Callback?): MessageTextItem? {
|
||||
|
||||
val message = messageContent.body.let {
|
||||
val formattedBody = span {
|
||||
text = it
|
||||
textColor = colorProvider.getColor(R.color.slate_grey)
|
||||
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
|
||||
textStyle = "italic"
|
||||
}
|
||||
linkifyBody(formattedBody, callback)
|
||||
@ -291,6 +378,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
return MessageTextItem_()
|
||||
.message(message)
|
||||
.informationData(informationData)
|
||||
.reactionPillCallback(callback)
|
||||
.avatarClickListener(
|
||||
DebouncedClickListener(View.OnClickListener { view ->
|
||||
callback?.onAvatarClicked(informationData)
|
||||
@ -309,7 +397,10 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildEmoteMessageItem(messageContent: MessageEmoteContent, informationData: MessageInformationData,
|
||||
private fun buildEmoteMessageItem(messageContent: MessageEmoteContent,
|
||||
informationData: MessageInformationData,
|
||||
hasBeenEdited: Boolean,
|
||||
editSummary: EditAggregatedSummary?,
|
||||
callback: TimelineEventController.Callback?): MessageTextItem? {
|
||||
|
||||
val message = messageContent.body.let {
|
||||
@ -317,8 +408,16 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
linkifyBody(formattedBody, callback)
|
||||
}
|
||||
return MessageTextItem_()
|
||||
.message(message)
|
||||
.apply {
|
||||
if (hasBeenEdited) {
|
||||
val spannable = annotateWithEdited(message, callback, informationData, editSummary)
|
||||
message(spannable)
|
||||
} else {
|
||||
message(message)
|
||||
}
|
||||
}
|
||||
.informationData(informationData)
|
||||
.reactionPillCallback(callback)
|
||||
.avatarClickListener(
|
||||
DebouncedClickListener(View.OnClickListener { view ->
|
||||
callback?.onAvatarClicked(informationData)
|
||||
@ -337,6 +436,20 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildRedactedItem(informationData: MessageInformationData,
|
||||
callback: TimelineEventController.Callback?): RedactedMessageItem? {
|
||||
return RedactedMessageItem_()
|
||||
.informationData(informationData)
|
||||
.avatarClickListener(
|
||||
DebouncedClickListener(View.OnClickListener { view ->
|
||||
callback?.onAvatarClicked(informationData)
|
||||
}))
|
||||
.memberClickListener(
|
||||
DebouncedClickListener(View.OnClickListener { view ->
|
||||
callback?.onMemberNameClicked(informationData)
|
||||
}))
|
||||
}
|
||||
|
||||
private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): CharSequence {
|
||||
val spannable = SpannableStringBuilder(body)
|
||||
MatrixLinkify.addLinks(spannable, object : MatrixPermalinkSpan.Callback {
|
||||
@ -347,32 +460,4 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
VectorLinkify.addLinks(spannable, true)
|
||||
return spannable
|
||||
}
|
||||
|
||||
//Based on riot-web implementation
|
||||
@ColorRes
|
||||
private fun getColorFor(sender: String): Int {
|
||||
var hash = 0
|
||||
var i = 0
|
||||
var chr: Char
|
||||
if (sender.isEmpty()) {
|
||||
return R.color.username_1
|
||||
}
|
||||
while (i < sender.length) {
|
||||
chr = sender[i]
|
||||
hash = (hash shl 5) - hash + chr.toInt()
|
||||
hash = hash or 0
|
||||
i++
|
||||
}
|
||||
val cI = Math.abs(hash) % 8 + 1
|
||||
return when (cI) {
|
||||
1 -> R.color.username_1
|
||||
2 -> R.color.username_2
|
||||
3 -> R.color.username_3
|
||||
4 -> R.color.username_4
|
||||
5 -> R.color.username_5
|
||||
6 -> R.color.username_6
|
||||
7 -> R.color.username_7
|
||||
else -> R.color.username_8
|
||||
}
|
||||
}
|
||||
}
|
@ -16,28 +16,24 @@
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline.factory
|
||||
|
||||
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
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.format.NoticeEventFormatter
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderAvatar
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderName
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_
|
||||
|
||||
class RoomTopicItemFactory(private val stringProvider: StringProvider) {
|
||||
class NoticeItemFactory(private val eventFormatter: NoticeEventFormatter) {
|
||||
|
||||
fun create(event: TimelineEvent): NoticeItem? {
|
||||
val formattedText = eventFormatter.format(event) ?: return null
|
||||
val senderName = event.senderName()
|
||||
val senderAvatar = event.senderAvatar()
|
||||
|
||||
val content: RoomTopicContent = event.root.content.toModel() ?: return null
|
||||
val text = if (content.topic.isNullOrEmpty()) {
|
||||
stringProvider.getString(R.string.notice_room_topic_removed, event.senderName)
|
||||
} else {
|
||||
stringProvider.getString(R.string.notice_room_topic_changed, event.senderName, content.topic)
|
||||
}
|
||||
return NoticeItem_()
|
||||
.noticeText(text)
|
||||
.avatarUrl(event.senderAvatar)
|
||||
.memberName(event.senderName)
|
||||
.noticeText(formattedText)
|
||||
.avatarUrl(senderAvatar)
|
||||
.memberName(senderName)
|
||||
}
|
||||
|
||||
|
@ -1,135 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline.factory
|
||||
|
||||
import android.text.TextUtils
|
||||
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.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.resources.StringProvider
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.RoomMemberEventHelper
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_
|
||||
|
||||
|
||||
//TODO : complete with call membership events¬
|
||||
class RoomMemberItemFactory(private val stringProvider: StringProvider) {
|
||||
|
||||
fun create(event: TimelineEvent): NoticeItem? {
|
||||
val eventContent: RoomMember? = event.root.content.toModel()
|
||||
val prevEventContent: RoomMember? = event.root.prevContent.toModel()
|
||||
val noticeText = buildRoomMemberNotice(event, eventContent, prevEventContent) ?: return null
|
||||
val senderAvatar = RoomMemberEventHelper.senderAvatar(eventContent, prevEventContent, event)
|
||||
val senderName = RoomMemberEventHelper.senderName(eventContent, prevEventContent, event)
|
||||
|
||||
return NoticeItem_()
|
||||
.userId(event.root.sender ?: "")
|
||||
.noticeText(noticeText)
|
||||
.avatarUrl(senderAvatar)
|
||||
.memberName(senderName)
|
||||
}
|
||||
|
||||
private fun buildRoomMemberNotice(event: TimelineEvent, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
|
||||
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.senderName)
|
||||
}
|
||||
displayText.append(displayAvatarText)
|
||||
}
|
||||
return displayText.toString()
|
||||
}
|
||||
|
||||
private fun buildMembershipNotice(event: TimelineEvent, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
|
||||
val senderDisplayName = event.senderName ?: event.root.sender
|
||||
val targetDisplayName = eventContent?.displayName ?: event.root.sender
|
||||
return when {
|
||||
Membership.INVITE == eventContent?.membership -> {
|
||||
// TODO get userId
|
||||
val selfUserId = ""
|
||||
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 {
|
||||
val leftDisplayName = RoomMemberEventHelper.senderName(eventContent, prevEventContent, event)
|
||||
stringProvider.getString(R.string.notice_room_leave, leftDisplayName)
|
||||
}
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline.factory
|
||||
|
||||
import android.text.TextUtils
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.RoomNameContent
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.resources.StringProvider
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_
|
||||
|
||||
class RoomNameItemFactory(private val stringProvider: StringProvider) {
|
||||
|
||||
fun create(event: TimelineEvent): NoticeItem? {
|
||||
|
||||
val content: RoomNameContent = event.root.content.toModel() ?: return null
|
||||
val text = if (!TextUtils.isEmpty(content.name)) {
|
||||
stringProvider.getString(R.string.notice_room_name_changed, event.senderName, content.name)
|
||||
} else {
|
||||
stringProvider.getString(R.string.notice_room_name_removed, event.senderName)
|
||||
}
|
||||
return NoticeItem_()
|
||||
.noticeText(text)
|
||||
.avatarUrl(event.senderAvatar)
|
||||
.memberName(event.senderName)
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -24,13 +24,9 @@ import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventCo
|
||||
import timber.log.Timber
|
||||
|
||||
class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
|
||||
private val roomNameItemFactory: RoomNameItemFactory,
|
||||
private val roomTopicItemFactory: RoomTopicItemFactory,
|
||||
private val roomMemberItemFactory: RoomMemberItemFactory,
|
||||
private val roomHistoryVisibilityItemFactory: RoomHistoryVisibilityItemFactory,
|
||||
private val callItemFactory: CallItemFactory,
|
||||
private val encryptionItemFactory: EncryptionItemFactory,
|
||||
private val encryptedItemFactory: EncryptedItemFactory,
|
||||
private val noticeItemFactory: NoticeItemFactory,
|
||||
private val defaultItemFactory: DefaultItemFactory) {
|
||||
|
||||
fun create(event: TimelineEvent,
|
||||
@ -39,29 +35,31 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
|
||||
|
||||
val computedModel = try {
|
||||
when (event.root.getClearType()) {
|
||||
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)
|
||||
EventType.STATE_HISTORY_VISIBILITY -> roomHistoryVisibilityItemFactory.create(event)
|
||||
|
||||
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, callback)
|
||||
// State and call
|
||||
EventType.STATE_ROOM_NAME,
|
||||
EventType.STATE_ROOM_TOPIC,
|
||||
EventType.STATE_ROOM_MEMBER,
|
||||
EventType.STATE_HISTORY_VISIBILITY,
|
||||
EventType.CALL_INVITE,
|
||||
EventType.CALL_HANGUP,
|
||||
EventType.CALL_ANSWER -> callItemFactory.create(event)
|
||||
EventType.CALL_ANSWER -> noticeItemFactory.create(event)
|
||||
|
||||
EventType.ENCRYPTION -> encryptionItemFactory.create(event)
|
||||
|
||||
EventType.ENCRYPTED -> encryptedItemFactory.create(event)
|
||||
// Crypto
|
||||
EventType.ENCRYPTION -> encryptionItemFactory.create(event)
|
||||
EventType.ENCRYPTED -> encryptedItemFactory.create(event)
|
||||
|
||||
// Unhandled event types (yet)
|
||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
||||
EventType.STICKER,
|
||||
EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event)
|
||||
|
||||
else -> null
|
||||
EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event)
|
||||
else -> {
|
||||
Timber.w("Ignored event (type: ${event.root.type}")
|
||||
null
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error")
|
||||
|
||||
Timber.e(e, "failed to create message item")
|
||||
defaultItemFactory.create(event, e)
|
||||
}
|
||||
return (computedModel ?: EmptyItem_())
|
||||
|
@ -0,0 +1,182 @@
|
||||
/*
|
||||
* 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.format
|
||||
|
||||
import android.text.TextUtils
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.*
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.resources.StringProvider
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderName
|
||||
import timber.log.Timber
|
||||
|
||||
class NoticeEventFormatter(private val stringProvider: StringProvider) {
|
||||
|
||||
fun format(timelineEvent: TimelineEvent): CharSequence? {
|
||||
return when (val type = timelineEvent.root.getClearType()) {
|
||||
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.senderName)
|
||||
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.senderName)
|
||||
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderName())
|
||||
EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderName)
|
||||
EventType.CALL_INVITE,
|
||||
EventType.CALL_HANGUP,
|
||||
EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.senderName)
|
||||
else -> {
|
||||
Timber.v("Type $type not handled by this formatter")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatRoomNameEvent(event: Event, senderName: String?): CharSequence? {
|
||||
val content = event.getClearContent().toModel<RoomNameContent>() ?: return null
|
||||
return if (!TextUtils.isEmpty(content.name)) {
|
||||
stringProvider.getString(R.string.notice_room_name_changed, senderName, content.name)
|
||||
} else {
|
||||
stringProvider.getString(R.string.notice_room_name_removed, senderName)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatRoomTopicEvent(event: Event, senderName: String?): CharSequence? {
|
||||
val content = event.getClearContent().toModel<RoomTopicContent>() ?: return null
|
||||
return if (content.topic.isNullOrEmpty()) {
|
||||
stringProvider.getString(R.string.notice_room_topic_removed, senderName)
|
||||
} else {
|
||||
stringProvider.getString(R.string.notice_room_topic_changed, senderName, content.topic)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?): CharSequence? {
|
||||
val historyVisibility = event.getClearContent().toModel<RoomHistoryVisibilityContent>()?.historyVisibility
|
||||
?: return null
|
||||
|
||||
val formattedVisibility = when (historyVisibility) {
|
||||
RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared)
|
||||
RoomHistoryVisibility.INVITED -> stringProvider.getString(R.string.notice_room_visibility_invited)
|
||||
RoomHistoryVisibility.JOINED -> stringProvider.getString(R.string.notice_room_visibility_joined)
|
||||
RoomHistoryVisibility.WORLD_READABLE -> stringProvider.getString(R.string.notice_room_visibility_world_readable)
|
||||
}
|
||||
return stringProvider.getString(R.string.notice_made_future_room_visibility, senderName, formattedVisibility)
|
||||
}
|
||||
|
||||
private fun formatCallEvent(event: Event, senderName: String?): CharSequence? {
|
||||
return when {
|
||||
EventType.CALL_INVITE == event.type -> {
|
||||
val content = event.getClearContent().toModel<CallInviteContent>() ?: return null
|
||||
val isVideoCall = content.offer.sdp == CallInviteContent.Offer.SDP_VIDEO
|
||||
return if (isVideoCall) {
|
||||
stringProvider.getString(R.string.notice_placed_video_call, senderName)
|
||||
} else {
|
||||
stringProvider.getString(R.string.notice_placed_voice_call, senderName)
|
||||
}
|
||||
}
|
||||
EventType.CALL_ANSWER == event.type -> stringProvider.getString(R.string.notice_answered_call, senderName)
|
||||
EventType.CALL_HANGUP == event.type -> stringProvider.getString(R.string.notice_ended_call, senderName)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatRoomMemberEvent(event: Event, senderName: String?): String? {
|
||||
val eventContent: RoomMember? = event.getClearContent().toModel()
|
||||
val prevEventContent: RoomMember? = event.prevContent.toModel()
|
||||
val isMembershipEvent = prevEventContent?.membership != eventContent?.membership
|
||||
return if (isMembershipEvent) {
|
||||
buildMembershipNotice(event, senderName, eventContent, prevEventContent)
|
||||
} else {
|
||||
buildProfileNotice(event, senderName, eventContent, prevEventContent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildProfileNotice(event: Event, senderName: String?, 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.sender, eventContent?.displayName)
|
||||
eventContent?.displayName.isNullOrEmpty() ->
|
||||
stringProvider.getString(R.string.notice_display_name_removed, event.sender, prevEventContent?.displayName)
|
||||
else ->
|
||||
stringProvider.getString(R.string.notice_display_name_changed_from,
|
||||
event.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, senderName)
|
||||
}
|
||||
displayText.append(displayAvatarText)
|
||||
}
|
||||
return displayText.toString()
|
||||
}
|
||||
|
||||
private fun buildMembershipNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
|
||||
val senderDisplayName = senderName ?: event.sender
|
||||
val targetDisplayName = eventContent?.displayName ?: event.sender
|
||||
return when {
|
||||
Membership.INVITE == eventContent?.membership -> {
|
||||
// TODO get userId
|
||||
val selfUserId = ""
|
||||
when {
|
||||
eventContent.thirdPartyInvite != null ->
|
||||
stringProvider.getString(R.string.notice_room_third_party_registered_invite,
|
||||
targetDisplayName, eventContent.thirdPartyInvite?.displayName)
|
||||
TextUtils.equals(event.stateKey, selfUserId) ->
|
||||
stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName)
|
||||
event.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.sender, event.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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -16,25 +16,11 @@
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline.helper
|
||||
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
|
||||
object RoomMemberEventHelper {
|
||||
|
||||
fun senderAvatar(eventContent: RoomMember?, prevEventContent: RoomMember?, event: TimelineEvent): String? {
|
||||
return if (eventContent?.membership == Membership.LEAVE && eventContent.avatarUrl == null && prevEventContent?.avatarUrl != null) {
|
||||
prevEventContent.avatarUrl
|
||||
} else {
|
||||
event.senderAvatar
|
||||
}
|
||||
}
|
||||
|
||||
fun senderName(eventContent: RoomMember?, prevEventContent: RoomMember?, event: TimelineEvent): String? {
|
||||
return if (eventContent?.membership == Membership.LEAVE && eventContent.displayName == null && prevEventContent?.displayName != null) {
|
||||
prevEventContent.displayName
|
||||
} else {
|
||||
event.senderName
|
||||
}
|
||||
}
|
||||
}
|
@ -17,6 +17,10 @@
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline.helper
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.RelationType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotredesign.core.extensions.localDateTime
|
||||
|
||||
@ -40,13 +44,44 @@ object TimelineDisplayableEvents {
|
||||
}
|
||||
|
||||
fun TimelineEvent.isDisplayable(): Boolean {
|
||||
return TimelineDisplayableEvents.DISPLAYABLE_TYPES.contains(root.getClearType()) && !root.content.isNullOrEmpty()
|
||||
if (!TimelineDisplayableEvents.DISPLAYABLE_TYPES.contains(root.type)) {
|
||||
return false
|
||||
}
|
||||
if (root.content.isNullOrEmpty()) {
|
||||
return false
|
||||
}
|
||||
//Edits should be filtered out!
|
||||
if (EventType.MESSAGE == root.type
|
||||
&& root.content.toModel<MessageContent>()?.relatesTo?.type == RelationType.REPLACE) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
//
|
||||
//fun List<TimelineEvent>.filterDisplayableEvents(): List<TimelineEvent> {
|
||||
// return this.filter {
|
||||
// it.isDisplayable()
|
||||
// }
|
||||
//}
|
||||
|
||||
fun TimelineEvent.senderAvatar(): String? {
|
||||
// We might have no avatar when user leave, so we try to get it from prevContent
|
||||
return senderAvatar
|
||||
?: if (root.type == EventType.STATE_ROOM_MEMBER) {
|
||||
root.prevContent.toModel<RoomMember>()?.avatarUrl
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun List<TimelineEvent>.filterDisplayableEvents(): List<TimelineEvent> {
|
||||
return this.filter {
|
||||
it.isDisplayable()
|
||||
}
|
||||
fun TimelineEvent.senderName(): String? {
|
||||
// We might have no senderName when user leave, so we try to get it from prevContent
|
||||
return senderName
|
||||
?: if (root.type == EventType.STATE_ROOM_MEMBER) {
|
||||
root.prevContent.toModel<RoomMember>()?.displayName
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun TimelineEvent.canBeMerged(): Boolean {
|
||||
|
@ -19,6 +19,7 @@ package im.vector.riotredesign.features.home.room.detail.timeline.item
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewStub
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.helper.widget.Flow
|
||||
@ -29,6 +30,7 @@ import com.airbnb.epoxy.EpoxyAttribute
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.utils.DimensionUtils.dpToPx
|
||||
import im.vector.riotredesign.features.home.AvatarRenderer
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.riotredesign.features.reactions.widget.ReactionButton
|
||||
|
||||
|
||||
@ -48,6 +50,19 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
||||
@EpoxyAttribute
|
||||
var memberClickListener: View.OnClickListener? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var reactionPillCallback: TimelineEventController.ReactionPillCallback? = null
|
||||
|
||||
var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener {
|
||||
override fun onReacted(reactionButton: ReactionButton) {
|
||||
reactionPillCallback?.onClickOnReactionPill(informationData, reactionButton.reactionString, true)
|
||||
}
|
||||
|
||||
override fun onUnReacted(reactionButton: ReactionButton) {
|
||||
reactionPillCallback?.onClickOnReactionPill(informationData, reactionButton.reactionString, false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun bind(holder: H) {
|
||||
super.bind(holder)
|
||||
if (informationData.showInformation) {
|
||||
@ -65,42 +80,60 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
||||
holder.timeView.text = informationData.time
|
||||
holder.memberNameView.text = informationData.memberName
|
||||
AvatarRenderer.render(informationData.avatarUrl, informationData.senderId, informationData.memberName?.toString(), holder.avatarImageView)
|
||||
holder.view.setOnClickListener(cellClickListener)
|
||||
holder.view.setOnLongClickListener(longClickListener)
|
||||
holder.avatarImageView.setOnLongClickListener(longClickListener)
|
||||
holder.memberNameView.setOnLongClickListener(longClickListener)
|
||||
} else {
|
||||
holder.avatarImageView.setOnClickListener(null)
|
||||
holder.memberNameView.setOnClickListener(null)
|
||||
holder.avatarImageView.visibility = View.GONE
|
||||
holder.memberNameView.visibility = View.GONE
|
||||
holder.timeView.visibility = View.GONE
|
||||
holder.view.setOnClickListener(null)
|
||||
holder.view.setOnLongClickListener(null)
|
||||
holder.avatarImageView.setOnLongClickListener(null)
|
||||
holder.memberNameView.setOnLongClickListener(null)
|
||||
}
|
||||
holder.view.setOnClickListener(cellClickListener)
|
||||
holder.view.setOnLongClickListener(longClickListener)
|
||||
|
||||
if (informationData.orderedReactionList.isNullOrEmpty()) {
|
||||
holder.reactionWrapper.isVisible = false
|
||||
if (!shouldShowReactionAtBottom() || informationData.orderedReactionList.isNullOrEmpty()) {
|
||||
holder.reactionWrapper?.isVisible = false
|
||||
} else {
|
||||
holder.reactionWrapper.isVisible = true
|
||||
//inflate if needed
|
||||
if (holder.reactionFlowHelper == null) {
|
||||
holder.reactionWrapper = holder.view.findViewById<ViewStub>(R.id.messageBottomInfo).inflate() as? ViewGroup
|
||||
holder.reactionFlowHelper = holder.view.findViewById(R.id.reactionsFlowHelper)
|
||||
}
|
||||
holder.reactionWrapper?.isVisible = true
|
||||
//clear all reaction buttons (but not the Flow helper!)
|
||||
holder.reactionWrapper.children.forEach { (it as? ReactionButton)?.isGone = true }
|
||||
holder.reactionWrapper?.children?.forEach { (it as? ReactionButton)?.isGone = true }
|
||||
val idToRefInFlow = ArrayList<Int>()
|
||||
informationData.orderedReactionList?.forEachIndexed { index, reaction ->
|
||||
(holder.reactionWrapper.children.elementAt(index) as? ReactionButton)?.let { reactionButton ->
|
||||
informationData.orderedReactionList?.chunked(7)?.firstOrNull()?.forEachIndexed { index, reaction ->
|
||||
(holder.reactionWrapper?.children?.elementAtOrNull(index) as? ReactionButton)?.let { reactionButton ->
|
||||
reactionButton.isVisible = true
|
||||
reactionButton.reactedListener = reactionClickListener
|
||||
reactionButton.setTag(R.id.messageBottomInfo, reaction.key)
|
||||
idToRefInFlow.add(reactionButton.id)
|
||||
reactionButton.reactionString = reaction.first
|
||||
reactionButton.reactionCount = reaction.second
|
||||
reactionButton.setChecked(reaction.third)
|
||||
reactionButton.reactionString = reaction.key
|
||||
reactionButton.reactionCount = reaction.count
|
||||
reactionButton.setChecked(reaction.addedByMe)
|
||||
reactionButton.isEnabled = reaction.synced
|
||||
}
|
||||
}
|
||||
// Just setting the view as gone will break the FlowHelper (and invisible will take too much space),
|
||||
// so have to update ref ids
|
||||
holder.reactionFlowHelper.referencedIds = idToRefInFlow.toIntArray()
|
||||
holder.reactionFlowHelper?.referencedIds = idToRefInFlow.toIntArray()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && !holder.view.isInLayout) {
|
||||
holder.reactionFlowHelper.requestLayout()
|
||||
holder.reactionFlowHelper?.requestLayout()
|
||||
}
|
||||
|
||||
holder.reactionWrapper?.setOnLongClickListener(longClickListener)
|
||||
}
|
||||
}
|
||||
|
||||
open fun shouldShowReactionAtBottom(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
protected fun View.renderSendState() {
|
||||
isClickable = informationData.sendState.isSent()
|
||||
alpha = if (informationData.sendState.isSent()) 1f else 0.5f
|
||||
@ -112,8 +145,8 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
||||
val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
||||
val timeView by bind<TextView>(R.id.messageTimeView)
|
||||
|
||||
val reactionWrapper: ViewGroup by bind(R.id.messageBottomInfo)
|
||||
val reactionFlowHelper: Flow by bind(R.id.reactionsFlowHelper)
|
||||
var reactionWrapper: ViewGroup? = null
|
||||
var reactionFlowHelper: Flow? = null
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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.item
|
||||
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
||||
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_blank_stub)
|
||||
abstract class BlankItem : VectorEpoxyModel<BlankItem.BlankHolder>() {
|
||||
class BlankHolder : VectorEpoxyHolder()
|
||||
}
|
@ -40,6 +40,6 @@ abstract class DefaultItem : BaseEventItem<DefaultItem.Holder>() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val STUB_ID = R.id.messageContentDefaultStub
|
||||
private const val STUB_ID = R.id.messageContentDefaultStub
|
||||
}
|
||||
}
|
@ -43,6 +43,8 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
|
||||
ContentUploadStateTrackerBinder.bind(informationData.eventId, mediaData, holder.progressLayout)
|
||||
holder.imageView.setOnClickListener(clickListener)
|
||||
holder.imageView.setOnLongClickListener(longClickListener)
|
||||
holder.mediaContentView.setOnClickListener(cellClickListener)
|
||||
holder.mediaContentView.setOnLongClickListener(longClickListener)
|
||||
holder.imageView.renderSendState()
|
||||
holder.playContentView.visibility = if (playable) View.VISIBLE else View.GONE
|
||||
}
|
||||
@ -62,6 +64,8 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
|
||||
val imageView by bind<ImageView>(R.id.messageThumbnailView)
|
||||
val playContentView by bind<ImageView>(R.id.messageMediaPlayView)
|
||||
|
||||
val mediaContentView by bind<ViewGroup>(R.id.messageContentMedia)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -16,9 +16,8 @@
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline.item
|
||||
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
|
||||
import android.os.Parcelable
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@ -31,5 +30,15 @@ data class MessageInformationData(
|
||||
val memberName: CharSequence? = null,
|
||||
val showInformation: Boolean = true,
|
||||
/*List of reactions (emoji,count,isSelected)*/
|
||||
var orderedReactionList: List<Triple<String,Int,Boolean>>? = null
|
||||
) : Parcelable
|
||||
var orderedReactionList: List<ReactionInfoData>? = null,
|
||||
var hasBeenEdited: Boolean = false
|
||||
) : Parcelable
|
||||
|
||||
|
||||
@Parcelize
|
||||
data class ReactionInfoData(
|
||||
val key: String,
|
||||
val count: Int,
|
||||
val addedByMe: Boolean,
|
||||
val synced: Boolean
|
||||
) : Parcelable
|
||||
|
@ -16,23 +16,19 @@
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline.item
|
||||
|
||||
import android.text.Spannable
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import androidx.core.text.PrecomputedTextCompat
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.matrix.android.api.permalinks.MatrixLinkify
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.features.html.PillImageSpan
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.saket.bettermovementmethod.BetterLinkMovementMethod
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||
abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
||||
@ -41,18 +37,29 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
||||
var message: CharSequence? = null
|
||||
@EpoxyAttribute
|
||||
override lateinit var informationData: MessageInformationData
|
||||
@EpoxyAttribute
|
||||
var clickListener: View.OnClickListener? = null
|
||||
|
||||
val mvmtMethod = BetterLinkMovementMethod.newInstance().also {
|
||||
it.setOnLinkClickListener { textView, url ->
|
||||
//Return false to let android manage the click on the link
|
||||
false
|
||||
}
|
||||
it.setOnLinkLongClickListener { textView, url ->
|
||||
//Long clicks are handled by parent, return true to block android to do something with url
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
MatrixLinkify.addLinkMovementMethod(holder.messageView)
|
||||
|
||||
holder.messageView.movementMethod = mvmtMethod
|
||||
|
||||
val textFuture = PrecomputedTextCompat.getTextFuture(message ?: "",
|
||||
TextViewCompat.getTextMetricsParams(holder.messageView),
|
||||
null)
|
||||
holder.messageView.setTextFuture(textFuture)
|
||||
holder.messageView.renderSendState()
|
||||
holder.messageView.setOnClickListener (clickListener)
|
||||
holder.messageView.setOnClickListener(cellClickListener)
|
||||
holder.messageView.setOnLongClickListener(longClickListener)
|
||||
findPillsAndProcess { it.bind(holder.messageView) }
|
||||
}
|
||||
|
@ -57,6 +57,6 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val STUB_ID = R.id.messageContentNoticeStub
|
||||
private const val STUB_ID = R.id.messageContentNoticeStub
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline.item
|
||||
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotredesign.R
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||
abstract class RedactedMessageItem : AbsMessageItem<RedactedMessageItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
override lateinit var informationData: MessageInformationData
|
||||
|
||||
override fun getStubType(): Int = STUB_ID
|
||||
|
||||
override fun shouldShowReactionAtBottom() = false
|
||||
|
||||
class Holder : AbsMessageItem.Holder() {
|
||||
override fun getStubId(): Int = STUB_ID
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val STUB_ID = R.id.messageContentRedactedStub
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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 AlphabeticalRoomComparator
|
||||
: Comparator<RoomSummary> {
|
||||
|
||||
override fun compare(leftRoomSummary: RoomSummary?, rightRoomSummary: RoomSummary?): Int {
|
||||
return when {
|
||||
rightRoomSummary?.displayName == null -> -1
|
||||
leftRoomSummary?.displayName == null -> 1
|
||||
else -> leftRoomSummary.displayName.compareTo(rightRoomSummary.displayName)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user