forked from GitHub-Mirror/riotX-android
Rename app
to vector
This commit is contained in:
37
vector/src/main/AndroidManifest.xml
Normal file
37
vector/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="im.vector.riotredesign">
|
||||
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:name=".Riot"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme.Light"
|
||||
tools:replace="android:allowBackup">
|
||||
|
||||
<activity
|
||||
android:name=".features.MainActivity"
|
||||
android:theme="@style/Theme.Riot.Splash">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".features.home.HomeActivity" />
|
||||
<activity android:name=".features.login.LoginActivity" />
|
||||
<activity android:name=".features.media.MediaViewerActivity" />
|
||||
<activity
|
||||
android:name=".features.rageshake.BugReportActivity"
|
||||
android:label="@string/title_activity_bug_report" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
69
vector/src/main/java/im/vector/riotredesign/Riot.kt
Normal file
69
vector/src/main/java/im/vector/riotredesign/Riot.kt
Normal file
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.multidex.MultiDex
|
||||
import com.airbnb.epoxy.EpoxyAsyncUtil
|
||||
import com.airbnb.epoxy.EpoxyController
|
||||
import com.facebook.stetho.Stetho
|
||||
import com.github.piasy.biv.BigImageViewer
|
||||
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.home.HomeModule
|
||||
import im.vector.riotredesign.features.rageshake.VectorFileLogger
|
||||
import im.vector.riotredesign.features.rageshake.VectorUncaughtExceptionHandler
|
||||
import org.koin.log.EmptyLogger
|
||||
import org.koin.standalone.StandAloneContext.startKoin
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
class Riot : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
VectorUncaughtExceptionHandler.activate(this)
|
||||
|
||||
// Log
|
||||
VectorFileLogger.init(this)
|
||||
Timber.plant(Timber.DebugTree(), VectorFileLogger)
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Stetho.initializeWithDefaults(this)
|
||||
}
|
||||
|
||||
AndroidThreeTen.init(this)
|
||||
BigImageViewer.initialize(GlideImageLoader.with(applicationContext))
|
||||
EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
|
||||
EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
|
||||
val appModule = AppModule(applicationContext).definition
|
||||
val homeModule = HomeModule().definition
|
||||
startKoin(listOf(appModule, homeModule), logger = EmptyLogger())
|
||||
|
||||
Matrix.getInstance().setApplicationFlavor(BuildConfig.FLAVOR_DESCRIPTION)
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base)
|
||||
MultiDex.install(this)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.core.di
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Context.MODE_PRIVATE
|
||||
import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.riotredesign.core.resources.ColorProvider
|
||||
import im.vector.riotredesign.core.resources.LocaleProvider
|
||||
import im.vector.riotredesign.core.resources.StringProvider
|
||||
import im.vector.riotredesign.features.home.group.SelectedGroupStore
|
||||
import im.vector.riotredesign.features.home.room.VisibleRoomStore
|
||||
import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository
|
||||
import im.vector.riotredesign.features.home.room.list.RoomSummaryComparator
|
||||
import org.koin.dsl.module.module
|
||||
|
||||
class AppModule(private val context: Context) {
|
||||
|
||||
val definition = module {
|
||||
|
||||
single {
|
||||
LocaleProvider(context.resources)
|
||||
}
|
||||
|
||||
single {
|
||||
StringProvider(context.resources)
|
||||
}
|
||||
|
||||
single {
|
||||
ColorProvider(context)
|
||||
}
|
||||
|
||||
single {
|
||||
context.getSharedPreferences("im.vector.riot", MODE_PRIVATE)
|
||||
}
|
||||
|
||||
single {
|
||||
RoomSelectionRepository(get())
|
||||
}
|
||||
|
||||
single {
|
||||
SelectedGroupStore()
|
||||
}
|
||||
|
||||
single {
|
||||
VisibleRoomStore()
|
||||
}
|
||||
|
||||
single {
|
||||
RoomSummaryComparator()
|
||||
}
|
||||
|
||||
factory {
|
||||
Matrix.getInstance().currentSession!!
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -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.core.epoxy
|
||||
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotredesign.R
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_empty)
|
||||
abstract class EmptyItem : RiotEpoxyModel<EmptyItem.Holder>() {
|
||||
class Holder : RiotEpoxyHolder()
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.core.epoxy
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import im.vector.riotredesign.core.platform.DefaultListUpdateCallback
|
||||
import im.vector.riotredesign.core.platform.Restorable
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
private const val LAYOUT_MANAGER_STATE = "LAYOUT_MANAGER_STATE"
|
||||
|
||||
class LayoutManagerStateRestorer(private val layoutManager: RecyclerView.LayoutManager) : Restorable, DefaultListUpdateCallback {
|
||||
|
||||
private var layoutManagerState = AtomicReference<Parcelable?>()
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
val layoutManagerState = layoutManager.onSaveInstanceState()
|
||||
outState.putParcelable(LAYOUT_MANAGER_STATE, layoutManagerState)
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
|
||||
val parcelable = savedInstanceState?.getParcelable<Parcelable>(LAYOUT_MANAGER_STATE)
|
||||
layoutManagerState.set(parcelable)
|
||||
}
|
||||
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
layoutManagerState.getAndSet(null)?.also {
|
||||
layoutManager.onRestoreInstanceState(it)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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.content.Context
|
||||
import android.widget.ProgressBar
|
||||
import com.airbnb.epoxy.ModelView
|
||||
|
||||
@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
|
||||
class LoadingItem(context: Context) : ProgressBar(context)
|
@ -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.epoxy
|
||||
|
||||
import android.view.View
|
||||
import com.airbnb.epoxy.EpoxyHolder
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
/**
|
||||
* A pattern for easier view binding with an [EpoxyHolder]
|
||||
*
|
||||
* See [SampleKotlinModelWithHolder] for a usage example.
|
||||
*/
|
||||
abstract class RiotEpoxyHolder : EpoxyHolder() {
|
||||
private lateinit var view: View
|
||||
|
||||
override fun bindView(itemView: View) {
|
||||
view = itemView
|
||||
}
|
||||
|
||||
protected fun <V : View> bind(id: Int): ReadOnlyProperty<RiotEpoxyHolder, V> =
|
||||
Lazy { holder: RiotEpoxyHolder, prop ->
|
||||
holder.view.findViewById(id) as V?
|
||||
?: throw IllegalStateException("View ID $id for '${prop.name}' not found.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Taken from Kotterknife.
|
||||
* https://github.com/JakeWharton/kotterknife
|
||||
*/
|
||||
private class Lazy<V>(
|
||||
private val initializer: (RiotEpoxyHolder, KProperty<*>) -> V
|
||||
) : ReadOnlyProperty<RiotEpoxyHolder, V> {
|
||||
private object EMPTY
|
||||
|
||||
private var value: Any? = EMPTY
|
||||
|
||||
override fun getValue(thisRef: RiotEpoxyHolder, property: KProperty<*>): V {
|
||||
if (value == EMPTY) {
|
||||
value = initializer(thisRef, property)
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return value as V
|
||||
}
|
||||
}
|
||||
}
|
@ -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.epoxy
|
||||
|
||||
import com.airbnb.epoxy.EpoxyModelWithHolder
|
||||
import com.airbnb.epoxy.VisibilityState
|
||||
|
||||
abstract class RiotEpoxyModel<H : RiotEpoxyHolder> : EpoxyModelWithHolder<H>() {
|
||||
|
||||
private var onModelVisibilityStateChangedListener: OnVisibilityStateChangedListener? = null
|
||||
|
||||
override fun onVisibilityStateChanged(visibilityState: Int, view: H) {
|
||||
onModelVisibilityStateChangedListener?.onVisibilityStateChanged(visibilityState)
|
||||
super.onVisibilityStateChanged(visibilityState, view)
|
||||
}
|
||||
|
||||
fun setOnVisibilityStateChanged(listener: OnVisibilityStateChangedListener): RiotEpoxyModel<H> {
|
||||
this.onModelVisibilityStateChangedListener = listener
|
||||
return this
|
||||
}
|
||||
|
||||
interface OnVisibilityStateChangedListener {
|
||||
fun onVisibilityStateChanged(@VisibilityState.Visibility visibilityState: Int)
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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 androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
fun AppCompatActivity.addFragment(fragment: Fragment, frameId: Int) {
|
||||
supportFragmentManager.inTransaction { add(frameId, fragment) }
|
||||
}
|
||||
|
||||
fun AppCompatActivity.replaceFragment(fragment: Fragment, frameId: Int) {
|
||||
supportFragmentManager.inTransaction { replace(frameId, fragment) }
|
||||
}
|
||||
|
||||
fun AppCompatActivity.addFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
|
||||
supportFragmentManager.inTransaction { replace(frameId, fragment).addToBackStack(tag) }
|
||||
}
|
||||
|
||||
fun AppCompatActivity.hideKeyboard() {
|
||||
currentFocus?.hideKeyboard()
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2018 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.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
fun Boolean.toOnOff() = if (this) "ON" else "OFF"
|
||||
|
||||
/**
|
||||
* Apply argument to a Fragment
|
||||
*/
|
||||
fun <T : Fragment> T.withArgs(block: Bundle.() -> Unit) = apply { arguments = Bundle().apply(block) }
|
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.text.Editable
|
||||
import android.text.InputType
|
||||
import android.text.TextWatcher
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.EditText
|
||||
import im.vector.riotredesign.R
|
||||
|
||||
fun EditText.setupAsSearch() {
|
||||
addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(editable: Editable?) {
|
||||
val clearIcon = if (editable?.isNotEmpty() == true) R.drawable.ic_clear_white else 0
|
||||
setCompoundDrawablesWithIntrinsicBounds(0, 0, clearIcon, 0)
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
|
||||
})
|
||||
|
||||
maxLines = 1
|
||||
inputType = InputType.TYPE_CLASS_TEXT
|
||||
imeOptions = EditorInfo.IME_ACTION_SEARCH
|
||||
setOnEditorActionListener { _, actionId, event ->
|
||||
var consumed = false
|
||||
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
|
||||
hideKeyboard()
|
||||
consumed = true
|
||||
}
|
||||
consumed
|
||||
}
|
||||
|
||||
setOnTouchListener(View.OnTouchListener { _, event ->
|
||||
if (event.action == MotionEvent.ACTION_UP) {
|
||||
if (event.rawX >= (this.right - this.compoundPaddingRight)) {
|
||||
text = null
|
||||
return@OnTouchListener true
|
||||
}
|
||||
}
|
||||
return@OnTouchListener false
|
||||
})
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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 im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.riotredesign.core.resources.DateProvider
|
||||
import org.threeten.bp.LocalDateTime
|
||||
|
||||
fun Event.localDateTime(): LocalDateTime {
|
||||
return DateProvider.toLocalDateTime(originServerTs)
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.core.extensions
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
fun androidx.fragment.app.Fragment.addFragment(fragment: Fragment, frameId: Int) {
|
||||
fragmentManager?.inTransaction { add(frameId, fragment) }
|
||||
}
|
||||
|
||||
fun androidx.fragment.app.Fragment.replaceFragment(fragment: Fragment, frameId: Int) {
|
||||
fragmentManager?.inTransaction { replace(frameId, fragment) }
|
||||
}
|
||||
|
||||
fun androidx.fragment.app.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) {
|
||||
childFragmentManager.inTransaction { add(frameId, fragment) }
|
||||
}
|
||||
|
||||
fun androidx.fragment.app.Fragment.replaceChildFragment(fragment: Fragment, frameId: Int) {
|
||||
childFragmentManager.inTransaction { replace(frameId, fragment) }
|
||||
}
|
||||
|
||||
fun androidx.fragment.app.Fragment.addChildFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
|
||||
childFragmentManager.inTransaction { replace(frameId, fragment).addToBackStack(tag) }
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.core.extensions
|
||||
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
|
||||
inline fun androidx.fragment.app.FragmentManager.inTransaction(func: FragmentTransaction.() -> FragmentTransaction) {
|
||||
beginTransaction().func().commit()
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright 2019 New Vector Ltd
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.core.extensions
|
||||
|
||||
/**
|
||||
* Returns the last element yielding the smallest value of the given function or `null` if there are no elements.
|
||||
*/
|
||||
public inline fun <T, R : Comparable<R>> Iterable<T>.lastMinBy(selector: (T) -> R): T? {
|
||||
val iterator = iterator()
|
||||
if (!iterator.hasNext()) return null
|
||||
var minElem = iterator.next()
|
||||
var minValue = selector(minElem)
|
||||
while (iterator.hasNext()) {
|
||||
val e = iterator.next()
|
||||
val v = selector(e)
|
||||
if (minValue >= v) {
|
||||
minElem = e
|
||||
minValue = v
|
||||
}
|
||||
}
|
||||
return minElem
|
||||
}
|
@ -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.core.extensions
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import im.vector.riotredesign.core.utils.LiveEvent
|
||||
import im.vector.riotredesign.core.utils.EventObserver
|
||||
|
||||
inline fun <T> LiveData<T>.observeK(owner: LifecycleOwner, crossinline observer: (T?) -> Unit) {
|
||||
this.observe(owner, Observer { observer(it) })
|
||||
}
|
||||
|
||||
inline fun <T> LiveData<T>.observeNotNull(owner: LifecycleOwner, crossinline observer: (T) -> Unit) {
|
||||
this.observe(owner, Observer { it?.run(observer) })
|
||||
}
|
||||
|
||||
inline fun <T> LiveData<LiveEvent<T>>.observeEvent(owner: LifecycleOwner, crossinline observer: (T) -> Unit) {
|
||||
this.observe(owner, EventObserver { it.run(observer) })
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.core.extensions
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
|
||||
fun View.hideKeyboard() {
|
||||
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(windowToken, 0)
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.glide;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.bumptech.glide.GlideBuilder;
|
||||
import com.bumptech.glide.annotation.GlideModule;
|
||||
import com.bumptech.glide.module.AppGlideModule;
|
||||
|
||||
@GlideModule
|
||||
public final class MyAppGlideModule extends AppGlideModule {
|
||||
|
||||
@Override
|
||||
public void applyOptions(Context context, GlideBuilder builder) {
|
||||
builder.setLogLevel(Log.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 android.widget.FrameLayout
|
||||
|
||||
class CheckableFrameLayout : FrameLayout, 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,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.platform
|
||||
|
||||
import androidx.recyclerview.widget.ListUpdateCallback
|
||||
|
||||
interface DefaultListUpdateCallback : ListUpdateCallback {
|
||||
|
||||
override fun onChanged(position: Int, count: Int, tag: Any?) {
|
||||
//no-op
|
||||
}
|
||||
|
||||
override fun onMoved(position: Int, count: Int) {
|
||||
//no-op
|
||||
}
|
||||
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
//no-op
|
||||
}
|
||||
|
||||
override fun onRemoved(position: Int, count: Int) {
|
||||
//no-op
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.core.platform
|
||||
|
||||
interface OnBackPressed {
|
||||
|
||||
/**
|
||||
* Returns true, if the on back pressed event has been handled by this Fragment.
|
||||
* Otherwise return false
|
||||
*/
|
||||
fun onBackPressed(): Boolean
|
||||
}
|
@ -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.core.platform
|
||||
|
||||
import android.os.Bundle
|
||||
|
||||
interface Restorable {
|
||||
|
||||
fun onSaveInstanceState(outState: Bundle)
|
||||
|
||||
fun onRestoreInstanceState(savedInstanceState: Bundle?)
|
||||
|
||||
}
|
@ -0,0 +1,272 @@
|
||||
/*
|
||||
* 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.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.annotation.*
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import butterknife.BindView
|
||||
import butterknife.ButterKnife
|
||||
import butterknife.Unbinder
|
||||
import com.airbnb.mvrx.BaseMvRxActivity
|
||||
import com.bumptech.glide.util.Util
|
||||
import im.vector.riotredesign.BuildConfig
|
||||
import im.vector.riotredesign.R
|
||||
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.ThemeUtils
|
||||
import im.vector.riotredesign.receivers.DebugReceiver
|
||||
import im.vector.ui.themes.ActivityOtherThemes
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.disposables.Disposable
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
abstract class RiotActivity : BaseMvRxActivity() {
|
||||
/* ==========================================================================================
|
||||
* UI
|
||||
* ========================================================================================== */
|
||||
|
||||
@Nullable
|
||||
@BindView(R.id.toolbar)
|
||||
protected lateinit var toolbar: Toolbar
|
||||
|
||||
/* ==========================================================================================
|
||||
* DATA
|
||||
* ========================================================================================== */
|
||||
|
||||
private var unBinder: Unbinder? = null
|
||||
|
||||
private var savedInstanceState: Bundle? = null
|
||||
|
||||
// For debug only
|
||||
private var debugReceiver: DebugReceiver? = null
|
||||
|
||||
private val uiDisposables = CompositeDisposable()
|
||||
private val restorables = ArrayList<Restorable>()
|
||||
|
||||
private var rageShake: RageShake? = null
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
restorables.forEach { it.onSaveInstanceState(outState) }
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
|
||||
restorables.forEach { it.onRestoreInstanceState(savedInstanceState) }
|
||||
super.onRestoreInstanceState(savedInstanceState)
|
||||
}
|
||||
|
||||
@MainThread
|
||||
protected fun <T : Restorable> T.register(): T {
|
||||
Util.assertMainThread()
|
||||
restorables.add(this)
|
||||
return this
|
||||
}
|
||||
|
||||
protected fun Disposable.disposeOnDestroy(): Disposable {
|
||||
uiDisposables.add(this)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Shake detector
|
||||
rageShake = RageShake(this)
|
||||
|
||||
ThemeUtils.setActivityTheme(this, getOtherThemes())
|
||||
|
||||
doBeforeSetContentView()
|
||||
|
||||
if (getLayoutRes() != -1) {
|
||||
setContentView(getLayoutRes())
|
||||
}
|
||||
|
||||
unBinder = ButterKnife.bind(this)
|
||||
|
||||
this.savedInstanceState = savedInstanceState
|
||||
|
||||
initUiAndData()
|
||||
|
||||
val titleRes = getTitleRes()
|
||||
if (titleRes != -1) {
|
||||
supportActionBar?.let {
|
||||
it.setTitle(titleRes)
|
||||
} ?: run {
|
||||
setTitle(titleRes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
unBinder?.unbind()
|
||||
unBinder = null
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (this !is BugReportActivity) {
|
||||
rageShake?.start()
|
||||
}
|
||||
|
||||
DebugReceiver
|
||||
.getIntentFilter(this)
|
||||
.takeIf { BuildConfig.DEBUG }
|
||||
?.let {
|
||||
debugReceiver = DebugReceiver()
|
||||
registerReceiver(debugReceiver, it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
rageShake?.stop()
|
||||
|
||||
debugReceiver?.let {
|
||||
unregisterReceiver(debugReceiver)
|
||||
debugReceiver = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
|
||||
if (hasFocus && displayInFullscreen()) {
|
||||
setFullScreen()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean, newConfig: Configuration?) {
|
||||
super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig)
|
||||
|
||||
Timber.w("onMultiWindowModeChanged. isInMultiWindowMode: $isInMultiWindowMode")
|
||||
BugReporter.inMultiWindowMode = isInMultiWindowMode
|
||||
}
|
||||
|
||||
|
||||
/* ==========================================================================================
|
||||
* PRIVATE METHODS
|
||||
* ========================================================================================== */
|
||||
|
||||
/**
|
||||
* Force to render the activity in fullscreen
|
||||
*/
|
||||
private fun setFullScreen() {
|
||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* MENU MANAGEMENT
|
||||
* ========================================================================================== */
|
||||
|
||||
final override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
val menuRes = getMenuRes()
|
||||
|
||||
if (menuRes != -1) {
|
||||
menuInflater.inflate(menuRes, menu)
|
||||
ThemeUtils.tintMenuIcons(menu, ThemeUtils.getColor(this, getMenuTint()))
|
||||
return true
|
||||
}
|
||||
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* PROTECTED METHODS
|
||||
* ========================================================================================== */
|
||||
|
||||
/**
|
||||
* Get the saved instance state.
|
||||
* Ensure {@link isFirstCreation()} returns false before calling this
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
protected fun getSavedInstanceState(): Bundle {
|
||||
return savedInstanceState!!
|
||||
}
|
||||
|
||||
/**
|
||||
* Is first creation
|
||||
*
|
||||
* @return true if Activity is created for the first time (and not restored by the system)
|
||||
*/
|
||||
protected fun isFirstCreation() = savedInstanceState == null
|
||||
|
||||
/**
|
||||
* Configure the Toolbar. It MUST be present in your layout with id "toolbar"
|
||||
*/
|
||||
protected fun configureToolbar() {
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
supportActionBar?.let {
|
||||
it.setDisplayShowHomeEnabled(true)
|
||||
it.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* OPEN METHODS
|
||||
* ========================================================================================== */
|
||||
|
||||
@LayoutRes
|
||||
open fun getLayoutRes() = -1
|
||||
|
||||
open fun displayInFullscreen() = false
|
||||
|
||||
open fun doBeforeSetContentView() = Unit
|
||||
|
||||
open fun initUiAndData() = Unit
|
||||
|
||||
@StringRes
|
||||
open fun getTitleRes() = -1
|
||||
|
||||
@MenuRes
|
||||
open fun getMenuRes() = -1
|
||||
|
||||
@AttrRes
|
||||
open fun getMenuTint() = R.attr.vctr_icon_tint_on_dark_action_bar_color
|
||||
|
||||
/**
|
||||
* Return a object containing other themes for this activity
|
||||
*/
|
||||
open fun getOtherThemes(): ActivityOtherThemes = ActivityOtherThemes.Default
|
||||
}
|
@ -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.core.platform
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.MainThread
|
||||
import com.airbnb.mvrx.BaseMvRxFragment
|
||||
import com.airbnb.mvrx.MvRx
|
||||
import com.bumptech.glide.util.Util.assertMainThread
|
||||
|
||||
abstract class RiotFragment : BaseMvRxFragment(), OnBackPressed {
|
||||
|
||||
val riotActivity: RiotActivity by lazy {
|
||||
activity as RiotActivity
|
||||
}
|
||||
|
||||
private val restorables = ArrayList<Restorable>()
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
restorables.forEach { it.onSaveInstanceState(outState) }
|
||||
}
|
||||
|
||||
override fun onViewStateRestored(savedInstanceState: Bundle?) {
|
||||
restorables.forEach { it.onRestoreInstanceState(savedInstanceState) }
|
||||
super.onViewStateRestored(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onBackPressed(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun invalidate() {
|
||||
//no-ops by default
|
||||
}
|
||||
|
||||
protected fun setArguments(args: Parcelable? = null) {
|
||||
arguments = args?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } }
|
||||
}
|
||||
|
||||
@MainThread
|
||||
protected fun <T : Restorable> T.register(): T {
|
||||
assertMainThread()
|
||||
restorables.add(this)
|
||||
return this
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.core.platform
|
||||
|
||||
import com.airbnb.mvrx.BaseMvRxViewModel
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
|
||||
abstract class RiotViewModel<S : MvRxState>(initialState: S)
|
||||
: BaseMvRxViewModel<S>(initialState, debugMode = false)
|
97
vector/src/main/java/im/vector/riotredesign/core/platform/StateView.kt
Executable file
97
vector/src/main/java/im/vector/riotredesign/core/platform/StateView.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.FrameLayout
|
||||
import im.vector.riotredesign.R
|
||||
import kotlinx.android.synthetic.main.view_state.view.*
|
||||
|
||||
class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0)
|
||||
: FrameLayout(context, attrs, defStyle) {
|
||||
|
||||
sealed class State {
|
||||
object Content : State()
|
||||
object Loading : State()
|
||||
data class Empty(val message: CharSequence? = null) : State()
|
||||
data class Error(val message: CharSequence? = null) : State()
|
||||
}
|
||||
|
||||
|
||||
private var eventCallback: EventCallback? = null
|
||||
|
||||
var contentView: View? = null
|
||||
|
||||
var state: State = State.Empty()
|
||||
set(newState) {
|
||||
if (newState != state) {
|
||||
update(newState)
|
||||
}
|
||||
}
|
||||
|
||||
interface EventCallback {
|
||||
fun onRetryClicked()
|
||||
}
|
||||
|
||||
init {
|
||||
View.inflate(context, R.layout.view_state, this)
|
||||
layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
||||
errorRetryView.setOnClickListener {
|
||||
eventCallback?.onRetryClicked()
|
||||
}
|
||||
state = State.Content
|
||||
}
|
||||
|
||||
|
||||
private fun update(newState: State) {
|
||||
when (newState) {
|
||||
is StateView.State.Content -> {
|
||||
progressBar.visibility = View.INVISIBLE
|
||||
errorView.visibility = View.INVISIBLE
|
||||
emptyView.visibility = View.INVISIBLE
|
||||
contentView?.visibility = View.VISIBLE
|
||||
}
|
||||
is StateView.State.Loading -> {
|
||||
progressBar.visibility = View.VISIBLE
|
||||
errorView.visibility = View.INVISIBLE
|
||||
emptyView.visibility = View.INVISIBLE
|
||||
contentView?.visibility = View.INVISIBLE
|
||||
}
|
||||
is StateView.State.Empty -> {
|
||||
progressBar.visibility = View.INVISIBLE
|
||||
errorView.visibility = View.INVISIBLE
|
||||
emptyView.visibility = View.VISIBLE
|
||||
emptyMessageView.text = newState.message
|
||||
if (contentView != null) {
|
||||
contentView!!.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
is StateView.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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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.appcompat.widget.Toolbar
|
||||
|
||||
interface ToolbarConfigurable {
|
||||
|
||||
fun configure(toolbar: Toolbar)
|
||||
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright 2019 New Vector Ltd
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.core.resources
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
class ColorProvider(private val context: Context) {
|
||||
|
||||
fun getColor(@ColorRes colorRes: Int): Int {
|
||||
return ContextCompat.getColor(context, colorRes)
|
||||
}
|
||||
|
||||
}
|
@ -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.core.resources
|
||||
|
||||
import org.threeten.bp.Instant
|
||||
import org.threeten.bp.LocalDateTime
|
||||
import org.threeten.bp.ZoneId
|
||||
|
||||
object DateProvider {
|
||||
|
||||
private val zoneId = ZoneId.systemDefault()
|
||||
|
||||
fun toLocalDateTime(timestamp: Long?): LocalDateTime {
|
||||
val instant = Instant.ofEpochMilli(timestamp ?: 0)
|
||||
return LocalDateTime.ofInstant(instant, zoneId)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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.core.os.ConfigurationCompat
|
||||
import java.util.*
|
||||
|
||||
class LocaleProvider(private val resources: Resources) {
|
||||
|
||||
fun current(): Locale {
|
||||
return ConfigurationCompat.getLocales(resources.configuration)[0]
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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.resources
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.annotation.NonNull
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
class StringProvider(private val resources: Resources) {
|
||||
|
||||
/**
|
||||
* Returns a localized string from the application's package's
|
||||
* default string table.
|
||||
*
|
||||
* @param resId Resource id for the string
|
||||
* @return The string data associated with the resource, stripped of styled
|
||||
* text information.
|
||||
*/
|
||||
@NonNull
|
||||
fun getString(@StringRes resId: Int): String {
|
||||
return resources.getString(resId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a localized formatted string from the application's package's
|
||||
* default string table, substituting the format arguments as defined in
|
||||
* [java.util.Formatter] and [java.lang.String.format].
|
||||
*
|
||||
* @param resId Resource id for the format string
|
||||
* @param formatArgs The format arguments that will be used for
|
||||
* substitution.
|
||||
* @return The string data associated with the resource, formatted and
|
||||
* stripped of styled text information.
|
||||
*/
|
||||
@NonNull
|
||||
fun getString(@StringRes resId: Int, vararg formatArgs: Any?): String {
|
||||
return resources.getString(resId, *formatArgs)
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2018 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.utils
|
||||
|
||||
import android.content.Context
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
// Implementation should return true in case of success
|
||||
typealias ActionOnFile = (file: File) -> Boolean
|
||||
|
||||
/* ==========================================================================================
|
||||
* Delete
|
||||
* ========================================================================================== */
|
||||
|
||||
fun deleteAllFiles(context: Context) {
|
||||
Timber.v("Delete cache dir:")
|
||||
recursiveActionOnFile(context.cacheDir, ::deleteAction)
|
||||
|
||||
Timber.v("Delete files dir:")
|
||||
recursiveActionOnFile(context.filesDir, ::deleteAction)
|
||||
}
|
||||
|
||||
private fun deleteAction(file: File): Boolean {
|
||||
if (file.exists()) {
|
||||
Timber.v("deleteFile: $file")
|
||||
return file.delete()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* Log
|
||||
* ========================================================================================== */
|
||||
|
||||
fun lsFiles(context: Context) {
|
||||
Timber.v("Content of cache dir:")
|
||||
recursiveActionOnFile(context.cacheDir, ::logAction)
|
||||
|
||||
Timber.v("Content of files dir:")
|
||||
recursiveActionOnFile(context.filesDir, ::logAction)
|
||||
}
|
||||
|
||||
private fun logAction(file: File): Boolean {
|
||||
if (file.isDirectory) {
|
||||
Timber.d(file.toString())
|
||||
} else {
|
||||
Timber.d(file.toString() + " " + file.length() + " bytes")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* Private
|
||||
* ========================================================================================== */
|
||||
|
||||
/**
|
||||
* Return true in case of success
|
||||
*/
|
||||
private fun recursiveActionOnFile(file: File, action: ActionOnFile): Boolean {
|
||||
if (file.isDirectory) {
|
||||
file.list().forEach {
|
||||
val result = recursiveActionOnFile(File(file, it), action)
|
||||
|
||||
if (!result) {
|
||||
// Break the loop
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return action.invoke(file)
|
||||
}
|
||||
|
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.utils
|
||||
|
||||
import androidx.lifecycle.Observer
|
||||
|
||||
open class LiveEvent<out T>(private val content: T) {
|
||||
|
||||
var hasBeenHandled = false
|
||||
private set // Allow external read but not write
|
||||
|
||||
/**
|
||||
* Returns the content and prevents its use again.
|
||||
*/
|
||||
fun getContentIfNotHandled(): T? {
|
||||
return if (hasBeenHandled) {
|
||||
null
|
||||
} else {
|
||||
hasBeenHandled = true
|
||||
content
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content, even if it's already been handled.
|
||||
*/
|
||||
fun peekContent(): T = content
|
||||
}
|
||||
|
||||
/**
|
||||
* An [Observer] for [LiveEvent]s, simplifying the pattern of checking if the [LiveEvent]'s content has
|
||||
* already been handled.
|
||||
*
|
||||
* [onEventUnhandledContent] is *only* called if the [LiveEvent]'s contents has not been handled.
|
||||
*/
|
||||
class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit) : Observer<LiveEvent<T>> {
|
||||
override fun onChanged(event: LiveEvent<T>?) {
|
||||
event?.getContentIfNotHandled()?.let { value ->
|
||||
onEventUnhandledContent(value)
|
||||
}
|
||||
}
|
||||
}
|
@ -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.utils
|
||||
|
||||
import com.jakewharton.rxrelay2.BehaviorRelay
|
||||
import io.reactivex.Observable
|
||||
|
||||
open class RxStore<T>(defaultValue: T? = null) {
|
||||
|
||||
private val storeSubject: BehaviorRelay<T> = if (defaultValue == null) {
|
||||
BehaviorRelay.create<T>()
|
||||
} else {
|
||||
BehaviorRelay.createDefault(defaultValue)
|
||||
}
|
||||
|
||||
fun observe(): Observable<T> {
|
||||
return storeSubject.hide().distinctUntilChanged()
|
||||
}
|
||||
|
||||
fun post(value: T) {
|
||||
storeSubject.accept(value)
|
||||
}
|
||||
}
|
@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright 2018 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.utils
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.app.Activity
|
||||
import android.content.*
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.features.settings.VectorLocale
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Tells if the application ignores battery optimizations.
|
||||
*
|
||||
* Ignoring them allows the app to run in background to make background sync with the homeserver.
|
||||
* This user option appears on Android M but Android O enforces its usage and kills apps not
|
||||
* authorised by the user to run in background.
|
||||
*
|
||||
* @param context the context
|
||||
* @return true if battery optimisations are ignored
|
||||
*/
|
||||
fun isIgnoringBatteryOptimizations(context: Context): Boolean {
|
||||
// no issue before Android M, battery optimisations did not exist
|
||||
return Build.VERSION.SDK_INT < Build.VERSION_CODES.M
|
||||
|| (context.getSystemService(Context.POWER_SERVICE) as PowerManager?)?.isIgnoringBatteryOptimizations(context.packageName) == true
|
||||
}
|
||||
|
||||
/**
|
||||
* display the system dialog for granting this permission. If previously granted, the
|
||||
* system will not show it (so you should call this method).
|
||||
*
|
||||
* Note: If the user finally does not grant the permission, PushManager.isBackgroundSyncAllowed()
|
||||
* will return false and the notification privacy will fallback to "LOW_DETAIL".
|
||||
*/
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
fun requestDisablingBatteryOptimization(activity: Activity, fragment: Fragment?, requestCode: Int) {
|
||||
val intent = Intent()
|
||||
intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
||||
intent.data = Uri.parse("package:" + activity.packageName)
|
||||
if (fragment != null) {
|
||||
fragment.startActivityForResult(intent, requestCode)
|
||||
} else {
|
||||
activity.startActivityForResult(intent, requestCode)
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// Clipboard helper
|
||||
//==============================================================================================================
|
||||
|
||||
/**
|
||||
* Copy a text to the clipboard, and display a Toast when done
|
||||
*
|
||||
* @param context the context
|
||||
* @param text the text to copy
|
||||
*/
|
||||
fun copyToClipboard(context: Context, text: CharSequence) {
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboard.primaryClip = ClipData.newPlainText("", text)
|
||||
context.toast(R.string.copied_to_clipboard)
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the device locale
|
||||
*
|
||||
* @return the device locale
|
||||
*/
|
||||
fun getDeviceLocale(context: Context): Locale {
|
||||
var locale: Locale
|
||||
|
||||
locale = try {
|
||||
val packageManager = context.packageManager
|
||||
val resources = packageManager.getResourcesForApplication("android")
|
||||
resources.configuration.locale
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## getDeviceLocale() failed " + e.message)
|
||||
// Fallback to application locale
|
||||
VectorLocale.applicationLocale
|
||||
}
|
||||
|
||||
return locale
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows notification settings for the current app.
|
||||
* In android O will directly opens the notification settings, in lower version it will show the App settings
|
||||
*/
|
||||
fun startNotificationSettingsIntent(fragment: Fragment, requestCode: Int) {
|
||||
val intent = Intent()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, fragment.context?.packageName)
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
|
||||
intent.putExtra("app_package", fragment.context?.packageName)
|
||||
intent.putExtra("app_uid", fragment.context?.applicationInfo?.uid)
|
||||
} else {
|
||||
intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
|
||||
intent.addCategory(Intent.CATEGORY_DEFAULT);
|
||||
val uri = Uri.fromParts("package", fragment.activity?.packageName, null)
|
||||
intent.data = uri
|
||||
}
|
||||
fragment.startActivityForResult(intent, requestCode)
|
||||
}
|
||||
|
||||
// TODO This comes from NotificationUtils
|
||||
fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
|
||||
|
||||
/**
|
||||
* Shows notification system settings for the given channel id.
|
||||
*/
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
fun startNotificationChannelSettingsIntent(fragment: Fragment, channelID: String) {
|
||||
if (!supportNotificationChannels()) return
|
||||
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply {
|
||||
putExtra(Settings.EXTRA_APP_PACKAGE, fragment.context?.packageName)
|
||||
putExtra(Settings.EXTRA_CHANNEL_ID, channelID)
|
||||
}
|
||||
fragment.startActivity(intent)
|
||||
}
|
||||
|
||||
fun startAddGoogleAccountIntent(fragment: Fragment, requestCode: Int) {
|
||||
try {
|
||||
val intent = Intent(Settings.ACTION_ADD_ACCOUNT)
|
||||
intent.putExtra(Settings.EXTRA_ACCOUNT_TYPES, arrayOf("com.google"))
|
||||
fragment.startActivityForResult(intent, requestCode)
|
||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||
fragment.activity?.toast(R.string.error_no_external_application_found)
|
||||
}
|
||||
}
|
||||
|
||||
fun startSharePlainTextIntent(fragment: Fragment, chooserTitle: String?, text: String, subject: String? = null) {
|
||||
val share = Intent(Intent.ACTION_SEND)
|
||||
share.type = "text/plain"
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
share.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT)
|
||||
} else {
|
||||
share.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
// Add data to the intent, the receiving app will decide what to do with it.
|
||||
share.putExtra(Intent.EXTRA_SUBJECT, subject)
|
||||
share.putExtra(Intent.EXTRA_TEXT, text)
|
||||
try {
|
||||
fragment.startActivity(Intent.createChooser(share, chooserTitle))
|
||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||
fragment.activity?.toast(R.string.error_no_external_application_found)
|
||||
}
|
||||
}
|
||||
|
||||
fun startImportTextFromFileIntent(fragment: Fragment, requestCode: Int) {
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "text/plain"
|
||||
}
|
||||
if (intent.resolveActivity(fragment.activity!!.packageManager) != null) {
|
||||
fragment.startActivityForResult(intent, requestCode)
|
||||
} else {
|
||||
fragment.activity?.toast(R.string.error_no_external_application_found)
|
||||
}
|
||||
}
|
||||
|
||||
// Not in KTX anymore
|
||||
fun Context.toast(resId: Int) {
|
||||
Toast.makeText(this, resId, Toast.LENGTH_SHORT).show()
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features
|
||||
|
||||
import android.os.Bundle
|
||||
import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.riotredesign.core.platform.RiotActivity
|
||||
import im.vector.riotredesign.features.home.HomeActivity
|
||||
import im.vector.riotredesign.features.login.LoginActivity
|
||||
|
||||
|
||||
class MainActivity : RiotActivity() {
|
||||
|
||||
private val authenticator = Matrix.getInstance().authenticator()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val intent = if (authenticator.hasActiveSessions()) {
|
||||
HomeActivity.newIntent(this)
|
||||
} else {
|
||||
LoginActivity.newIntent(this)
|
||||
}
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.widget.ImageView
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.amulyakhare.textdrawable.TextDrawable
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.request.target.DrawableImageViewTarget
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.matrix.android.api.MatrixPatterns
|
||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.glide.GlideApp
|
||||
import im.vector.riotredesign.core.glide.GlideRequest
|
||||
import im.vector.riotredesign.core.glide.GlideRequests
|
||||
|
||||
/**
|
||||
* This helper centralise ways to retrieve avatar into ImageView or even generic Target<Drawable>
|
||||
*/
|
||||
|
||||
object AvatarRenderer {
|
||||
|
||||
private const val THUMBNAIL_SIZE = 250
|
||||
|
||||
@UiThread
|
||||
fun render(roomMember: RoomMember, imageView: ImageView) {
|
||||
render(roomMember.avatarUrl, roomMember.displayName, imageView)
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun render(roomSummary: RoomSummary, imageView: ImageView) {
|
||||
render(roomSummary.avatarUrl, roomSummary.displayName, imageView)
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun render(avatarUrl: String?, name: String?, imageView: ImageView) {
|
||||
render(imageView.context, GlideApp.with(imageView), avatarUrl, name, DrawableImageViewTarget(imageView))
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun render(context: Context,
|
||||
glideRequest: GlideRequests,
|
||||
avatarUrl: String?,
|
||||
name: String?,
|
||||
target: Target<Drawable>) {
|
||||
if (name.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
val placeholder = getPlaceholderDrawable(context, name)
|
||||
buildGlideRequest(glideRequest, avatarUrl)
|
||||
.placeholder(placeholder)
|
||||
.into(target)
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
fun getPlaceholderDrawable(context: Context, text: String): Drawable {
|
||||
val avatarColor = ContextCompat.getColor(context, R.color.pale_teal)
|
||||
return if (text.isEmpty()) {
|
||||
TextDrawable.builder().buildRound("", avatarColor)
|
||||
} else {
|
||||
val isUserId = MatrixPatterns.isUserId(text)
|
||||
val firstLetterIndex = if (isUserId) 1 else 0
|
||||
val firstLetter = text[firstLetterIndex].toString().toUpperCase()
|
||||
TextDrawable.builder().buildRound(firstLetter, avatarColor)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// PRIVATE API *********************************************************************************
|
||||
|
||||
private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest<Drawable> {
|
||||
val resolvedUrl = Matrix.getInstance().currentSession!!.contentUrlResolver()
|
||||
.resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||
|
||||
return glideRequest
|
||||
.load(resolvedUrl)
|
||||
.apply(RequestOptions.circleCropTransform())
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import com.airbnb.mvrx.viewModel
|
||||
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.RiotActivity
|
||||
import im.vector.riotredesign.core.platform.ToolbarConfigurable
|
||||
import im.vector.riotredesign.features.home.room.detail.LoadingRoomDetailFragment
|
||||
import im.vector.riotredesign.features.rageshake.BugReporter
|
||||
import im.vector.riotredesign.features.rageshake.VectorUncaughtExceptionHandler
|
||||
import kotlinx.android.synthetic.main.activity_home.*
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.android.scope.ext.android.bindScope
|
||||
import org.koin.android.scope.ext.android.getOrCreateScope
|
||||
|
||||
|
||||
class HomeActivity : RiotActivity(), ToolbarConfigurable {
|
||||
|
||||
private val homeActivityViewModel: HomeActivityViewModel by viewModel()
|
||||
private val homeNavigator by inject<HomeNavigator>()
|
||||
|
||||
private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
|
||||
override fun onDrawerStateChanged(newState: Int) {
|
||||
hideKeyboard()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_home)
|
||||
bindScope(getOrCreateScope(HomeModule.HOME_SCOPE))
|
||||
homeNavigator.activity = this
|
||||
drawerLayout.addDrawerListener(drawerListener)
|
||||
if (savedInstanceState == null) {
|
||||
val homeDrawerFragment = HomeDrawerFragment.newInstance()
|
||||
val loadingDetail = LoadingRoomDetailFragment.newInstance()
|
||||
replaceFragment(loadingDetail, R.id.homeDetailFragmentContainer)
|
||||
replaceFragment(homeDrawerFragment, R.id.homeDrawerFragmentContainer)
|
||||
}
|
||||
homeActivityViewModel.openRoomLiveData.observeEvent(this) {
|
||||
homeNavigator.openRoomDetail(it, null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
drawerLayout.removeDrawerListener(drawerListener)
|
||||
homeNavigator.activity = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (VectorUncaughtExceptionHandler.didAppCrash(this)) {
|
||||
VectorUncaughtExceptionHandler.clearAppCrashStatus(this)
|
||||
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.send_bug_report_app_crashed)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.yes) { _, _ -> BugReporter.openBugReportScreen(this) }
|
||||
.setNegativeButton(R.string.no) { _, _ -> BugReporter.deleteCrashFile(this) }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun configure(toolbar: Toolbar) {
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setHomeButtonEnabled(true)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
val drawerToggle = ActionBarDrawerToggle(this, drawerLayout, toolbar, 0, 0)
|
||||
drawerLayout.addDrawerListener(drawerToggle)
|
||||
drawerToggle.syncState()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
drawerLayout.openDrawer(GravityCompat.START)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
||||
drawerLayout.closeDrawer(GravityCompat.START)
|
||||
} else {
|
||||
val handled = recursivelyDispatchOnBackPressed(supportFragmentManager)
|
||||
if (!handled) {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun recursivelyDispatchOnBackPressed(fm: FragmentManager): Boolean {
|
||||
if (fm.backStackEntryCount == 0)
|
||||
return false
|
||||
val reverseOrder = fm.fragments.filter { it is OnBackPressed }.reversed()
|
||||
for (f in reverseOrder) {
|
||||
val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager)
|
||||
if (handledByChildFragments) {
|
||||
return true
|
||||
}
|
||||
val backPressable = f as OnBackPressed
|
||||
if (backPressable.onBackPressed()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
fun newIntent(context: Context): Intent {
|
||||
return Intent(context, HomeActivity::class.java)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.rx.rx
|
||||
import im.vector.riotredesign.core.platform.RiotViewModel
|
||||
import im.vector.riotredesign.core.utils.LiveEvent
|
||||
import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository
|
||||
import io.reactivex.rxkotlin.subscribeBy
|
||||
import org.koin.android.ext.android.get
|
||||
|
||||
class EmptyState : MvRxState
|
||||
|
||||
class HomeActivityViewModel(state: EmptyState,
|
||||
private val session: Session,
|
||||
roomSelectionRepository: RoomSelectionRepository
|
||||
) : RiotViewModel<EmptyState>(state) {
|
||||
|
||||
companion object : MvRxViewModelFactory<HomeActivityViewModel, EmptyState> {
|
||||
|
||||
@JvmStatic
|
||||
override fun create(viewModelContext: ViewModelContext, state: EmptyState): HomeActivityViewModel? {
|
||||
val session = Matrix.getInstance().currentSession!!
|
||||
val roomSelectionRepository = viewModelContext.activity.get<RoomSelectionRepository>()
|
||||
return HomeActivityViewModel(state, session, roomSelectionRepository)
|
||||
}
|
||||
}
|
||||
|
||||
private val _openRoomLiveData = MutableLiveData<LiveEvent<String>>()
|
||||
val openRoomLiveData: LiveData<LiveEvent<String>>
|
||||
get() = _openRoomLiveData
|
||||
|
||||
init {
|
||||
val lastSelectedRoomId = roomSelectionRepository.lastSelectedRoom()
|
||||
if (lastSelectedRoomId == null || session.getRoom(lastSelectedRoomId) == null) {
|
||||
getTheFirstRoomWhenAvailable()
|
||||
} else {
|
||||
_openRoomLiveData.postValue(LiveEvent(lastSelectedRoomId))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTheFirstRoomWhenAvailable() {
|
||||
session.rx().liveRoomSummaries()
|
||||
.filter { it.isNotEmpty() }
|
||||
.first(emptyList())
|
||||
.subscribeBy {
|
||||
val firstRoom = it.firstOrNull()
|
||||
if (firstRoom != null) {
|
||||
_openRoomLiveData.postValue(LiveEvent(firstRoom.roomId))
|
||||
}
|
||||
}
|
||||
.disposeOnClear()
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.extensions.replaceChildFragment
|
||||
import im.vector.riotredesign.core.platform.RiotFragment
|
||||
import im.vector.riotredesign.features.home.group.GroupListFragment
|
||||
import im.vector.riotredesign.features.home.room.list.RoomListFragment
|
||||
|
||||
class HomeDrawerFragment : RiotFragment() {
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance(): HomeDrawerFragment {
|
||||
return HomeDrawerFragment()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_home_drawer, container, false)
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
if (savedInstanceState == null) {
|
||||
val groupListFragment = GroupListFragment.newInstance()
|
||||
replaceChildFragment(groupListFragment, R.id.groupListFragmentContainer)
|
||||
val roomListFragment = RoomListFragment.newInstance()
|
||||
replaceChildFragment(roomListFragment, R.id.roomListFragmentContainer)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.fragment.app.Fragment
|
||||
import im.vector.riotredesign.core.glide.GlideApp
|
||||
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.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.dsl.module.module
|
||||
|
||||
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 {
|
||||
|
||||
// Activity scope
|
||||
|
||||
scope(HOME_SCOPE) {
|
||||
HomeNavigator()
|
||||
}
|
||||
|
||||
scope(HOME_SCOPE) {
|
||||
HomePermalinkHandler(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)
|
||||
|
||||
val timelineItemFactory = TimelineItemFactory(messageItemFactory = messageItemFactory,
|
||||
roomNameItemFactory = RoomNameItemFactory(get()),
|
||||
roomTopicItemFactory = RoomTopicItemFactory(get()),
|
||||
roomMemberItemFactory = RoomMemberItemFactory(get()),
|
||||
roomHistoryVisibilityItemFactory = RoomHistoryVisibilityItemFactory(get()),
|
||||
callItemFactory = CallItemFactory(get()),
|
||||
defaultItemFactory = DefaultItemFactory()
|
||||
)
|
||||
TimelineEventController(timelineDateFormatter, timelineItemFactory, timelineMediaSizeProvider)
|
||||
}
|
||||
|
||||
factory {
|
||||
RoomSummaryController(get())
|
||||
}
|
||||
|
||||
factory {
|
||||
GroupSummaryController()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home
|
||||
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.extensions.addFragmentToBackstack
|
||||
import im.vector.riotredesign.core.extensions.replaceFragment
|
||||
import im.vector.riotredesign.features.home.room.detail.RoomDetailArgs
|
||||
import im.vector.riotredesign.features.home.room.detail.RoomDetailFragment
|
||||
import kotlinx.android.synthetic.main.activity_home.*
|
||||
import timber.log.Timber
|
||||
|
||||
class HomeNavigator {
|
||||
|
||||
var activity: HomeActivity? = null
|
||||
|
||||
private var rootRoomId: String? = null
|
||||
|
||||
fun openRoomDetail(roomId: String,
|
||||
eventId: String?,
|
||||
addToBackstack: Boolean = false) {
|
||||
Timber.v("Open room detail $roomId - $eventId - $addToBackstack")
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun openGroupDetail(groupId: String) {
|
||||
Timber.v("Open group detail $groupId")
|
||||
}
|
||||
|
||||
fun openUserDetail(userId: String) {
|
||||
Timber.v("Open user detail $userId")
|
||||
}
|
||||
|
||||
// Private Methods *****************************************************************************
|
||||
|
||||
private fun clearBackStack(fragmentManager: FragmentManager) {
|
||||
if (fragmentManager.backStackEntryCount > 0) {
|
||||
val first = fragmentManager.getBackStackEntryAt(0)
|
||||
fragmentManager.popBackStack(first.id, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isRoot(roomId: String): Boolean {
|
||||
return rootRoomId == roomId
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home
|
||||
|
||||
import android.net.Uri
|
||||
import im.vector.matrix.android.api.permalinks.PermalinkData
|
||||
import im.vector.matrix.android.api.permalinks.PermalinkParser
|
||||
|
||||
class HomePermalinkHandler(private val navigator: HomeNavigator) {
|
||||
|
||||
fun launch(deepLink: String?) {
|
||||
val uri = deepLink?.let { Uri.parse(it) }
|
||||
launch(uri)
|
||||
}
|
||||
|
||||
fun launch(deepLink: Uri?) {
|
||||
if (deepLink == null) {
|
||||
return
|
||||
}
|
||||
val permalinkData = PermalinkParser.parse(deepLink)
|
||||
when (permalinkData) {
|
||||
is PermalinkData.EventLink -> {
|
||||
navigator.openRoomDetail(permalinkData.roomIdOrAlias, permalinkData.eventId, true)
|
||||
}
|
||||
is PermalinkData.RoomLink -> {
|
||||
navigator.openRoomDetail(permalinkData.roomIdOrAlias, null, true)
|
||||
}
|
||||
is PermalinkData.GroupLink -> {
|
||||
navigator.openGroupDetail(permalinkData.groupId)
|
||||
}
|
||||
is PermalinkData.UserLink -> {
|
||||
navigator.openUserDetail(permalinkData.userId)
|
||||
}
|
||||
is PermalinkData.FallbackLink -> {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.group
|
||||
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
|
||||
sealed class GroupListActions {
|
||||
|
||||
data class SelectGroup(val groupSummary: GroupSummary) : GroupListActions()
|
||||
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.group
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.airbnb.mvrx.Incomplete
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.platform.RiotFragment
|
||||
import im.vector.riotredesign.core.platform.StateView
|
||||
import im.vector.riotredesign.features.home.HomeModule
|
||||
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 : RiotFragment(), GroupSummaryController.Callback {
|
||||
|
||||
companion object {
|
||||
fun newInstance(): GroupListFragment {
|
||||
return GroupListFragment()
|
||||
}
|
||||
}
|
||||
|
||||
private val viewModel: GroupListViewModel by fragmentViewModel()
|
||||
private val groupController by inject<GroupSummaryController>()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_group_list, container, false)
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
bindScope(getOrCreateScope(HomeModule.GROUP_LIST_SCOPE))
|
||||
groupController.callback = this
|
||||
stateView.contentView = epoxyRecyclerView
|
||||
epoxyRecyclerView.setController(groupController)
|
||||
viewModel.subscribe { renderState(it) }
|
||||
}
|
||||
|
||||
private fun renderState(state: GroupListViewState) {
|
||||
when (state.asyncGroups) {
|
||||
is Incomplete -> renderLoading()
|
||||
is Success -> renderSuccess(state)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderSuccess(state: GroupListViewState) {
|
||||
stateView.state = StateView.State.Content
|
||||
groupController.setData(state)
|
||||
}
|
||||
|
||||
private fun renderLoading() {
|
||||
stateView.state = StateView.State.Loading
|
||||
}
|
||||
|
||||
override fun onGroupSelected(groupSummary: GroupSummary) {
|
||||
viewModel.accept(GroupListActions.SelectGroup(groupSummary))
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.group
|
||||
|
||||
import 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.rx.rx
|
||||
import im.vector.riotredesign.core.platform.RiotViewModel
|
||||
import org.koin.android.ext.android.get
|
||||
|
||||
class GroupListViewModel(initialState: GroupListViewState,
|
||||
private val selectedGroupHolder: SelectedGroupStore,
|
||||
private val session: Session
|
||||
) : RiotViewModel<GroupListViewState>(initialState) {
|
||||
|
||||
companion object : MvRxViewModelFactory<GroupListViewModel, GroupListViewState> {
|
||||
|
||||
@JvmStatic
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
observeGroupSummaries()
|
||||
observeState()
|
||||
}
|
||||
|
||||
private fun observeState() {
|
||||
subscribe {
|
||||
val selectedGroup = Option.fromNullable(it.selectedGroup)
|
||||
selectedGroupHolder.post(selectedGroup)
|
||||
}
|
||||
}
|
||||
|
||||
fun accept(action: GroupListActions) {
|
||||
when (action) {
|
||||
is GroupListActions.SelectGroup -> handleSelectGroup(action)
|
||||
}
|
||||
}
|
||||
|
||||
// PRIVATE METHODS *****************************************************************************
|
||||
|
||||
private fun handleSelectGroup(action: GroupListActions.SelectGroup) = withState { state ->
|
||||
if (state.selectedGroup?.groupId != action.groupSummary.groupId) {
|
||||
setState { copy(selectedGroup = action.groupSummary) }
|
||||
} else {
|
||||
setState { copy(selectedGroup = null) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun observeGroupSummaries() {
|
||||
session
|
||||
.rx().liveGroupSummaries()
|
||||
.execute { async ->
|
||||
copy(asyncGroups = async)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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.group
|
||||
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
|
||||
data class GroupListViewState(
|
||||
val asyncGroups: Async<List<GroupSummary>> = Uninitialized,
|
||||
val selectedGroup: GroupSummary? = null
|
||||
) : MvRxState
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.group
|
||||
|
||||
import com.airbnb.epoxy.TypedEpoxyController
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
|
||||
class GroupSummaryController : TypedEpoxyController<GroupListViewState>() {
|
||||
|
||||
var callback: Callback? = null
|
||||
|
||||
override fun buildModels(viewState: GroupListViewState) {
|
||||
buildGroupModels(viewState.asyncGroups(), viewState.selectedGroup)
|
||||
}
|
||||
|
||||
private fun buildGroupModels(summaries: List<GroupSummary>?, selected: GroupSummary?) {
|
||||
if (summaries.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
summaries.forEach { groupSummary ->
|
||||
val isSelected = groupSummary.groupId == selected?.groupId
|
||||
groupSummaryItem {
|
||||
id(groupSummary.groupId)
|
||||
groupName(groupSummary.displayName)
|
||||
selected(isSelected)
|
||||
avatarUrl(groupSummary.avatarUrl)
|
||||
listener { callback?.onGroupSelected(groupSummary) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onGroupSelected(groupSummary: GroupSummary)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.group
|
||||
|
||||
import android.widget.ImageView
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.epoxy.RiotEpoxyHolder
|
||||
import im.vector.riotredesign.core.epoxy.RiotEpoxyModel
|
||||
import im.vector.riotredesign.core.platform.CheckableFrameLayout
|
||||
import im.vector.riotredesign.features.home.AvatarRenderer
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_group)
|
||||
abstract class GroupSummaryItem : RiotEpoxyModel<GroupSummaryItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute lateinit var groupName: CharSequence
|
||||
@EpoxyAttribute var avatarUrl: String? = null
|
||||
@EpoxyAttribute var selected: Boolean = false
|
||||
@EpoxyAttribute var listener: (() -> Unit)? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
holder.rootView.isSelected = selected
|
||||
holder.rootView.setOnClickListener { listener?.invoke() }
|
||||
AvatarRenderer.render(avatarUrl, groupName.toString(), holder.avatarImageView)
|
||||
}
|
||||
|
||||
class Holder : RiotEpoxyHolder() {
|
||||
val avatarImageView by bind<ImageView>(R.id.groupAvatarImageView)
|
||||
val rootView by bind<CheckableFrameLayout>(R.id.itemGroupLayout)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.group
|
||||
|
||||
import arrow.core.Option
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
import im.vector.riotredesign.core.utils.RxStore
|
||||
|
||||
class SelectedGroupStore : RxStore<Option<GroupSummary>>(Option.empty())
|
@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import im.vector.riotredesign.core.utils.RxStore
|
||||
|
||||
class VisibleRoomStore : RxStore<String>()
|
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.bumptech.glide.Glide
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.platform.RiotFragment
|
||||
import kotlinx.android.synthetic.main.fragment_loading_room_detail.*
|
||||
|
||||
class LoadingRoomDetailFragment : RiotFragment() {
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance(): LoadingRoomDetailFragment {
|
||||
return LoadingRoomDetailFragment()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_loading_room_detail, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
Glide.with(this)
|
||||
.load(R.drawable.riot_splash)
|
||||
.into(animatedLogoImageView)
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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 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()
|
||||
object IsDisplayed : RoomDetailActions()
|
||||
data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions()
|
||||
data class LoadMore(val direction: Timeline.Direction) : RoomDetailActions()
|
||||
|
||||
}
|
@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.airbnb.epoxy.EpoxyVisibilityTracker
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
|
||||
import im.vector.riotredesign.core.platform.RiotFragment
|
||||
import im.vector.riotredesign.core.platform.ToolbarConfigurable
|
||||
import im.vector.riotredesign.features.home.AvatarRenderer
|
||||
import im.vector.riotredesign.features.home.HomeModule
|
||||
import im.vector.riotredesign.features.home.HomePermalinkHandler
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener
|
||||
import im.vector.riotredesign.features.media.MediaContentRenderer
|
||||
import im.vector.riotredesign.features.media.MediaViewerActivity
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.fragment_room_detail.*
|
||||
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
|
||||
|
||||
|
||||
@Parcelize
|
||||
data class RoomDetailArgs(
|
||||
val roomId: String,
|
||||
val eventId: String? = null
|
||||
) : Parcelable
|
||||
|
||||
|
||||
class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance(args: RoomDetailArgs): RoomDetailFragment {
|
||||
return RoomDetailFragment().apply {
|
||||
setArguments(args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
|
||||
private val timelineEventController: TimelineEventController by inject { parametersOf(this) }
|
||||
private val homePermalinkHandler: HomePermalinkHandler by inject()
|
||||
|
||||
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_room_detail, container, false)
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
bindScope(getOrCreateScope(HomeModule.ROOM_DETAIL_SCOPE))
|
||||
setupRecyclerView()
|
||||
setupToolbar()
|
||||
setupSendButton()
|
||||
roomDetailViewModel.subscribe { renderState(it) }
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
roomDetailViewModel.process(RoomDetailActions.IsDisplayed)
|
||||
}
|
||||
|
||||
// PRIVATE METHODS *****************************************************************************
|
||||
|
||||
private fun setupToolbar() {
|
||||
val parentActivity = riotActivity
|
||||
if (parentActivity is ToolbarConfigurable) {
|
||||
parentActivity.configure(toolbar)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
val epoxyVisibilityTracker = EpoxyVisibilityTracker()
|
||||
epoxyVisibilityTracker.attach(recyclerView)
|
||||
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
|
||||
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
|
||||
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager)
|
||||
recyclerView.layoutManager = layoutManager
|
||||
recyclerView.itemAnimator = null
|
||||
recyclerView.setHasFixedSize(true)
|
||||
timelineEventController.addModelBuildListener {
|
||||
it.dispatchTo(stateRestorer)
|
||||
it.dispatchTo(scrollOnNewMessageCallback)
|
||||
}
|
||||
|
||||
recyclerView.addOnScrollListener(
|
||||
EndlessRecyclerViewScrollListener(layoutManager, RoomDetailViewModel.PAGINATION_COUNT) { direction ->
|
||||
roomDetailViewModel.process(RoomDetailActions.LoadMore(direction))
|
||||
})
|
||||
recyclerView.setController(timelineEventController)
|
||||
timelineEventController.callback = this
|
||||
}
|
||||
|
||||
private fun setupSendButton() {
|
||||
sendButton.setOnClickListener {
|
||||
val textMessage = composerEditText.text.toString()
|
||||
if (textMessage.isNotBlank()) {
|
||||
roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage))
|
||||
composerEditText.text = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderState(state: RoomDetailViewState) {
|
||||
renderRoomSummary(state)
|
||||
timelineEventController.setTimeline(state.timeline)
|
||||
}
|
||||
|
||||
private fun renderRoomSummary(state: RoomDetailViewState) {
|
||||
state.asyncRoomSummary()?.let {
|
||||
toolbarTitleView.text = it.displayName
|
||||
AvatarRenderer.render(it, toolbarAvatarImageView)
|
||||
if (it.topic.isNotEmpty()) {
|
||||
toolbarSubtitleView.visibility = View.VISIBLE
|
||||
toolbarSubtitleView.text = it.topic
|
||||
} else {
|
||||
toolbarSubtitleView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TimelineEventController.Callback ************************************************************
|
||||
|
||||
override fun onUrlClicked(url: String) {
|
||||
homePermalinkHandler.launch(url)
|
||||
}
|
||||
|
||||
override fun onEventVisible(event: TimelineEvent) {
|
||||
roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event))
|
||||
}
|
||||
|
||||
override fun onMediaClicked(mediaData: MediaContentRenderer.Data, view: View) {
|
||||
val intent = MediaViewerActivity.newIntent(riotActivity, mediaData)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail
|
||||
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.jakewharton.rxrelay2.BehaviorRelay
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.rx.rx
|
||||
import im.vector.riotredesign.core.platform.RiotViewModel
|
||||
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.koin.android.ext.android.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class RoomDetailViewModel(initialState: RoomDetailViewState,
|
||||
private val session: Session,
|
||||
private val visibleRoomHolder: VisibleRoomStore
|
||||
) : RiotViewModel<RoomDetailViewState>(initialState) {
|
||||
|
||||
private val room = session.getRoom(initialState.roomId)!!
|
||||
private val roomId = initialState.roomId
|
||||
private val eventId = initialState.eventId
|
||||
private val displayedEventsObservable = BehaviorRelay.create<RoomDetailActions.EventDisplayed>()
|
||||
private val timeline = room.createTimeline(eventId, TimelineDisplayableEvents.DISPLAYABLE_TYPES)
|
||||
|
||||
companion object : MvRxViewModelFactory<RoomDetailViewModel, RoomDetailViewState> {
|
||||
|
||||
const val PAGINATION_COUNT = 50
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
observeRoomSummary()
|
||||
observeEventDisplayedActions()
|
||||
room.loadRoomMembersIfNeeded()
|
||||
timeline.start()
|
||||
setState { copy(timeline = this@RoomDetailViewModel.timeline) }
|
||||
}
|
||||
|
||||
fun process(action: RoomDetailActions) {
|
||||
when (action) {
|
||||
is RoomDetailActions.SendMessage -> handleSendMessage(action)
|
||||
is RoomDetailActions.IsDisplayed -> handleIsDisplayed()
|
||||
is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action)
|
||||
is RoomDetailActions.LoadMore -> handleLoadMore(action)
|
||||
}
|
||||
}
|
||||
|
||||
// PRIVATE METHODS *****************************************************************************
|
||||
|
||||
private fun handleSendMessage(action: RoomDetailActions.SendMessage) {
|
||||
room.sendTextMessage(action.text, callback = object : MatrixCallback<Event> {})
|
||||
}
|
||||
|
||||
private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) {
|
||||
displayedEventsObservable.accept(action)
|
||||
}
|
||||
|
||||
private fun handleIsDisplayed() {
|
||||
visibleRoomHolder.post(roomId)
|
||||
}
|
||||
|
||||
private fun handleLoadMore(action: RoomDetailActions.LoadMore) {
|
||||
timeline.paginate(action.direction, PAGINATION_COUNT)
|
||||
}
|
||||
|
||||
private fun observeEventDisplayedActions() {
|
||||
// We are buffering scroll events for one second
|
||||
// and keep the most recent one to set the read receipt on.
|
||||
displayedEventsObservable
|
||||
.buffer(1, TimeUnit.SECONDS)
|
||||
.filter { it.isNotEmpty() }
|
||||
.subscribeBy(onNext = { actions ->
|
||||
val mostRecentEvent = actions.maxBy { it.event.displayIndex }
|
||||
mostRecentEvent?.event?.root?.eventId?.let { eventId ->
|
||||
room.setReadReceipt(eventId, callback = object : MatrixCallback<Void> {})
|
||||
}
|
||||
})
|
||||
.disposeOnClear()
|
||||
}
|
||||
|
||||
private fun observeRoomSummary() {
|
||||
room.rx().liveRoomSummary()
|
||||
.execute { async ->
|
||||
copy(asyncRoomSummary = async)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
timeline.dispose()
|
||||
super.onCleared()
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail
|
||||
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
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
|
||||
|
||||
data class RoomDetailViewState(
|
||||
val roomId: String,
|
||||
val eventId: String?,
|
||||
val timeline: Timeline? = null,
|
||||
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
||||
val asyncTimelineData: Async<TimelineData> = Uninitialized
|
||||
) : MvRxState {
|
||||
|
||||
constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)
|
||||
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 androidx.recyclerview.widget.LinearLayoutManager
|
||||
import im.vector.riotredesign.core.platform.DefaultListUpdateCallback
|
||||
|
||||
class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager) : DefaultListUpdateCallback {
|
||||
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
if (position == 0 && layoutManager.findFirstVisibleItemPosition() == 0) {
|
||||
layoutManager.scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,201 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListUpdateCallback
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.airbnb.epoxy.EpoxyController
|
||||
import com.airbnb.epoxy.EpoxyModel
|
||||
import com.airbnb.epoxy.VisibilityState
|
||||
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.RiotEpoxyModel
|
||||
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.*
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.DaySeparatorItem_
|
||||
import im.vector.riotredesign.features.media.MediaContentRenderer
|
||||
|
||||
class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
||||
private val timelineItemFactory: TimelineItemFactory,
|
||||
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
|
||||
private val backgroundHandler: Handler = TimelineAsyncHelper.getBackgroundHandler()
|
||||
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener {
|
||||
|
||||
interface Callback {
|
||||
fun onEventVisible(event: TimelineEvent)
|
||||
fun onUrlClicked(url: String)
|
||||
fun onMediaClicked(mediaData: MediaContentRenderer.Data, view: View)
|
||||
}
|
||||
|
||||
private val modelCache = arrayListOf<List<EpoxyModel<*>>>()
|
||||
private var currentSnapshot: List<TimelineEvent> = emptyList()
|
||||
private var inSubmitList: Boolean = false
|
||||
private var timeline: Timeline? = null
|
||||
|
||||
var callback: Callback? = null
|
||||
|
||||
private val listUpdateCallback = object : ListUpdateCallback {
|
||||
|
||||
@Synchronized
|
||||
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||
assertUpdateCallbacksAllowed()
|
||||
(position until (position + count)).forEach {
|
||||
modelCache[it] = emptyList()
|
||||
}
|
||||
requestModelBuild()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||
assertUpdateCallbacksAllowed()
|
||||
val model = modelCache.removeAt(fromPosition)
|
||||
modelCache.add(toPosition, model)
|
||||
requestModelBuild()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
assertUpdateCallbacksAllowed()
|
||||
if (modelCache.isNotEmpty() && position == modelCache.size) {
|
||||
modelCache[position - 1] = emptyList()
|
||||
}
|
||||
(0 until count).forEach {
|
||||
modelCache.add(position, emptyList())
|
||||
}
|
||||
requestModelBuild()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onRemoved(position: Int, count: Int) {
|
||||
assertUpdateCallbacksAllowed()
|
||||
(0 until count).forEach {
|
||||
modelCache.removeAt(position)
|
||||
}
|
||||
requestModelBuild()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
requestModelBuild()
|
||||
}
|
||||
|
||||
fun setTimeline(timeline: Timeline?) {
|
||||
if (this.timeline != timeline) {
|
||||
this.timeline = timeline
|
||||
this.timeline?.listener = this
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
super.onAttachedToRecyclerView(recyclerView)
|
||||
timelineMediaSizeProvider.recyclerView = recyclerView
|
||||
}
|
||||
|
||||
override fun buildModels() {
|
||||
LoadingItemModel_()
|
||||
.id("forward_loading_item")
|
||||
.addWhen(Timeline.Direction.FORWARDS)
|
||||
|
||||
|
||||
val timelineModels = getModels()
|
||||
add(timelineModels)
|
||||
|
||||
LoadingItemModel_()
|
||||
.id("backward_loading_item")
|
||||
.addWhen(Timeline.Direction.BACKWARDS)
|
||||
}
|
||||
|
||||
// Timeline.LISTENER ***************************************************************************
|
||||
|
||||
override fun onUpdated(snapshot: List<TimelineEvent>) {
|
||||
submitSnapshot(snapshot)
|
||||
}
|
||||
|
||||
private fun submitSnapshot(newSnapshot: List<TimelineEvent>) {
|
||||
backgroundHandler.post {
|
||||
inSubmitList = true
|
||||
val diffCallback = TimelineEventDiffUtilCallback(currentSnapshot, newSnapshot)
|
||||
currentSnapshot = newSnapshot
|
||||
val diffResult = DiffUtil.calculateDiff(diffCallback)
|
||||
diffResult.dispatchUpdatesTo(listUpdateCallback)
|
||||
inSubmitList = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertUpdateCallbacksAllowed() {
|
||||
require(inSubmitList || Looper.myLooper() == backgroundHandler.looper)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun getModels(): List<EpoxyModel<*>> {
|
||||
(0 until modelCache.size).forEach { position ->
|
||||
if (modelCache[position].isEmpty()) {
|
||||
modelCache[position] = buildItemModels(position, currentSnapshot)
|
||||
}
|
||||
}
|
||||
return modelCache.flatten()
|
||||
}
|
||||
|
||||
private fun buildItemModels(currentPosition: Int, items: List<TimelineEvent>): List<EpoxyModel<*>> {
|
||||
val epoxyModels = ArrayList<EpoxyModel<*>>()
|
||||
val event = items[currentPosition]
|
||||
val nextEvent = items.nextDisplayableEvent(currentPosition)
|
||||
|
||||
val date = event.root.localDateTime()
|
||||
val nextDate = nextEvent?.root?.localDateTime()
|
||||
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
|
||||
|
||||
timelineItemFactory.create(event, nextEvent, callback).also {
|
||||
it.id(event.localId)
|
||||
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
|
||||
epoxyModels.add(it)
|
||||
}
|
||||
if (addDaySeparator) {
|
||||
val formattedDay = dateFormatter.formatMessageDay(date)
|
||||
val daySeparatorItem = DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay)
|
||||
epoxyModels.add(daySeparatorItem)
|
||||
}
|
||||
return epoxyModels
|
||||
}
|
||||
|
||||
private fun LoadingItemModel_.addWhen(direction: Timeline.Direction) {
|
||||
val shouldAdd = timeline?.let {
|
||||
it.hasMoreToLoad(direction)
|
||||
} ?: false
|
||||
addIf(shouldAdd, this@TimelineEventController)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class TimelineEventVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?,
|
||||
private val event: TimelineEvent)
|
||||
: RiotEpoxyModel.OnVisibilityStateChangedListener {
|
||||
|
||||
override fun onVisibilityStateChanged(visibilityState: Int) {
|
||||
if (visibilityState == VisibilityState.VISIBLE) {
|
||||
callback?.onEventVisible(event)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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.detail.timeline.animation
|
||||
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
|
||||
private const val ANIM_DURATION_IN_MILLIS = 100L
|
||||
|
||||
class TimelineItemAnimator : DefaultItemAnimator() {
|
||||
|
||||
init {
|
||||
addDuration = ANIM_DURATION_IN_MILLIS
|
||||
removeDuration = 0
|
||||
moveDuration = 0
|
||||
changeDuration = 0
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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 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
|
||||
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 CallItemFactory(private val stringProvider: StringProvider) {
|
||||
|
||||
fun create(event: TimelineEvent): NoticeItem? {
|
||||
val roomMember = event.roomMember ?: return null
|
||||
val text = buildNoticeText(event.root, roomMember) ?: return null
|
||||
return NoticeItem_()
|
||||
.noticeText(text)
|
||||
.avatarUrl(roomMember.avatarUrl)
|
||||
.memberName(roomMember.displayName)
|
||||
}
|
||||
|
||||
private fun buildNoticeText(event: Event, roomMember: RoomMember): CharSequence? {
|
||||
return when {
|
||||
EventType.CALL_INVITE == event.type -> {
|
||||
val content = event.content.toModel<CallInviteContent>() ?: return null
|
||||
val isVideoCall = content.offer.sdp == CallInviteContent.Offer.SDP_VIDEO
|
||||
return if (isVideoCall) {
|
||||
stringProvider.getString(R.string.notice_placed_video_call, roomMember.displayName)
|
||||
} else {
|
||||
stringProvider.getString(R.string.notice_placed_voice_call, roomMember.displayName)
|
||||
}
|
||||
}
|
||||
EventType.CALL_ANSWER == event.type -> stringProvider.getString(R.string.notice_answered_call, roomMember.displayName)
|
||||
EventType.CALL_HANGUP == event.type -> stringProvider.getString(R.string.notice_ended_call, roomMember.displayName)
|
||||
else -> null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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 im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.DefaultItem
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.DefaultItem_
|
||||
|
||||
class DefaultItemFactory {
|
||||
|
||||
fun create(event: TimelineEvent, exception: Exception? = null): DefaultItem? {
|
||||
val text = if (exception == null) {
|
||||
"${event.root.type} events are not yet handled"
|
||||
} else {
|
||||
"an exception occurred when rendering the event ${event.root.eventId}"
|
||||
}
|
||||
return DefaultItem_().text(text)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,162 @@
|
||||
/*
|
||||
* 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.Spannable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.util.Linkify
|
||||
import im.vector.matrix.android.api.permalinks.MatrixLinkify
|
||||
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.message.*
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.epoxy.RiotEpoxyModel
|
||||
import im.vector.riotredesign.core.extensions.localDateTime
|
||||
import im.vector.riotredesign.core.resources.ColorProvider
|
||||
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
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.*
|
||||
import im.vector.riotredesign.features.html.EventHtmlRenderer
|
||||
import im.vector.riotredesign.features.media.MediaContentRenderer
|
||||
import me.gujun.android.span.span
|
||||
|
||||
class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
|
||||
private val timelineDateFormatter: TimelineDateFormatter,
|
||||
private val htmlRenderer: EventHtmlRenderer) {
|
||||
|
||||
fun create(event: TimelineEvent,
|
||||
nextEvent: TimelineEvent?,
|
||||
callback: TimelineEventController.Callback?
|
||||
): RiotEpoxyModel<*>? {
|
||||
|
||||
val roomMember = event.roomMember
|
||||
val nextRoomMember = nextEvent?.roomMember
|
||||
|
||||
val date = event.root.localDateTime()
|
||||
val nextDate = nextEvent?.root?.localDateTime()
|
||||
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
|
||||
val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60))
|
||||
?: false
|
||||
|
||||
val showInformation = addDaySeparator
|
||||
|| nextRoomMember != roomMember
|
||||
|| nextEvent?.root?.type != EventType.MESSAGE
|
||||
|| isNextMessageReceivedMoreThanOneHourAgo
|
||||
|
||||
val messageContent: MessageContent = event.root.content.toModel() ?: return null
|
||||
val time = timelineDateFormatter.formatMessageHour(date)
|
||||
val avatarUrl = roomMember?.avatarUrl
|
||||
val memberName = roomMember?.displayName ?: event.root.sender
|
||||
val informationData = MessageInformationData(time, avatarUrl, memberName, showInformation)
|
||||
|
||||
return when (messageContent) {
|
||||
is MessageTextContent -> buildTextMessageItem(messageContent, informationData, callback)
|
||||
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, callback)
|
||||
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, callback)
|
||||
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, callback)
|
||||
else -> buildNotHandledMessageItem(messageContent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNotHandledMessageItem(messageContent: MessageContent): DefaultItem? {
|
||||
val text = "${messageContent.type} message events are not yet handled"
|
||||
return DefaultItem_().text(text)
|
||||
}
|
||||
|
||||
private fun buildImageMessageItem(messageContent: MessageImageContent,
|
||||
informationData: MessageInformationData,
|
||||
callback: TimelineEventController.Callback?): MessageImageItem? {
|
||||
|
||||
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
|
||||
val data = MediaContentRenderer.Data(
|
||||
messageContent.body,
|
||||
url = messageContent.url,
|
||||
height = messageContent.info?.height,
|
||||
maxHeight = maxHeight,
|
||||
width = messageContent.info?.width,
|
||||
maxWidth = maxWidth,
|
||||
rotation = messageContent.info?.rotation,
|
||||
orientation = messageContent.info?.orientation
|
||||
)
|
||||
return MessageImageItem_()
|
||||
.informationData(informationData)
|
||||
.mediaData(data)
|
||||
.clickListener { view -> callback?.onMediaClicked(data, view) }
|
||||
}
|
||||
|
||||
private fun buildTextMessageItem(messageContent: MessageTextContent,
|
||||
informationData: MessageInformationData,
|
||||
callback: TimelineEventController.Callback?): MessageTextItem? {
|
||||
|
||||
val bodyToUse = messageContent.formattedBody
|
||||
?.let {
|
||||
htmlRenderer.render(it)
|
||||
}
|
||||
?: messageContent.body
|
||||
|
||||
val linkifiedBody = linkifyBody(bodyToUse, callback)
|
||||
return MessageTextItem_()
|
||||
.message(linkifiedBody)
|
||||
.informationData(informationData)
|
||||
}
|
||||
|
||||
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)
|
||||
textStyle = "italic"
|
||||
}
|
||||
linkifyBody(formattedBody, callback)
|
||||
}
|
||||
return MessageTextItem_()
|
||||
.message(message)
|
||||
.informationData(informationData)
|
||||
}
|
||||
|
||||
private fun buildEmoteMessageItem(messageContent: MessageEmoteContent,
|
||||
informationData: MessageInformationData,
|
||||
callback: TimelineEventController.Callback?): MessageTextItem? {
|
||||
|
||||
val message = messageContent.body.let {
|
||||
val formattedBody = "* ${informationData.memberName} $it"
|
||||
linkifyBody(formattedBody, callback)
|
||||
}
|
||||
return MessageTextItem_()
|
||||
.message(message)
|
||||
.informationData(informationData)
|
||||
}
|
||||
|
||||
private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): Spannable {
|
||||
val spannable = SpannableStringBuilder(body)
|
||||
MatrixLinkify.addLinks(spannable, object : MatrixPermalinkSpan.Callback {
|
||||
override fun onUrlClicked(url: String) {
|
||||
callback?.onUrlClicked(url)
|
||||
}
|
||||
})
|
||||
Linkify.addLinks(spannable, Linkify.ALL)
|
||||
return spannable
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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 im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
|
||||
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent
|
||||
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.item.NoticeItem
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_
|
||||
|
||||
|
||||
class RoomHistoryVisibilityItemFactory(private val stringProvider: StringProvider) {
|
||||
|
||||
fun create(event: TimelineEvent): NoticeItem? {
|
||||
val roomMember = event.roomMember ?: return null
|
||||
val noticeText = buildNoticeText(event.root, roomMember) ?: return null
|
||||
return NoticeItem_()
|
||||
.noticeText(noticeText)
|
||||
.avatarUrl(roomMember.avatarUrl)
|
||||
.memberName(roomMember.displayName)
|
||||
}
|
||||
|
||||
private fun buildNoticeText(event: Event, roomMember: RoomMember): CharSequence? {
|
||||
val content = event.content.toModel<RoomHistoryVisibilityContent>() ?: return null
|
||||
val formattedVisibility = when (content.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, roomMember.displayName, formattedVisibility)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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.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 roomMember = event.roomMember ?: return null
|
||||
val noticeText = buildRoomMemberNotice(event) ?: return null
|
||||
return NoticeItem_()
|
||||
.noticeText(noticeText)
|
||||
.avatarUrl(roomMember.avatarUrl)
|
||||
.memberName(roomMember.displayName)
|
||||
}
|
||||
|
||||
private fun buildRoomMemberNotice(event: TimelineEvent): String? {
|
||||
val eventContent: RoomMember? = event.root.content.toModel()
|
||||
val prevEventContent: RoomMember? = event.root.prevContent.toModel()
|
||||
val isMembershipEvent = prevEventContent?.membership != eventContent?.membership
|
||||
return if (isMembershipEvent) {
|
||||
buildMembershipNotice(event, eventContent, prevEventContent)
|
||||
} else {
|
||||
buildProfileNotice(event, eventContent, prevEventContent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildProfileNotice(event: TimelineEvent, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
|
||||
val displayText = StringBuilder()
|
||||
// Check display name has been changed
|
||||
if (!TextUtils.equals(eventContent?.displayName, prevEventContent?.displayName)) {
|
||||
val displayNameText = when {
|
||||
prevEventContent?.displayName.isNullOrEmpty() ->
|
||||
stringProvider.getString(R.string.notice_display_name_set, event.root.sender, eventContent?.displayName)
|
||||
eventContent?.displayName.isNullOrEmpty() ->
|
||||
stringProvider.getString(R.string.notice_display_name_removed, event.root.sender, prevEventContent?.displayName)
|
||||
else ->
|
||||
stringProvider.getString(R.string.notice_display_name_changed_from,
|
||||
event.root.sender, prevEventContent?.displayName, eventContent?.displayName)
|
||||
}
|
||||
displayText.append(displayNameText)
|
||||
}
|
||||
// Check whether the avatar has been changed
|
||||
if (!TextUtils.equals(eventContent?.avatarUrl, prevEventContent?.avatarUrl)) {
|
||||
val displayAvatarText = if (displayText.isNotEmpty()) {
|
||||
displayText.append(" ")
|
||||
stringProvider.getString(R.string.notice_avatar_changed_too)
|
||||
} else {
|
||||
stringProvider.getString(R.string.notice_avatar_url_changed, event.roomMember?.displayName)
|
||||
}
|
||||
displayText.append(displayAvatarText)
|
||||
}
|
||||
return displayText.toString()
|
||||
}
|
||||
|
||||
private fun buildMembershipNotice(event: TimelineEvent, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
|
||||
val senderDisplayName = event.roomMember?.displayName ?: return null
|
||||
val targetDisplayName = eventContent?.displayName ?: event.root.sender
|
||||
return when {
|
||||
Membership.INVITE == eventContent?.membership -> {
|
||||
// TODO get userId
|
||||
val selfUserId: String = ""
|
||||
when {
|
||||
eventContent.thirdPartyInvite != null ->
|
||||
stringProvider.getString(R.string.notice_room_third_party_registered_invite,
|
||||
targetDisplayName, eventContent.thirdPartyInvite?.displayName)
|
||||
TextUtils.equals(event.root.stateKey, selfUserId) ->
|
||||
stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName)
|
||||
event.root.stateKey.isNullOrEmpty() ->
|
||||
stringProvider.getString(R.string.notice_room_invite_no_invitee, senderDisplayName)
|
||||
else ->
|
||||
stringProvider.getString(R.string.notice_room_invite, senderDisplayName, targetDisplayName)
|
||||
}
|
||||
}
|
||||
Membership.JOIN == eventContent?.membership ->
|
||||
stringProvider.getString(R.string.notice_room_join, senderDisplayName)
|
||||
Membership.LEAVE == eventContent?.membership ->
|
||||
// 2 cases here: this member may have left voluntarily or they may have been "left" by someone else ie. kicked
|
||||
return if (TextUtils.equals(event.root.sender, event.root.stateKey)) {
|
||||
if (prevEventContent?.membership == Membership.INVITE) {
|
||||
stringProvider.getString(R.string.notice_room_reject, senderDisplayName)
|
||||
} else {
|
||||
stringProvider.getString(R.string.notice_room_leave, senderDisplayName)
|
||||
}
|
||||
} else if (prevEventContent?.membership == Membership.INVITE) {
|
||||
stringProvider.getString(R.string.notice_room_withdraw, senderDisplayName, targetDisplayName)
|
||||
} else if (prevEventContent?.membership == Membership.JOIN) {
|
||||
stringProvider.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName)
|
||||
} else if (prevEventContent?.membership == Membership.BAN) {
|
||||
stringProvider.getString(R.string.notice_room_unban, senderDisplayName, targetDisplayName)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
Membership.BAN == eventContent?.membership ->
|
||||
stringProvider.getString(R.string.notice_room_ban, senderDisplayName, targetDisplayName)
|
||||
Membership.KNOCK == eventContent?.membership ->
|
||||
stringProvider.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline.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()
|
||||
val roomMember = event.roomMember
|
||||
if (content == null || roomMember == null) {
|
||||
return null
|
||||
}
|
||||
val text = if (!TextUtils.isEmpty(content.name)) {
|
||||
stringProvider.getString(R.string.notice_room_name_changed, roomMember.displayName, content.name)
|
||||
} else {
|
||||
stringProvider.getString(R.string.notice_room_name_removed, roomMember.displayName)
|
||||
}
|
||||
return NoticeItem_()
|
||||
.noticeText(text)
|
||||
.avatarUrl(roomMember.avatarUrl)
|
||||
.memberName(roomMember.displayName)
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.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.item.NoticeItem
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_
|
||||
|
||||
class RoomTopicItemFactory(private val stringProvider: StringProvider) {
|
||||
|
||||
fun create(event: TimelineEvent): NoticeItem? {
|
||||
|
||||
val content: RoomTopicContent? = event.root.content.toModel()
|
||||
val roomMember = event.roomMember
|
||||
if (content == null || roomMember == null) {
|
||||
return null
|
||||
}
|
||||
val text = if (content.topic.isNullOrEmpty()) {
|
||||
stringProvider.getString(R.string.notice_room_topic_removed, roomMember.displayName)
|
||||
} else {
|
||||
stringProvider.getString(R.string.notice_room_topic_changed, roomMember.displayName, content.topic)
|
||||
}
|
||||
return NoticeItem_()
|
||||
.noticeText(text)
|
||||
.avatarUrl(roomMember.avatarUrl)
|
||||
.memberName(roomMember.displayName)
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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.timeline.factory
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotredesign.core.epoxy.EmptyItem_
|
||||
import im.vector.riotredesign.core.epoxy.RiotEpoxyModel
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||
|
||||
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 defaultItemFactory: DefaultItemFactory) {
|
||||
|
||||
fun create(event: TimelineEvent,
|
||||
nextEvent: TimelineEvent?,
|
||||
callback: TimelineEventController.Callback?): RiotEpoxyModel<*> {
|
||||
|
||||
val computedModel = try {
|
||||
when (event.root.type) {
|
||||
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, callback)
|
||||
EventType.STATE_ROOM_NAME -> roomNameItemFactory.create(event)
|
||||
EventType.STATE_ROOM_TOPIC -> roomTopicItemFactory.create(event)
|
||||
EventType.STATE_ROOM_MEMBER -> roomMemberItemFactory.create(event)
|
||||
EventType.STATE_HISTORY_VISIBILITY -> roomHistoryVisibilityItemFactory.create(event)
|
||||
|
||||
EventType.CALL_INVITE,
|
||||
EventType.CALL_HANGUP,
|
||||
EventType.CALL_ANSWER -> callItemFactory.create(event)
|
||||
|
||||
EventType.ENCRYPTED,
|
||||
EventType.ENCRYPTION,
|
||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
||||
EventType.STICKER,
|
||||
EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event)
|
||||
|
||||
else -> null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
defaultItemFactory.create(event, e)
|
||||
}
|
||||
return computedModel ?: EmptyItem_()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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.helper
|
||||
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
|
||||
class EndlessRecyclerViewScrollListener(private val layoutManager: LinearLayoutManager,
|
||||
private val visibleThreshold: Int,
|
||||
private val onLoadMore: (Timeline.Direction) -> Unit
|
||||
) : RecyclerView.OnScrollListener() {
|
||||
|
||||
// The total number of items in the dataset after the last load
|
||||
private var previousTotalItemCount = 0
|
||||
// True if we are still waiting for the last set of data to load.
|
||||
private var loadingBackwards = true
|
||||
private var loadingForwards = true
|
||||
|
||||
// This happens many times a second during a scroll, so be wary of the code you place here.
|
||||
// We are given a few useful parameters to help us work out if we need to load some more data,
|
||||
// but first we check if we are waiting for the previous load to finish.
|
||||
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
|
||||
val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition()
|
||||
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
||||
val totalItemCount = layoutManager.itemCount
|
||||
|
||||
// The minimum amount of items to have below your current scroll position
|
||||
// before loading more.
|
||||
// If the total item count is zero and the previous isn't, assume the
|
||||
// list is invalidated and should be reset back to initial state
|
||||
if (totalItemCount < previousTotalItemCount) {
|
||||
previousTotalItemCount = totalItemCount
|
||||
if (totalItemCount == 0) {
|
||||
loadingForwards = true
|
||||
loadingBackwards = true
|
||||
}
|
||||
}
|
||||
// If it’s still loading, we check to see if the dataset count has
|
||||
// changed, if so we conclude it has finished loading
|
||||
if (totalItemCount > previousTotalItemCount) {
|
||||
loadingBackwards = false
|
||||
loadingForwards = false
|
||||
previousTotalItemCount = totalItemCount
|
||||
}
|
||||
// If it isn’t currently loading, we check to see if we have reached
|
||||
// the visibleThreshold and need to reload more data.
|
||||
if (!loadingBackwards && lastVisibleItemPosition + visibleThreshold > totalItemCount) {
|
||||
loadingBackwards = true
|
||||
onLoadMore(Timeline.Direction.BACKWARDS)
|
||||
}
|
||||
if (!loadingForwards && firstVisibleItemPosition < visibleThreshold) {
|
||||
loadingForwards = true
|
||||
onLoadMore(Timeline.Direction.FORWARDS)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright 2019 New Vector Ltd
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline.helper
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
|
||||
private const val THREAD_NAME = "Timeline_Building_Thread"
|
||||
|
||||
object TimelineAsyncHelper {
|
||||
|
||||
private var backgroundHandler: Handler? = null
|
||||
|
||||
fun getBackgroundHandler(): Handler {
|
||||
return backgroundHandler ?: createBackgroundHandler().also { backgroundHandler = it }
|
||||
}
|
||||
|
||||
private fun createBackgroundHandler(): Handler {
|
||||
val handlerThread = HandlerThread(THREAD_NAME)
|
||||
handlerThread.start()
|
||||
return Handler(handlerThread.looper)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline.helper
|
||||
|
||||
import im.vector.riotredesign.core.resources.LocaleProvider
|
||||
import org.threeten.bp.LocalDateTime
|
||||
import org.threeten.bp.format.DateTimeFormatter
|
||||
|
||||
class TimelineDateFormatter(private val localeProvider: LocaleProvider) {
|
||||
|
||||
private val messageHourFormatter by lazy {
|
||||
DateTimeFormatter.ofPattern("H:mm", localeProvider.current())
|
||||
}
|
||||
private val messageDayFormatter by lazy {
|
||||
DateTimeFormatter.ofPattern("EEE d MMM", localeProvider.current())
|
||||
}
|
||||
|
||||
fun formatMessageHour(localDateTime: LocalDateTime): String {
|
||||
return messageHourFormatter.format(localDateTime)
|
||||
}
|
||||
|
||||
fun formatMessageDay(localDateTime: LocalDateTime): String {
|
||||
return messageDayFormatter.format(localDateTime)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline.helper
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
|
||||
object TimelineDisplayableEvents {
|
||||
|
||||
val DISPLAYABLE_TYPES = listOf(
|
||||
EventType.MESSAGE,
|
||||
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,
|
||||
EventType.ENCRYPTED,
|
||||
EventType.ENCRYPTION,
|
||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
||||
EventType.STICKER,
|
||||
EventType.STATE_ROOM_CREATE
|
||||
)
|
||||
}
|
||||
|
||||
fun TimelineEvent.isDisplayable(): Boolean {
|
||||
return TimelineDisplayableEvents.DISPLAYABLE_TYPES.contains(root.type) && !root.content.isNullOrEmpty()
|
||||
}
|
||||
|
||||
fun List<TimelineEvent>.filterDisplayableEvents(): List<TimelineEvent> {
|
||||
return this.filter {
|
||||
it.isDisplayable()
|
||||
}
|
||||
}
|
||||
|
||||
fun List<TimelineEvent>.nextDisplayableEvent(index: Int): TimelineEvent? {
|
||||
return if (index == size - 1) {
|
||||
null
|
||||
} else {
|
||||
subList(index + 1, this.size).firstOrNull { it.isDisplayable() }
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline.helper
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
|
||||
class TimelineEventDiffUtilCallback(private val oldList: List<TimelineEvent>,
|
||||
private val newList: List<TimelineEvent>) : DiffUtil.Callback() {
|
||||
|
||||
override fun getOldListSize(): Int {
|
||||
return oldList.size
|
||||
}
|
||||
|
||||
override fun getNewListSize(): Int {
|
||||
return newList.size
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val oldItem = oldList[oldItemPosition]
|
||||
val newItem = newList[newItemPosition]
|
||||
return oldItem.localId == newItem.localId
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val oldItem = oldList[oldItemPosition]
|
||||
val newItem = newList[newItemPosition]
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline.helper
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class TimelineMediaSizeProvider {
|
||||
|
||||
lateinit var recyclerView: RecyclerView
|
||||
private var cachedSize: Pair<Int, Int>? = null
|
||||
|
||||
fun getMaxSize(): Pair<Int, Int> {
|
||||
return cachedSize ?: computeMaxSize().also { cachedSize = it }
|
||||
}
|
||||
|
||||
private fun computeMaxSize(): Pair<Int, Int> {
|
||||
val width = recyclerView.width
|
||||
val height = recyclerView.height
|
||||
val maxImageWidth: Int
|
||||
val maxImageHeight: Int
|
||||
// landscape / portrait
|
||||
if (width < height) {
|
||||
maxImageWidth = Math.round(width * 0.7f)
|
||||
maxImageHeight = Math.round(height * 0.5f)
|
||||
} else {
|
||||
maxImageWidth = Math.round(width * 0.5f)
|
||||
maxImageHeight = Math.round(height * 0.7f)
|
||||
}
|
||||
return Pair(maxImageWidth, maxImageHeight)
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import im.vector.riotredesign.core.epoxy.RiotEpoxyHolder
|
||||
import im.vector.riotredesign.core.epoxy.RiotEpoxyModel
|
||||
import im.vector.riotredesign.features.home.AvatarRenderer
|
||||
|
||||
abstract class AbsMessageItem<H : AbsMessageItem.Holder> : RiotEpoxyModel<H>() {
|
||||
|
||||
abstract val informationData: MessageInformationData
|
||||
|
||||
override fun bind(holder: H) {
|
||||
if (informationData.showInformation) {
|
||||
holder.avatarImageView.visibility = View.VISIBLE
|
||||
holder.memberNameView.visibility = View.VISIBLE
|
||||
holder.timeView.visibility = View.VISIBLE
|
||||
holder.timeView.text = informationData.time
|
||||
holder.memberNameView.text = informationData.memberName
|
||||
AvatarRenderer.render(informationData.avatarUrl, informationData.memberName?.toString(), holder.avatarImageView)
|
||||
} else {
|
||||
holder.avatarImageView.visibility = View.GONE
|
||||
holder.memberNameView.visibility = View.GONE
|
||||
holder.timeView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Holder : RiotEpoxyHolder() {
|
||||
abstract val avatarImageView: ImageView
|
||||
abstract val memberNameView: TextView
|
||||
abstract val timeView: TextView
|
||||
}
|
||||
|
||||
}
|
@ -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.features.home.room.detail.timeline.item
|
||||
|
||||
import android.widget.TextView
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import com.airbnb.epoxy.EpoxyModelWithHolder
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.epoxy.RiotEpoxyHolder
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_day_separator)
|
||||
abstract class DaySeparatorItem : EpoxyModelWithHolder<DaySeparatorItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute lateinit var formattedDay: CharSequence
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
holder.dayTextView.text = formattedDay
|
||||
}
|
||||
|
||||
class Holder : RiotEpoxyHolder() {
|
||||
val dayTextView by bind<TextView>(R.id.itemDayTextView)
|
||||
}
|
||||
}
|
@ -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.features.home.room.detail.timeline.item
|
||||
|
||||
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.RiotEpoxyHolder
|
||||
import im.vector.riotredesign.core.epoxy.RiotEpoxyModel
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_default)
|
||||
abstract class DefaultItem : RiotEpoxyModel<DefaultItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute var text: CharSequence? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
holder.messageView.text = text
|
||||
}
|
||||
|
||||
class Holder : RiotEpoxyHolder() {
|
||||
val messageView by bind<TextView>(R.id.stateMessageView)
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline.item
|
||||
|
||||
import android.view.View
|
||||
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.features.media.MediaContentRenderer
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_image_message)
|
||||
abstract class MessageImageItem : AbsMessageItem<MessageImageItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute lateinit var mediaData: MediaContentRenderer.Data
|
||||
@EpoxyAttribute override lateinit var informationData: MessageInformationData
|
||||
@EpoxyAttribute var clickListener: View.OnClickListener? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
MediaContentRenderer.render(mediaData, MediaContentRenderer.Mode.THUMBNAIL, holder.imageView)
|
||||
holder.imageView.setOnClickListener(clickListener)
|
||||
}
|
||||
|
||||
class Holder : AbsMessageItem.Holder() {
|
||||
override val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
|
||||
override val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
||||
override val timeView by bind<TextView>(R.id.messageTimeView)
|
||||
val imageView by bind<ImageView>(R.id.messageImageView)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
data class MessageInformationData(
|
||||
val time: CharSequence? = null,
|
||||
val avatarUrl: String?,
|
||||
val memberName: CharSequence? = null,
|
||||
val showInformation: Boolean = true
|
||||
)
|
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline.item
|
||||
|
||||
import android.text.Spannable
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import androidx.core.text.PrecomputedTextCompat
|
||||
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
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_text_message)
|
||||
abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute var message: Spannable? = null
|
||||
@EpoxyAttribute override lateinit var informationData: MessageInformationData
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
MatrixLinkify.addLinkMovementMethod(holder.messageView)
|
||||
val textFuture = PrecomputedTextCompat.getTextFuture(message ?: "",
|
||||
TextViewCompat.getTextMetricsParams(holder.messageView),
|
||||
null)
|
||||
holder.messageView.setTextFuture(textFuture)
|
||||
findPillsAndProcess { it.bind(holder.messageView) }
|
||||
}
|
||||
|
||||
private fun findPillsAndProcess(processBlock: (span: PillImageSpan) -> Unit) {
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
val pillImageSpans: Array<PillImageSpan>? = withContext(Dispatchers.IO) {
|
||||
message?.let { spannable ->
|
||||
spannable.getSpans(0, spannable.length, PillImageSpan::class.java)
|
||||
}
|
||||
}
|
||||
pillImageSpans?.forEach { processBlock(it) }
|
||||
}
|
||||
}
|
||||
|
||||
class Holder : AbsMessageItem.Holder() {
|
||||
override val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
|
||||
override val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
||||
override val timeView by bind<TextView>(R.id.messageTimeView)
|
||||
val messageView by bind<AppCompatTextView>(R.id.messageTextView)
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline.item
|
||||
|
||||
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.RiotEpoxyHolder
|
||||
import im.vector.riotredesign.core.epoxy.RiotEpoxyModel
|
||||
import im.vector.riotredesign.features.home.AvatarRenderer
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_notice)
|
||||
abstract class NoticeItem : RiotEpoxyModel<NoticeItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute var noticeText: CharSequence? = null
|
||||
@EpoxyAttribute var avatarUrl: String? = null
|
||||
@EpoxyAttribute var memberName: CharSequence? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
holder.noticeTextView.text = noticeText
|
||||
AvatarRenderer.render(avatarUrl, memberName?.toString(), holder.avatarImageView)
|
||||
}
|
||||
|
||||
class Holder : RiotEpoxyHolder() {
|
||||
val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView)
|
||||
val noticeTextView by bind<TextView>(R.id.itemNoticeTextView)
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.list
|
||||
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.epoxy.RiotEpoxyHolder
|
||||
import im.vector.riotredesign.core.epoxy.RiotEpoxyModel
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_room_category)
|
||||
abstract class RoomCategoryItem : RiotEpoxyModel<RoomCategoryItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute lateinit var title: CharSequence
|
||||
@EpoxyAttribute var expanded: Boolean = false
|
||||
@EpoxyAttribute var unreadCount: Int = 0
|
||||
@EpoxyAttribute var showHighlighted: Boolean = false
|
||||
@EpoxyAttribute var listener: (() -> Unit)? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
val tintColor = ContextCompat.getColor(holder.rootView.context, R.color.bluey_grey_two)
|
||||
val expandedArrowDrawableRes = if (expanded) R.drawable.ic_expand_more_white else R.drawable.ic_expand_less_white
|
||||
val expandedArrowDrawable = ContextCompat.getDrawable(holder.rootView.context, expandedArrowDrawableRes)?.also {
|
||||
DrawableCompat.setTint(it, tintColor)
|
||||
}
|
||||
holder.unreadCounterBadgeView.render(unreadCount, showHighlighted)
|
||||
holder.titleView.setCompoundDrawablesWithIntrinsicBounds(expandedArrowDrawable, null, null, null)
|
||||
holder.titleView.text = title
|
||||
holder.rootView.setOnClickListener { listener?.invoke() }
|
||||
}
|
||||
|
||||
|
||||
class Holder : RiotEpoxyHolder() {
|
||||
val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.roomCategoryUnreadCounterBadgeView)
|
||||
val titleView by bind<TextView>(R.id.roomCategoryTitleView)
|
||||
val rootView by bind<ViewGroup>(R.id.roomCategoryRootView)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
sealed class RoomListActions {
|
||||
|
||||
data class SelectRoom(val roomSummary: RoomSummary) : RoomListActions()
|
||||
|
||||
data class FilterRooms(val roomName: CharSequence? = null) : RoomListActions()
|
||||
|
||||
data class ToggleCategory(val category: RoomCategory) : RoomListActions()
|
||||
|
||||
}
|
@ -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.home.room.list
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Incomplete
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
|
||||
import im.vector.riotredesign.core.extensions.observeEvent
|
||||
import im.vector.riotredesign.core.extensions.setupAsSearch
|
||||
import im.vector.riotredesign.core.platform.RiotFragment
|
||||
import im.vector.riotredesign.core.platform.StateView
|
||||
import im.vector.riotredesign.features.home.HomeModule
|
||||
import im.vector.riotredesign.features.home.HomeNavigator
|
||||
import kotlinx.android.synthetic.main.fragment_room_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 RoomListFragment : RiotFragment(), RoomSummaryController.Callback {
|
||||
|
||||
companion object {
|
||||
fun newInstance(): RoomListFragment {
|
||||
return RoomListFragment()
|
||||
}
|
||||
}
|
||||
|
||||
private val roomController by inject<RoomSummaryController>()
|
||||
private val homeNavigator by inject<HomeNavigator>()
|
||||
private val roomListViewModel: RoomListViewModel by fragmentViewModel()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_room_list, container, false)
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
bindScope(getOrCreateScope(HomeModule.ROOM_LIST_SCOPE))
|
||||
setupRecyclerView()
|
||||
setupFilterView()
|
||||
roomListViewModel.subscribe { renderState(it) }
|
||||
roomListViewModel.openRoomLiveData.observeEvent(this) {
|
||||
homeNavigator.openRoomDetail(it, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
val layoutManager = LinearLayoutManager(context)
|
||||
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
|
||||
epoxyRecyclerView.layoutManager = layoutManager
|
||||
roomController.callback = this
|
||||
roomController.addModelBuildListener { it.dispatchTo(stateRestorer) }
|
||||
stateView.contentView = epoxyRecyclerView
|
||||
epoxyRecyclerView.setController(roomController)
|
||||
}
|
||||
|
||||
private fun setupFilterView() {
|
||||
filterRoomView.setupAsSearch()
|
||||
filterRoomView.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) = Unit
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||
roomListViewModel.accept(RoomListActions.FilterRooms(s))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun renderState(state: RoomListViewState) {
|
||||
when (state.asyncRooms) {
|
||||
is Incomplete -> renderLoading()
|
||||
is Success -> renderSuccess(state)
|
||||
is Fail -> renderFailure(state.asyncRooms.error)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderSuccess(state: RoomListViewState) {
|
||||
if (state.asyncRooms().isNullOrEmpty()) {
|
||||
stateView.state = StateView.State.Empty(getString(R.string.room_list_empty))
|
||||
} else {
|
||||
stateView.state = StateView.State.Content
|
||||
}
|
||||
roomController.setData(state)
|
||||
}
|
||||
|
||||
private fun renderLoading() {
|
||||
stateView.state = StateView.State.Loading
|
||||
}
|
||||
|
||||
private fun renderFailure(error: Throwable) {
|
||||
val message = when (error) {
|
||||
is Failure.NetworkConnection -> getString(R.string.network_error_please_check_and_retry)
|
||||
else -> getString(R.string.unknown_error)
|
||||
}
|
||||
stateView.state = StateView.State.Error(message)
|
||||
}
|
||||
|
||||
// RoomSummaryController.Callback **************************************************************
|
||||
|
||||
override fun onRoomSelected(room: RoomSummary) {
|
||||
roomListViewModel.accept(RoomListActions.SelectRoom(room))
|
||||
}
|
||||
|
||||
override fun onToggleRoomCategory(roomCategory: RoomCategory) {
|
||||
roomListViewModel.accept(RoomListActions.ToggleCategory(roomCategory))
|
||||
}
|
||||
}
|
@ -0,0 +1,183 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.list
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import arrow.core.Option
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.jakewharton.rxrelay2.BehaviorRelay
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.tag.RoomTag
|
||||
import im.vector.matrix.rx.rx
|
||||
import im.vector.riotredesign.core.platform.RiotViewModel
|
||||
import im.vector.riotredesign.core.utils.LiveEvent
|
||||
import im.vector.riotredesign.features.home.group.SelectedGroupStore
|
||||
import im.vector.riotredesign.features.home.room.VisibleRoomStore
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.functions.Function3
|
||||
import org.koin.android.ext.android.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
typealias RoomListFilterName = CharSequence
|
||||
|
||||
class RoomListViewModel(initialState: RoomListViewState,
|
||||
private val session: Session,
|
||||
private val selectedGroupHolder: SelectedGroupStore,
|
||||
private val visibleRoomHolder: VisibleRoomStore,
|
||||
private val roomSelectionRepository: RoomSelectionRepository,
|
||||
private val roomSummaryComparator: RoomSummaryComparator)
|
||||
: RiotViewModel<RoomListViewState>(initialState) {
|
||||
|
||||
companion object : MvRxViewModelFactory<RoomListViewModel, RoomListViewState> {
|
||||
|
||||
@JvmStatic
|
||||
override fun create(viewModelContext: ViewModelContext, state: RoomListViewState): RoomListViewModel? {
|
||||
val currentSession = viewModelContext.activity.get<Session>()
|
||||
val roomSelectionRepository = viewModelContext.activity.get<RoomSelectionRepository>()
|
||||
val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupStore>()
|
||||
val visibleRoomHolder = viewModelContext.activity.get<VisibleRoomStore>()
|
||||
val roomSummaryComparator = viewModelContext.activity.get<RoomSummaryComparator>()
|
||||
return RoomListViewModel(state, currentSession, selectedGroupHolder, visibleRoomHolder, roomSelectionRepository, roomSummaryComparator)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private val roomListFilter = BehaviorRelay.createDefault<Option<RoomListFilterName>>(Option.empty())
|
||||
|
||||
private val _openRoomLiveData = MutableLiveData<LiveEvent<String>>()
|
||||
val openRoomLiveData: LiveData<LiveEvent<String>>
|
||||
get() = _openRoomLiveData
|
||||
|
||||
init {
|
||||
observeRoomSummaries()
|
||||
observeVisibleRoom()
|
||||
}
|
||||
|
||||
fun accept(action: RoomListActions) {
|
||||
when (action) {
|
||||
is RoomListActions.SelectRoom -> handleSelectRoom(action)
|
||||
is RoomListActions.FilterRooms -> handleFilterRooms(action)
|
||||
is RoomListActions.ToggleCategory -> handleToggleCategory(action)
|
||||
}
|
||||
}
|
||||
|
||||
// PRIVATE METHODS *****************************************************************************
|
||||
|
||||
private fun handleSelectRoom(action: RoomListActions.SelectRoom) = withState { state ->
|
||||
if (state.visibleRoomId != action.roomSummary.roomId) {
|
||||
roomSelectionRepository.saveLastSelectedRoom(action.roomSummary.roomId)
|
||||
_openRoomLiveData.postValue(LiveEvent(action.roomSummary.roomId))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFilterRooms(action: RoomListActions.FilterRooms) {
|
||||
val optionalFilter = Option.fromNullable(action.roomName)
|
||||
roomListFilter.accept(optionalFilter)
|
||||
}
|
||||
|
||||
private fun handleToggleCategory(action: RoomListActions.ToggleCategory) = setState {
|
||||
this.toggle(action.category)
|
||||
}
|
||||
|
||||
private fun observeVisibleRoom() {
|
||||
visibleRoomHolder.observe()
|
||||
.doOnNext {
|
||||
setState { copy(visibleRoomId = it) }
|
||||
}
|
||||
.subscribe()
|
||||
.disposeOnClear()
|
||||
}
|
||||
|
||||
private fun observeRoomSummaries() {
|
||||
Observable.combineLatest<List<RoomSummary>, Option<GroupSummary>, Option<RoomListFilterName>, RoomSummaries>(
|
||||
session.rx().liveRoomSummaries().throttleLast(300, TimeUnit.MILLISECONDS),
|
||||
selectedGroupHolder.observe(),
|
||||
roomListFilter.throttleLast(300, TimeUnit.MILLISECONDS),
|
||||
Function3 { rooms, selectedGroupOption, filterRoomOption ->
|
||||
val filteredRooms = filterRooms(rooms, filterRoomOption)
|
||||
val selectedGroup = selectedGroupOption.orNull()
|
||||
val filteredDirectRooms = filteredRooms
|
||||
.filter { it.isDirect }
|
||||
.filter {
|
||||
if (selectedGroup == null) {
|
||||
true
|
||||
} else {
|
||||
it.otherMemberIds
|
||||
.intersect(selectedGroup.userIds)
|
||||
.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
val filteredGroupRooms = filteredRooms
|
||||
.filter { !it.isDirect }
|
||||
.filter {
|
||||
selectedGroup?.roomIds?.contains(it.roomId) ?: true
|
||||
}
|
||||
buildRoomSummaries(filteredDirectRooms + filteredGroupRooms)
|
||||
}
|
||||
)
|
||||
.execute { async ->
|
||||
copy(
|
||||
asyncRooms = async
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterRooms(rooms: List<RoomSummary>, filterRoomOption: Option<RoomListFilterName>): List<RoomSummary> {
|
||||
val filterRoom = filterRoomOption.orNull()
|
||||
return rooms.filter {
|
||||
if (filterRoom.isNullOrBlank()) {
|
||||
true
|
||||
} else {
|
||||
it.displayName.contains(other = filterRoom, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildRoomSummaries(rooms: List<RoomSummary>): RoomSummaries {
|
||||
val favourites = ArrayList<RoomSummary>()
|
||||
val directChats = ArrayList<RoomSummary>()
|
||||
val groupRooms = ArrayList<RoomSummary>()
|
||||
val lowPriorities = ArrayList<RoomSummary>()
|
||||
val serverNotices = ArrayList<RoomSummary>()
|
||||
|
||||
for (room in rooms) {
|
||||
val tags = room.tags.map { it.name }
|
||||
when {
|
||||
tags.contains(RoomTag.ROOM_TAG_SERVER_NOTICE) -> serverNotices.add(room)
|
||||
tags.contains(RoomTag.ROOM_TAG_FAVOURITE) -> favourites.add(room)
|
||||
tags.contains(RoomTag.ROOM_TAG_LOW_PRIORITY) -> lowPriorities.add(room)
|
||||
room.isDirect -> directChats.add(room)
|
||||
else -> groupRooms.add(room)
|
||||
}
|
||||
}
|
||||
|
||||
return RoomSummaries().apply {
|
||||
put(RoomCategory.FAVOURITE, favourites.sortedWith(roomSummaryComparator))
|
||||
put(RoomCategory.DIRECT, directChats.sortedWith(roomSummaryComparator))
|
||||
put(RoomCategory.GROUP, groupRooms.sortedWith(roomSummaryComparator))
|
||||
put(RoomCategory.LOW_PRIORITY, lowPriorities.sortedWith(roomSummaryComparator))
|
||||
put(RoomCategory.SERVER_NOTICE, serverNotices.sortedWith(roomSummaryComparator))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.list
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.riotredesign.R
|
||||
|
||||
data class RoomListViewState(
|
||||
val asyncRooms: Async<RoomSummaries> = Uninitialized,
|
||||
val visibleRoomId: String? = null,
|
||||
val isFavouriteRoomsExpanded: Boolean = true,
|
||||
val isDirectRoomsExpanded: Boolean = false,
|
||||
val isGroupRoomsExpanded: Boolean = false,
|
||||
val isLowPriorityRoomsExpanded: Boolean = false,
|
||||
val isServerNoticeRoomsExpanded: Boolean = false
|
||||
) : MvRxState {
|
||||
|
||||
fun isCategoryExpanded(roomCategory: RoomCategory): Boolean {
|
||||
return when (roomCategory) {
|
||||
RoomCategory.FAVOURITE -> isFavouriteRoomsExpanded
|
||||
RoomCategory.DIRECT -> isDirectRoomsExpanded
|
||||
RoomCategory.GROUP -> isGroupRoomsExpanded
|
||||
RoomCategory.LOW_PRIORITY -> isLowPriorityRoomsExpanded
|
||||
RoomCategory.SERVER_NOTICE -> isServerNoticeRoomsExpanded
|
||||
}
|
||||
}
|
||||
|
||||
fun toggle(roomCategory: RoomCategory): RoomListViewState {
|
||||
return when (roomCategory) {
|
||||
RoomCategory.FAVOURITE -> copy(isFavouriteRoomsExpanded = !isFavouriteRoomsExpanded)
|
||||
RoomCategory.DIRECT -> copy(isDirectRoomsExpanded = !isDirectRoomsExpanded)
|
||||
RoomCategory.GROUP -> copy(isGroupRoomsExpanded = !isGroupRoomsExpanded)
|
||||
RoomCategory.LOW_PRIORITY -> copy(isLowPriorityRoomsExpanded = !isLowPriorityRoomsExpanded)
|
||||
RoomCategory.SERVER_NOTICE -> copy(isServerNoticeRoomsExpanded = !isServerNoticeRoomsExpanded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typealias RoomSummaries = LinkedHashMap<RoomCategory, List<RoomSummary>>
|
||||
|
||||
enum class RoomCategory(@StringRes val titleRes: Int) {
|
||||
FAVOURITE(R.string.bottom_action_favourites),
|
||||
DIRECT(R.string.bottom_action_people),
|
||||
GROUP(R.string.bottom_action_rooms),
|
||||
LOW_PRIORITY(R.string.low_priority_header),
|
||||
SERVER_NOTICE(R.string.system_alerts_header)
|
||||
}
|
||||
|
||||
fun RoomSummaries?.isNullOrEmpty(): Boolean {
|
||||
return this == null || isEmpty()
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.list
|
||||
|
||||
import android.content.SharedPreferences
|
||||
|
||||
private const val SHARED_PREFS_SELECTED_ROOM_KEY = "SHARED_PREFS_SELECTED_ROOM_KEY"
|
||||
|
||||
class RoomSelectionRepository(private val sharedPreferences: SharedPreferences) {
|
||||
|
||||
fun lastSelectedRoom(): String? {
|
||||
return sharedPreferences.getString(SHARED_PREFS_SELECTED_ROOM_KEY, null)
|
||||
}
|
||||
|
||||
fun saveLastSelectedRoom(roomId: String) {
|
||||
sharedPreferences.edit()
|
||||
.putString(SHARED_PREFS_SELECTED_ROOM_KEY, roomId)
|
||||
.apply()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.list
|
||||
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
|
||||
class RoomSummaryComparator
|
||||
: Comparator<RoomSummary> {
|
||||
|
||||
override fun compare(leftRoomSummary: RoomSummary?, rightRoomSummary: RoomSummary?): Int {
|
||||
val retValue: Int
|
||||
var leftHighlightCount = 0
|
||||
var rightHighlightCount = 0
|
||||
var leftNotificationCount = 0
|
||||
var rightNotificationCount = 0
|
||||
var rightTimestamp = 0L
|
||||
var leftTimestamp = 0L
|
||||
|
||||
if (null != leftRoomSummary) {
|
||||
leftHighlightCount = leftRoomSummary.highlightCount
|
||||
leftNotificationCount = leftRoomSummary.notificationCount
|
||||
leftTimestamp = leftRoomSummary.lastMessage?.originServerTs ?: 0
|
||||
}
|
||||
if (null != rightRoomSummary) {
|
||||
rightHighlightCount = rightRoomSummary.highlightCount
|
||||
rightNotificationCount = rightRoomSummary.notificationCount
|
||||
rightTimestamp = rightRoomSummary.lastMessage?.originServerTs ?: 0
|
||||
}
|
||||
|
||||
if (rightRoomSummary?.lastMessage == null) {
|
||||
retValue = -1
|
||||
} else if (leftRoomSummary?.lastMessage == null) {
|
||||
retValue = 1
|
||||
} else if (rightHighlightCount > 0 && leftHighlightCount == 0) {
|
||||
retValue = 1
|
||||
} else if (rightHighlightCount == 0 && leftHighlightCount > 0) {
|
||||
retValue = -1
|
||||
} else if (rightNotificationCount > 0 && leftNotificationCount == 0) {
|
||||
retValue = 1
|
||||
} else if (rightNotificationCount == 0 && leftNotificationCount > 0) {
|
||||
retValue = -1
|
||||
} else {
|
||||
val deltaTimestamp = rightTimestamp - leftTimestamp
|
||||
if (deltaTimestamp > 0) {
|
||||
retValue = 1
|
||||
} else if (deltaTimestamp < 0) {
|
||||
retValue = -1
|
||||
} else {
|
||||
retValue = 0
|
||||
}
|
||||
}
|
||||
return retValue
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,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.features.home.room.list
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.airbnb.epoxy.TypedEpoxyController
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.riotredesign.core.resources.StringProvider
|
||||
|
||||
class RoomSummaryController(private val stringProvider: StringProvider
|
||||
) : TypedEpoxyController<RoomListViewState>() {
|
||||
|
||||
var callback: Callback? = null
|
||||
|
||||
override fun buildModels(viewState: RoomListViewState) {
|
||||
val roomSummaries = viewState.asyncRooms()
|
||||
roomSummaries?.forEach { (category, summaries) ->
|
||||
if (summaries.isEmpty()) {
|
||||
return@forEach
|
||||
} else {
|
||||
val isExpanded = viewState.isCategoryExpanded(category)
|
||||
buildRoomCategory(viewState, summaries, category.titleRes, viewState.isCategoryExpanded(category)) {
|
||||
callback?.onToggleRoomCategory(category)
|
||||
}
|
||||
if (isExpanded) {
|
||||
buildRoomModels(summaries, viewState.visibleRoomId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildRoomCategory(viewState: RoomListViewState,
|
||||
summaries: List<RoomSummary>,
|
||||
@StringRes titleRes: Int,
|
||||
isExpanded: Boolean,
|
||||
mutateExpandedState: () -> Unit) {
|
||||
if (summaries.isEmpty()) {
|
||||
return
|
||||
}
|
||||
//TODO should add some business logic later
|
||||
val unreadCount = if (summaries.isEmpty()) {
|
||||
0
|
||||
} else {
|
||||
summaries.map { it.notificationCount }.reduce { acc, i -> acc + i }
|
||||
}
|
||||
val showHighlighted = summaries.any { it.highlightCount > 0 }
|
||||
roomCategoryItem {
|
||||
id(titleRes)
|
||||
title(stringProvider.getString(titleRes).toUpperCase())
|
||||
expanded(isExpanded)
|
||||
unreadCount(unreadCount)
|
||||
showHighlighted(showHighlighted)
|
||||
listener {
|
||||
mutateExpandedState()
|
||||
setData(viewState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildRoomModels(summaries: List<RoomSummary>, selectedRoomId: String?) {
|
||||
summaries.forEach { roomSummary ->
|
||||
val unreadCount = roomSummary.notificationCount
|
||||
val showHighlighted = roomSummary.highlightCount > 0
|
||||
val isSelected = roomSummary.roomId == selectedRoomId
|
||||
|
||||
roomSummaryItem {
|
||||
id(roomSummary.roomId)
|
||||
roomName(roomSummary.displayName)
|
||||
avatarUrl(roomSummary.avatarUrl)
|
||||
selected(isSelected)
|
||||
showHighlighted(showHighlighted)
|
||||
unreadCount(unreadCount)
|
||||
listener { callback?.onRoomSelected(roomSummary) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onToggleRoomCategory(roomCategory: RoomCategory)
|
||||
fun onRoomSelected(room: RoomSummary)
|
||||
}
|
||||
|
||||
}
|
@ -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.room.list
|
||||
|
||||
object RoomSummaryFormatter {
|
||||
|
||||
/**
|
||||
* Format the unread messages counter.
|
||||
*
|
||||
* @param count the count
|
||||
* @return the formatted value
|
||||
*/
|
||||
fun formatUnreadMessagesCounter(count: Int): String {
|
||||
return if (count > 999) {
|
||||
"${count / 1000}.${count % 1000 / 100}K"
|
||||
} else {
|
||||
count.toString()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.list
|
||||
|
||||
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.RiotEpoxyHolder
|
||||
import im.vector.riotredesign.core.epoxy.RiotEpoxyModel
|
||||
import im.vector.riotredesign.core.platform.CheckableFrameLayout
|
||||
import im.vector.riotredesign.features.home.AvatarRenderer
|
||||
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_room)
|
||||
abstract class RoomSummaryItem : RiotEpoxyModel<RoomSummaryItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute lateinit var roomName: CharSequence
|
||||
@EpoxyAttribute var avatarUrl: String? = null
|
||||
@EpoxyAttribute var selected: Boolean = false
|
||||
@EpoxyAttribute var unreadCount: Int = 0
|
||||
@EpoxyAttribute var showHighlighted: Boolean = false
|
||||
@EpoxyAttribute var listener: (() -> Unit)? = null
|
||||
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
holder.unreadCounterBadgeView.render(unreadCount, showHighlighted)
|
||||
holder.rootView.isChecked = selected
|
||||
holder.rootView.setOnClickListener { listener?.invoke() }
|
||||
holder.titleView.text = roomName
|
||||
AvatarRenderer.render(avatarUrl, roomName.toString(), holder.avatarImageView)
|
||||
}
|
||||
|
||||
class Holder : RiotEpoxyHolder() {
|
||||
val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.roomUnreadCounterBadgeView)
|
||||
val titleView by bind<TextView>(R.id.roomNameView)
|
||||
val avatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
|
||||
val rootView by bind<CheckableFrameLayout>(R.id.itemRoomLayout)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotredesign.features.home.room.list
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import im.vector.riotredesign.R
|
||||
|
||||
class UnreadCounterBadgeView : AppCompatTextView {
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
|
||||
fun render(count: Int, highlighted: Boolean) {
|
||||
if (count == 0) {
|
||||
visibility = View.INVISIBLE
|
||||
} else {
|
||||
visibility = View.VISIBLE
|
||||
val bgRes = if (highlighted) {
|
||||
R.drawable.bg_unread_highlight
|
||||
} else {
|
||||
R.drawable.bg_unread_notification
|
||||
}
|
||||
setBackgroundResource(bgRes)
|
||||
text = RoomSummaryFormatter.formatUnreadMessagesCounter(count)
|
||||
}
|
||||
}
|
||||
|
||||
enum class Status {
|
||||
NOTIFICATION,
|
||||
HIGHLIGHT
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,186 @@
|
||||
/*
|
||||
*
|
||||
* * 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.html
|
||||
|
||||
import android.content.Context
|
||||
import im.vector.matrix.android.api.permalinks.PermalinkData
|
||||
import im.vector.matrix.android.api.permalinks.PermalinkParser
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.riotredesign.core.glide.GlideRequests
|
||||
import org.commonmark.node.BlockQuote
|
||||
import org.commonmark.node.HtmlBlock
|
||||
import org.commonmark.node.HtmlInline
|
||||
import org.commonmark.node.Node
|
||||
import ru.noties.markwon.AbstractMarkwonPlugin
|
||||
import ru.noties.markwon.Markwon
|
||||
import ru.noties.markwon.MarkwonConfiguration
|
||||
import ru.noties.markwon.MarkwonVisitor
|
||||
import ru.noties.markwon.SpannableBuilder
|
||||
import ru.noties.markwon.html.HtmlTag
|
||||
import ru.noties.markwon.html.MarkwonHtmlParserImpl
|
||||
import ru.noties.markwon.html.MarkwonHtmlRenderer
|
||||
import ru.noties.markwon.html.TagHandler
|
||||
import ru.noties.markwon.html.tag.BlockquoteHandler
|
||||
import ru.noties.markwon.html.tag.EmphasisHandler
|
||||
import ru.noties.markwon.html.tag.HeadingHandler
|
||||
import ru.noties.markwon.html.tag.ImageHandler
|
||||
import ru.noties.markwon.html.tag.LinkHandler
|
||||
import ru.noties.markwon.html.tag.ListHandler
|
||||
import ru.noties.markwon.html.tag.StrikeHandler
|
||||
import ru.noties.markwon.html.tag.StrongEmphasisHandler
|
||||
import ru.noties.markwon.html.tag.SubScriptHandler
|
||||
import ru.noties.markwon.html.tag.SuperScriptHandler
|
||||
import ru.noties.markwon.html.tag.UnderlineHandler
|
||||
import java.util.Arrays.asList
|
||||
|
||||
class EventHtmlRenderer(glideRequests: GlideRequests,
|
||||
context: Context,
|
||||
session: Session) {
|
||||
|
||||
private val markwon = Markwon.builder(context)
|
||||
.usePlugin(MatrixPlugin.create(glideRequests, context, session))
|
||||
.build()
|
||||
|
||||
fun render(text: String): CharSequence {
|
||||
return markwon.toMarkdown(text)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class MatrixPlugin private constructor(private val glideRequests: GlideRequests,
|
||||
private val context: Context,
|
||||
private val session: Session) : AbstractMarkwonPlugin() {
|
||||
|
||||
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
|
||||
builder.htmlParser(MarkwonHtmlParserImpl.create())
|
||||
}
|
||||
|
||||
override fun configureHtmlRenderer(builder: MarkwonHtmlRenderer.Builder) {
|
||||
builder
|
||||
.setHandler(
|
||||
"img",
|
||||
ImageHandler.create())
|
||||
.setHandler(
|
||||
"a",
|
||||
MxLinkHandler(glideRequests, context, session))
|
||||
.setHandler(
|
||||
"blockquote",
|
||||
BlockquoteHandler())
|
||||
.setHandler(
|
||||
"sub",
|
||||
SubScriptHandler())
|
||||
.setHandler(
|
||||
"sup",
|
||||
SuperScriptHandler())
|
||||
.setHandler(
|
||||
asList<String>("b", "strong"),
|
||||
StrongEmphasisHandler())
|
||||
.setHandler(
|
||||
asList<String>("s", "del"),
|
||||
StrikeHandler())
|
||||
.setHandler(
|
||||
asList<String>("u", "ins"),
|
||||
UnderlineHandler())
|
||||
.setHandler(
|
||||
asList<String>("ul", "ol"),
|
||||
ListHandler())
|
||||
.setHandler(
|
||||
asList<String>("i", "em", "cite", "dfn"),
|
||||
EmphasisHandler())
|
||||
.setHandler(
|
||||
asList<String>("h1", "h2", "h3", "h4", "h5", "h6"),
|
||||
HeadingHandler())
|
||||
.setHandler("mx-reply",
|
||||
MxReplyTagHandler())
|
||||
|
||||
}
|
||||
|
||||
override fun afterRender(node: Node, visitor: MarkwonVisitor) {
|
||||
val configuration = visitor.configuration()
|
||||
configuration.htmlRenderer().render(visitor, configuration.htmlParser())
|
||||
}
|
||||
|
||||
override fun configureVisitor(builder: MarkwonVisitor.Builder) {
|
||||
builder
|
||||
.on(HtmlBlock::class.java) { visitor, htmlBlock -> visitHtml(visitor, htmlBlock.literal) }
|
||||
.on(HtmlInline::class.java) { visitor, htmlInline -> visitHtml(visitor, htmlInline.literal) }
|
||||
}
|
||||
|
||||
private fun visitHtml(visitor: MarkwonVisitor, html: String?) {
|
||||
if (html != null) {
|
||||
visitor.configuration().htmlParser().processFragment(visitor.builder(), html)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun create(glideRequests: GlideRequests, context: Context, session: Session): MatrixPlugin {
|
||||
return MatrixPlugin(glideRequests, context, session)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class MxLinkHandler(private val glideRequests: GlideRequests,
|
||||
private val context: Context,
|
||||
private val session: Session) : TagHandler() {
|
||||
|
||||
private val linkHandler = LinkHandler()
|
||||
|
||||
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
|
||||
val link = tag.attributes()["href"]
|
||||
if (link != null) {
|
||||
val permalinkData = PermalinkParser.parse(link)
|
||||
when (permalinkData) {
|
||||
is PermalinkData.UserLink -> {
|
||||
val user = session.getUser(permalinkData.userId)
|
||||
val span = PillImageSpan(glideRequests, context, permalinkData.userId, user)
|
||||
SpannableBuilder.setSpans(
|
||||
visitor.builder(),
|
||||
span,
|
||||
tag.start(),
|
||||
tag.end()
|
||||
)
|
||||
}
|
||||
else -> linkHandler.handle(visitor, renderer, tag)
|
||||
}
|
||||
} else {
|
||||
linkHandler.handle(visitor, renderer, tag)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class MxReplyTagHandler : TagHandler() {
|
||||
|
||||
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
|
||||
val configuration = visitor.configuration()
|
||||
val factory = configuration.spansFactory().get(BlockQuote::class.java)
|
||||
if (factory != null) {
|
||||
SpannableBuilder.setSpans(
|
||||
visitor.builder(),
|
||||
factory.getSpans(configuration, visitor.renderProps()),
|
||||
tag.start(),
|
||||
tag.end()
|
||||
)
|
||||
val replyText = visitor.builder().removeFromEnd(tag.end())
|
||||
visitor.builder().append("\n\n").append(replyText)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,136 @@
|
||||
/*
|
||||
* 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.html
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.style.ReplacementSpan
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.UiThread
|
||||
import com.bumptech.glide.request.target.SimpleTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import com.google.android.material.chip.ChipDrawable
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.glide.GlideRequests
|
||||
import im.vector.riotredesign.features.home.AvatarRenderer
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/**
|
||||
* This span is able to replace a text by a [ChipDrawable]
|
||||
* It's needed to call [bind] method to start requesting avatar, otherwise only the placeholder icon will be displayed if not already cached.
|
||||
*/
|
||||
|
||||
class PillImageSpan(private val glideRequests: GlideRequests,
|
||||
private val context: Context,
|
||||
private val userId: String,
|
||||
private val user: User?) : ReplacementSpan() {
|
||||
|
||||
private val displayName by lazy {
|
||||
if (user?.displayName.isNullOrEmpty()) userId else user?.displayName!!
|
||||
}
|
||||
|
||||
private val pillDrawable = createChipDrawable()
|
||||
private val target = PillImageSpanTarget(this)
|
||||
private var tv: WeakReference<TextView>? = null
|
||||
|
||||
@UiThread
|
||||
fun bind(textView: TextView) {
|
||||
tv = WeakReference(textView)
|
||||
AvatarRenderer.render(context, glideRequests, user?.avatarUrl, displayName, target)
|
||||
}
|
||||
|
||||
// ReplacementSpan *****************************************************************************
|
||||
|
||||
override fun getSize(paint: Paint, text: CharSequence,
|
||||
start: Int,
|
||||
end: Int,
|
||||
fm: Paint.FontMetricsInt?): Int {
|
||||
val rect = pillDrawable.bounds
|
||||
if (fm != null) {
|
||||
fm.ascent = -rect.bottom
|
||||
fm.descent = 0
|
||||
fm.top = fm.ascent
|
||||
fm.bottom = 0
|
||||
}
|
||||
return rect.right
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas, text: CharSequence,
|
||||
start: Int,
|
||||
end: Int,
|
||||
x: Float,
|
||||
top: Int,
|
||||
y: Int,
|
||||
bottom: Int,
|
||||
paint: Paint) {
|
||||
canvas.save()
|
||||
val transY = bottom - pillDrawable.bounds.bottom
|
||||
canvas.translate(x, transY.toFloat())
|
||||
pillDrawable.draw(canvas)
|
||||
canvas.restore()
|
||||
}
|
||||
|
||||
internal fun updateAvatarDrawable(drawable: Drawable?) {
|
||||
pillDrawable.apply {
|
||||
chipIcon = drawable
|
||||
}
|
||||
tv?.get()?.apply {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
// Private methods *****************************************************************************
|
||||
|
||||
private fun createChipDrawable(): ChipDrawable {
|
||||
val textPadding = context.resources.getDimension(R.dimen.pill_text_padding)
|
||||
return ChipDrawable.createFromResource(context, R.xml.pill_view).apply {
|
||||
setText(displayName)
|
||||
textEndPadding = textPadding
|
||||
textStartPadding = textPadding
|
||||
setChipMinHeightResource(R.dimen.pill_min_height)
|
||||
setChipIconSizeResource(R.dimen.pill_avatar_size)
|
||||
chipIcon = AvatarRenderer.getPlaceholderDrawable(context, displayName)
|
||||
setBounds(0, 0, intrinsicWidth, intrinsicHeight)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Glide target to handle avatar retrieval into [PillImageSpan].
|
||||
*/
|
||||
private class PillImageSpanTarget(pillImageSpan: PillImageSpan) : SimpleTarget<Drawable>() {
|
||||
|
||||
private val pillImageSpan = WeakReference(pillImageSpan)
|
||||
|
||||
override fun onResourceReady(drawable: Drawable, transition: Transition<in Drawable>?) {
|
||||
updateWith(drawable)
|
||||
}
|
||||
|
||||
override fun onLoadCleared(placeholder: Drawable?) {
|
||||
updateWith(placeholder)
|
||||
}
|
||||
|
||||
private fun updateWith(drawable: Drawable?) {
|
||||
pillImageSpan.get()?.apply {
|
||||
updateAvatarDrawable(drawable)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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.login
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import arrow.core.Try
|
||||
import com.jakewharton.rxbinding2.widget.RxTextView
|
||||
import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.platform.RiotActivity
|
||||
import im.vector.riotredesign.features.home.HomeActivity
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.functions.Function3
|
||||
import io.reactivex.rxkotlin.subscribeBy
|
||||
import kotlinx.android.synthetic.main.activity_login.*
|
||||
|
||||
private const val DEFAULT_HOME_SERVER_URI = "https://matrix.org"
|
||||
private const val DEFAULT_IDENTITY_SERVER_URI = "https://vector.im"
|
||||
private const val DEFAULT_ANTIVIRUS_SERVER_URI = "https://matrix.org"
|
||||
|
||||
class LoginActivity : RiotActivity() {
|
||||
|
||||
private val authenticator = Matrix.getInstance().authenticator()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_login)
|
||||
setupAuthButton()
|
||||
homeServerField.setText(DEFAULT_HOME_SERVER_URI)
|
||||
}
|
||||
|
||||
private fun authenticate() {
|
||||
val login = loginField.text?.trim().toString()
|
||||
val password = passwordField.text?.trim().toString()
|
||||
buildHomeServerConnectionConfig().fold(
|
||||
{ Toast.makeText(this@LoginActivity, "Authenticate failure: $it", Toast.LENGTH_LONG).show() },
|
||||
{ authenticateWith(it, login, password) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun authenticateWith(homeServerConnectionConfig: HomeServerConnectionConfig, login: String, password: String) {
|
||||
progressBar.visibility = View.VISIBLE
|
||||
authenticator.authenticate(homeServerConnectionConfig, login, password, object : MatrixCallback<Session> {
|
||||
override fun onSuccess(data: Session) {
|
||||
Matrix.getInstance().currentSession = data.apply { open() }
|
||||
goToHome()
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
progressBar.visibility = View.GONE
|
||||
Toast.makeText(this@LoginActivity, "Authenticate failure: $failure", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun buildHomeServerConnectionConfig(): Try<HomeServerConnectionConfig> {
|
||||
return Try {
|
||||
val homeServerUri = homeServerField.text?.trim().toString()
|
||||
HomeServerConnectionConfig.Builder()
|
||||
.withHomeServerUri(homeServerUri)
|
||||
.withIdentityServerUri(DEFAULT_IDENTITY_SERVER_URI)
|
||||
.withAntiVirusServerUri(DEFAULT_ANTIVIRUS_SERVER_URI)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupAuthButton() {
|
||||
Observable
|
||||
.combineLatest(
|
||||
RxTextView.textChanges(loginField).map { it.trim().isNotEmpty() },
|
||||
RxTextView.textChanges(passwordField).map { it.trim().isNotEmpty() },
|
||||
RxTextView.textChanges(homeServerField).map { it.trim().isNotEmpty() },
|
||||
Function3<Boolean, Boolean, Boolean, Boolean> { isLoginNotEmpty, isPasswordNotEmpty, isHomeServerNotEmpty ->
|
||||
isLoginNotEmpty && isPasswordNotEmpty && isHomeServerNotEmpty
|
||||
}
|
||||
)
|
||||
.subscribeBy { authenticateButton.isEnabled = it }
|
||||
.disposeOnDestroy()
|
||||
authenticateButton.setOnClickListener { authenticate() }
|
||||
}
|
||||
|
||||
private fun goToHome() {
|
||||
val intent = HomeActivity.newIntent(this)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newIntent(context: Context): Intent {
|
||||
return Intent(context, LoginActivity::class.java)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright 2019 New Vector Ltd
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.media
|
||||
|
||||
import com.github.piasy.biv.loader.ImageLoader
|
||||
import java.io.File
|
||||
|
||||
interface DefaultImageLoaderCallback : ImageLoader.Callback {
|
||||
|
||||
override fun onFinish() {
|
||||
//no-op
|
||||
}
|
||||
|
||||
override fun onSuccess(image: File?) {
|
||||
//no-op
|
||||
}
|
||||
|
||||
override fun onFail(error: Exception?) {
|
||||
//no-op
|
||||
}
|
||||
|
||||
override fun onCacheHit(imageType: Int, image: File?) {
|
||||
//no-op
|
||||
}
|
||||
|
||||
override fun onCacheMiss(imageType: Int, image: File?) {
|
||||
//no-op
|
||||
}
|
||||
|
||||
override fun onProgress(progress: Int) {
|
||||
//no-op
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
//no-op
|
||||
}
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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.media
|
||||
|
||||
import android.media.ExifInterface
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import android.widget.ImageView
|
||||
import com.github.piasy.biv.view.BigImageView
|
||||
import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
import im.vector.riotredesign.core.glide.GlideApp
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
object MediaContentRenderer {
|
||||
|
||||
@Parcelize
|
||||
data class Data(
|
||||
val filename: String,
|
||||
val url: String?,
|
||||
val height: Int?,
|
||||
val maxHeight: Int,
|
||||
val width: Int?,
|
||||
val maxWidth: Int,
|
||||
val orientation: Int?,
|
||||
val rotation: Int?
|
||||
) : Parcelable
|
||||
|
||||
enum class Mode {
|
||||
FULL_SIZE,
|
||||
THUMBNAIL
|
||||
}
|
||||
|
||||
fun render(data: Data, mode: Mode, imageView: ImageView) {
|
||||
val (width, height) = processSize(data, mode)
|
||||
imageView.layoutParams.height = height
|
||||
imageView.layoutParams.width = width
|
||||
val contentUrlResolver = Matrix.getInstance().currentSession!!.contentUrlResolver()
|
||||
val resolvedUrl = when (mode) {
|
||||
Mode.FULL_SIZE -> contentUrlResolver.resolveFullSize(data.url)
|
||||
Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||
} ?: return
|
||||
|
||||
GlideApp
|
||||
.with(imageView)
|
||||
.load(resolvedUrl)
|
||||
.thumbnail(0.3f)
|
||||
.into(imageView)
|
||||
}
|
||||
|
||||
fun render(data: Data, imageView: BigImageView) {
|
||||
val (width, height) = processSize(data, Mode.THUMBNAIL)
|
||||
val contentUrlResolver = Matrix.getInstance().currentSession!!.contentUrlResolver()
|
||||
val fullSize = contentUrlResolver.resolveFullSize(data.url)
|
||||
val thumbnail = contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||
imageView.showImage(
|
||||
Uri.parse(thumbnail),
|
||||
Uri.parse(fullSize)
|
||||
)
|
||||
}
|
||||
|
||||
private fun processSize(data: Data, mode: Mode): Pair<Int, Int> {
|
||||
val maxImageWidth = data.maxWidth
|
||||
val maxImageHeight = data.maxHeight
|
||||
val rotationAngle = data.rotation ?: 0
|
||||
val orientation = data.orientation ?: ExifInterface.ORIENTATION_NORMAL
|
||||
var width = data.width ?: maxImageWidth
|
||||
var height = data.height ?: maxImageHeight
|
||||
var finalHeight = -1
|
||||
var finalWidth = -1
|
||||
|
||||
// if the image size is known
|
||||
// compute the expected height
|
||||
if (width > 0 && height > 0) {
|
||||
// swap width and height if the image is side oriented
|
||||
if (rotationAngle == 90 || rotationAngle == 270) {
|
||||
val tmp = width
|
||||
width = height
|
||||
height = tmp
|
||||
} else if (orientation == ExifInterface.ORIENTATION_ROTATE_90 || orientation == ExifInterface.ORIENTATION_ROTATE_270) {
|
||||
val tmp = width
|
||||
width = height
|
||||
height = tmp
|
||||
}
|
||||
if (mode == Mode.FULL_SIZE) {
|
||||
finalHeight = height
|
||||
finalWidth = width
|
||||
} else {
|
||||
finalHeight = Math.min(maxImageWidth * height / width, maxImageHeight)
|
||||
finalWidth = finalHeight * width / height
|
||||
}
|
||||
}
|
||||
// ensure that some values are properly initialized
|
||||
if (finalHeight < 0) {
|
||||
finalHeight = maxImageHeight
|
||||
}
|
||||
if (finalWidth < 0) {
|
||||
finalWidth = maxImageWidth
|
||||
}
|
||||
return Pair(finalWidth, finalHeight)
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
/*
|
||||
*
|
||||
* * 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.media
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import com.github.piasy.biv.indicator.progresspie.ProgressPieIndicator
|
||||
import com.github.piasy.biv.view.GlideImageViewFactory
|
||||
import im.vector.riotredesign.core.platform.RiotActivity
|
||||
import kotlinx.android.synthetic.main.activity_media_viewer.*
|
||||
|
||||
|
||||
class MediaViewerActivity : RiotActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(im.vector.riotredesign.R.layout.activity_media_viewer)
|
||||
val mediaData = intent.getParcelableExtra<MediaContentRenderer.Data>(EXTRA_MEDIA_DATA)
|
||||
if (mediaData.url.isNullOrEmpty()) {
|
||||
finish()
|
||||
} else {
|
||||
configureToolbar(mediaViewerToolbar, mediaData)
|
||||
mediaViewerImageView.setImageViewFactory(GlideImageViewFactory())
|
||||
mediaViewerImageView.setProgressIndicator(ProgressPieIndicator())
|
||||
MediaContentRenderer.render(mediaData, mediaViewerImageView)
|
||||
}
|
||||
}
|
||||
|
||||
private fun configureToolbar(toolbar: Toolbar, mediaData: MediaContentRenderer.Data) {
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.apply {
|
||||
title = mediaData.filename
|
||||
setHomeButtonEnabled(true)
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val EXTRA_MEDIA_DATA = "EXTRA_MEDIA_DATA"
|
||||
|
||||
fun newIntent(context: Context, mediaData: MediaContentRenderer.Data): Intent {
|
||||
return Intent(context, MediaViewerActivity::class.java).apply {
|
||||
putExtra(EXTRA_MEDIA_DATA, mediaData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Copyright 2018 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.rageshake
|
||||
|
||||
import android.text.TextUtils
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.*
|
||||
import androidx.core.view.isVisible
|
||||
import butterknife.BindView
|
||||
import butterknife.OnCheckedChanged
|
||||
import butterknife.OnTextChanged
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.platform.RiotActivity
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Form to send a bug report
|
||||
*/
|
||||
class BugReportActivity : RiotActivity() {
|
||||
|
||||
/* ==========================================================================================
|
||||
* UI
|
||||
* ========================================================================================== */
|
||||
|
||||
@BindView(R.id.bug_report_edit_text)
|
||||
lateinit var mBugReportText: EditText
|
||||
|
||||
@BindView(R.id.bug_report_button_include_logs)
|
||||
lateinit var mIncludeLogsButton: CheckBox
|
||||
|
||||
@BindView(R.id.bug_report_button_include_crash_logs)
|
||||
lateinit var mIncludeCrashLogsButton: CheckBox
|
||||
|
||||
@BindView(R.id.bug_report_button_include_screenshot)
|
||||
lateinit var mIncludeScreenShotButton: CheckBox
|
||||
|
||||
@BindView(R.id.bug_report_screenshot_preview)
|
||||
lateinit var mScreenShotPreview: ImageView
|
||||
|
||||
@BindView(R.id.bug_report_progress_view)
|
||||
lateinit var mProgressBar: ProgressBar
|
||||
|
||||
@BindView(R.id.bug_report_progress_text_view)
|
||||
lateinit var mProgressTextView: TextView
|
||||
|
||||
@BindView(R.id.bug_report_scrollview)
|
||||
lateinit var mScrollView: View
|
||||
|
||||
@BindView(R.id.bug_report_mask_view)
|
||||
lateinit var mMaskView: View
|
||||
|
||||
override fun getLayoutRes() = R.layout.activity_bug_report
|
||||
|
||||
override fun initUiAndData() {
|
||||
configureToolbar()
|
||||
|
||||
if (BugReporter.screenshot != null) {
|
||||
mScreenShotPreview.setImageBitmap(BugReporter.screenshot)
|
||||
} else {
|
||||
mScreenShotPreview.isVisible = false
|
||||
mIncludeScreenShotButton.isChecked = false
|
||||
mIncludeScreenShotButton.isEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMenuRes() = R.menu.bug_report
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||
menu.findItem(R.id.ic_action_send_bug_report)?.let {
|
||||
val isValid = mBugReportText.text.toString().trim().length > 10
|
||||
&& !mMaskView.isVisible
|
||||
|
||||
it.isEnabled = isValid
|
||||
it.icon.alpha = if (isValid) 255 else 100
|
||||
}
|
||||
|
||||
return super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.ic_action_send_bug_report -> {
|
||||
sendBugReport()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send the bug report
|
||||
*/
|
||||
private fun sendBugReport() {
|
||||
mScrollView.alpha = 0.3f
|
||||
mMaskView.isVisible = true
|
||||
|
||||
invalidateOptionsMenu()
|
||||
|
||||
mProgressTextView.isVisible = true
|
||||
mProgressTextView.text = getString(R.string.send_bug_report_progress, 0.toString() + "")
|
||||
|
||||
mProgressBar.isVisible = true
|
||||
mProgressBar.progress = 0
|
||||
|
||||
BugReporter.sendBugReport(this,
|
||||
mIncludeLogsButton.isChecked,
|
||||
mIncludeCrashLogsButton.isChecked,
|
||||
mIncludeScreenShotButton.isChecked,
|
||||
mBugReportText.text.toString(),
|
||||
object : BugReporter.IMXBugReportListener {
|
||||
override fun onUploadFailed(reason: String?) {
|
||||
try {
|
||||
if (!TextUtils.isEmpty(reason)) {
|
||||
Toast.makeText(this@BugReportActivity,
|
||||
getString(R.string.send_bug_report_failed, reason), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## onUploadFailed() : failed to display the toast " + e.message)
|
||||
}
|
||||
|
||||
mMaskView.isVisible = false
|
||||
mProgressBar.isVisible = false
|
||||
mProgressTextView.isVisible = false
|
||||
mScrollView.alpha = 1.0f
|
||||
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
override fun onUploadCancelled() {
|
||||
onUploadFailed(null)
|
||||
}
|
||||
|
||||
override fun onProgress(progress: Int) {
|
||||
var progress = progress
|
||||
if (progress > 100) {
|
||||
Timber.e("## onProgress() : progress > 100")
|
||||
progress = 100
|
||||
} else if (progress < 0) {
|
||||
Timber.e("## onProgress() : progress < 0")
|
||||
progress = 0
|
||||
}
|
||||
|
||||
mProgressBar.progress = progress
|
||||
mProgressTextView.text = getString(R.string.send_bug_report_progress, progress.toString() + "")
|
||||
}
|
||||
|
||||
override fun onUploadSucceed() {
|
||||
try {
|
||||
Toast.makeText(this@BugReportActivity, R.string.send_bug_report_sent, Toast.LENGTH_LONG).show()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## onUploadSucceed() : failed to dismiss the toast " + e.message)
|
||||
}
|
||||
|
||||
try {
|
||||
finish()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## onUploadSucceed() : failed to dismiss the dialog " + e.message)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* UI Event
|
||||
* ========================================================================================== */
|
||||
|
||||
@OnTextChanged(R.id.bug_report_edit_text)
|
||||
internal fun textChanged() {
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
@OnCheckedChanged(R.id.bug_report_button_include_screenshot)
|
||||
internal fun onSendScreenshotChanged() {
|
||||
mScreenShotPreview.isVisible = mIncludeScreenShotButton.isChecked && BugReporter.screenshot != null
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
// Ensure there is no crash status remaining, which will be sent later on by mistake
|
||||
BugReporter.deleteCrashFile(this)
|
||||
|
||||
super.onBackPressed()
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* Companion
|
||||
* ========================================================================================== */
|
||||
|
||||
companion object {
|
||||
private val LOG_TAG = BugReportActivity::class.java.simpleName
|
||||
}
|
||||
}
|
687
vector/src/main/java/im/vector/riotredesign/features/rageshake/BugReporter.kt
Executable file
687
vector/src/main/java/im/vector/riotredesign/features/rageshake/BugReporter.kt
Executable file
@ -0,0 +1,687 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
* Copyright 2018 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.rageshake
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.os.AsyncTask
|
||||
import android.os.Build
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.riotredesign.BuildConfig
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.extensions.toOnOff
|
||||
import im.vector.riotredesign.core.utils.getDeviceLocale
|
||||
import im.vector.riotredesign.features.settings.VectorLocale
|
||||
import im.vector.riotredesign.features.themes.ThemeUtils
|
||||
import okhttp3.*
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import timber.log.Timber
|
||||
import java.io.*
|
||||
import java.net.HttpURLConnection
|
||||
import java.util.*
|
||||
import java.util.zip.GZIPOutputStream
|
||||
|
||||
/**
|
||||
* BugReporter creates and sends the bug reports.
|
||||
*/
|
||||
object BugReporter {
|
||||
var inMultiWindowMode = false
|
||||
|
||||
// filenames
|
||||
private const val LOG_CAT_ERROR_FILENAME = "logcatError.log"
|
||||
private const val LOG_CAT_FILENAME = "logcat.log"
|
||||
private const val LOG_CAT_SCREENSHOT_FILENAME = "screenshot.png"
|
||||
private const val CRASH_FILENAME = "crash.log"
|
||||
|
||||
|
||||
// the http client
|
||||
private val mOkHttpClient = OkHttpClient()
|
||||
|
||||
// the pending bug report call
|
||||
private var mBugReportCall: Call? = null
|
||||
|
||||
|
||||
// boolean to cancel the bug report
|
||||
private val mIsCancelled = false
|
||||
|
||||
/**
|
||||
* Get current Screenshot
|
||||
*
|
||||
* @return screenshot or null if not available
|
||||
*/
|
||||
var screenshot: Bitmap? = null
|
||||
private set
|
||||
|
||||
private const val BUFFER_SIZE = 1024 * 1024 * 50
|
||||
|
||||
private val LOGCAT_CMD_ERROR = arrayOf("logcat", ///< Run 'logcat' command
|
||||
"-d", ///< Dump the log rather than continue outputting it
|
||||
"-v", // formatting
|
||||
"threadtime", // include timestamps
|
||||
"AndroidRuntime:E " + ///< Pick all AndroidRuntime errors (such as uncaught exceptions)"communicatorjni:V " + ///< All communicatorjni logging
|
||||
"libcommunicator:V " + ///< All libcommunicator logging
|
||||
"DEBUG:V " + ///< All DEBUG logging - which includes native land crashes (seg faults, etc)
|
||||
"*:S" ///< Everything else silent, so don't pick it..
|
||||
)
|
||||
|
||||
private val LOGCAT_CMD_DEBUG = arrayOf("logcat", "-d", "-v", "threadtime", "*:*")
|
||||
|
||||
/**
|
||||
* Bug report upload listener
|
||||
*/
|
||||
interface IMXBugReportListener {
|
||||
/**
|
||||
* The bug report has been cancelled
|
||||
*/
|
||||
fun onUploadCancelled()
|
||||
|
||||
/**
|
||||
* The bug report upload failed.
|
||||
*
|
||||
* @param reason the failure reason
|
||||
*/
|
||||
fun onUploadFailed(reason: String?)
|
||||
|
||||
/**
|
||||
* The upload progress (in percent)
|
||||
*
|
||||
* @param progress the upload progress
|
||||
*/
|
||||
fun onProgress(progress: Int)
|
||||
|
||||
/**
|
||||
* The bug report upload succeeded.
|
||||
*/
|
||||
fun onUploadSucceed()
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a bug report.
|
||||
*
|
||||
* @param context the application context
|
||||
* @param withDevicesLogs true to include the device log
|
||||
* @param withCrashLogs true to include the crash logs
|
||||
* @param withScreenshot true to include the screenshot
|
||||
* @param theBugDescription the bug description
|
||||
* @param listener the listener
|
||||
*/
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
fun sendBugReport(context: Context,
|
||||
withDevicesLogs: Boolean,
|
||||
withCrashLogs: Boolean,
|
||||
withScreenshot: Boolean,
|
||||
theBugDescription: String,
|
||||
listener: IMXBugReportListener?) {
|
||||
object : AsyncTask<Void, Int, String>() {
|
||||
|
||||
// enumerate files to delete
|
||||
val mBugReportFiles: MutableList<File> = ArrayList()
|
||||
|
||||
override fun doInBackground(vararg voids: Void?): String? {
|
||||
var bugDescription = theBugDescription
|
||||
var serverError: String? = null
|
||||
val crashCallStack = getCrashDescription(context)
|
||||
|
||||
if (null != crashCallStack) {
|
||||
bugDescription += "\n\n\n\n--------------------------------- crash call stack ---------------------------------\n"
|
||||
bugDescription += crashCallStack
|
||||
}
|
||||
|
||||
val gzippedFiles = ArrayList<File>()
|
||||
|
||||
if (withDevicesLogs) {
|
||||
val files = VectorFileLogger.getLogFiles()
|
||||
|
||||
for (f in files) {
|
||||
if (!mIsCancelled) {
|
||||
val gzippedFile = compressFile(f)
|
||||
|
||||
if (null != gzippedFile) {
|
||||
gzippedFiles.add(gzippedFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!mIsCancelled && (withCrashLogs || withDevicesLogs)) {
|
||||
val gzippedLogcat = saveLogCat(context, false)
|
||||
|
||||
if (null != gzippedLogcat) {
|
||||
if (gzippedFiles.size == 0) {
|
||||
gzippedFiles.add(gzippedLogcat)
|
||||
} else {
|
||||
gzippedFiles.add(0, gzippedLogcat)
|
||||
}
|
||||
}
|
||||
|
||||
val crashDescription = getCrashFile(context)
|
||||
if (crashDescription.exists()) {
|
||||
val compressedCrashDescription = compressFile(crashDescription)
|
||||
|
||||
if (null != compressedCrashDescription) {
|
||||
if (gzippedFiles.size == 0) {
|
||||
gzippedFiles.add(compressedCrashDescription)
|
||||
} else {
|
||||
gzippedFiles.add(0, compressedCrashDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var deviceId = "undefined"
|
||||
var userId = "undefined"
|
||||
var matrixSdkVersion = "undefined"
|
||||
var olmVersion = "undefined"
|
||||
|
||||
Matrix.getInstance().currentSession?.let { session ->
|
||||
userId = session.sessionParams.credentials.userId
|
||||
deviceId = session.sessionParams.credentials.deviceId ?: "undefined"
|
||||
// TODO matrixSdkVersion = session.getVersion(true);
|
||||
// TODO olmVersion = session.getCryptoVersion(context, true);
|
||||
}
|
||||
|
||||
if (!mIsCancelled) {
|
||||
// build the multi part request
|
||||
val builder = BugReporterMultipartBody.Builder()
|
||||
.addFormDataPart("text", "[RiotX] $bugDescription")
|
||||
.addFormDataPart("app", "riot-android")
|
||||
.addFormDataPart("user_agent", Matrix.getInstance().getUserAgent())
|
||||
.addFormDataPart("user_id", userId)
|
||||
.addFormDataPart("device_id", deviceId)
|
||||
// TODO .addFormDataPart("version", Matrix.getInstance(context).getVersion(true, false))
|
||||
.addFormDataPart("branch_name", context.getString(R.string.git_branch_name))
|
||||
.addFormDataPart("matrix_sdk_version", matrixSdkVersion)
|
||||
.addFormDataPart("olm_version", olmVersion)
|
||||
.addFormDataPart("device", Build.MODEL.trim())
|
||||
.addFormDataPart("lazy_loading", true.toOnOff())
|
||||
.addFormDataPart("multi_window", inMultiWindowMode.toOnOff())
|
||||
.addFormDataPart("os", Build.VERSION.RELEASE + " (API " + Build.VERSION.SDK_INT + ") "
|
||||
+ Build.VERSION.INCREMENTAL + "-" + Build.VERSION.CODENAME)
|
||||
.addFormDataPart("locale", Locale.getDefault().toString())
|
||||
.addFormDataPart("app_language", VectorLocale.applicationLocale.toString())
|
||||
.addFormDataPart("default_app_language", getDeviceLocale(context).toString())
|
||||
.addFormDataPart("theme", ThemeUtils.getApplicationTheme(context))
|
||||
|
||||
val buildNumber = context.getString(R.string.build_number)
|
||||
if (!TextUtils.isEmpty(buildNumber) && buildNumber != "0") {
|
||||
builder.addFormDataPart("build_number", buildNumber)
|
||||
}
|
||||
|
||||
// add the gzipped files
|
||||
for (file in gzippedFiles) {
|
||||
builder.addFormDataPart("compressed-log", file.name, RequestBody.create(MediaType.parse("application/octet-stream"), file))
|
||||
}
|
||||
|
||||
mBugReportFiles.addAll(gzippedFiles)
|
||||
|
||||
if (withScreenshot) {
|
||||
val bitmap = screenshot
|
||||
|
||||
if (null != bitmap) {
|
||||
val logCatScreenshotFile = File(context.cacheDir.absolutePath, LOG_CAT_SCREENSHOT_FILENAME)
|
||||
|
||||
if (logCatScreenshotFile.exists()) {
|
||||
logCatScreenshotFile.delete()
|
||||
}
|
||||
|
||||
try {
|
||||
val fos = FileOutputStream(logCatScreenshotFile)
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)
|
||||
fos.flush()
|
||||
fos.close()
|
||||
|
||||
builder.addFormDataPart("file",
|
||||
logCatScreenshotFile.name, RequestBody.create(MediaType.parse("application/octet-stream"), logCatScreenshotFile))
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## sendBugReport() : fail to write screenshot$e")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
screenshot = null
|
||||
|
||||
// add some github labels
|
||||
builder.addFormDataPart("label", BuildConfig.VERSION_NAME)
|
||||
builder.addFormDataPart("label", BuildConfig.FLAVOR_DESCRIPTION)
|
||||
builder.addFormDataPart("label", context.getString(R.string.git_branch_name))
|
||||
|
||||
// Special for RiotX
|
||||
builder.addFormDataPart("label", "[RiotX]")
|
||||
|
||||
if (getCrashFile(context).exists()) {
|
||||
builder.addFormDataPart("label", "crash")
|
||||
deleteCrashFile(context)
|
||||
}
|
||||
|
||||
val requestBody = builder.build()
|
||||
|
||||
// add a progress listener
|
||||
requestBody.setWriteListener { totalWritten, contentLength ->
|
||||
val percentage: Int
|
||||
|
||||
if (-1L != contentLength) {
|
||||
if (totalWritten > contentLength) {
|
||||
percentage = 100
|
||||
} else {
|
||||
percentage = (totalWritten * 100 / contentLength).toInt()
|
||||
}
|
||||
} else {
|
||||
percentage = 0
|
||||
}
|
||||
|
||||
if (mIsCancelled && null != mBugReportCall) {
|
||||
mBugReportCall!!.cancel()
|
||||
}
|
||||
|
||||
Timber.d("## onWrite() : $percentage%")
|
||||
publishProgress(percentage)
|
||||
}
|
||||
|
||||
// build the request
|
||||
val request = Request.Builder()
|
||||
.url(context.getString(R.string.bug_report_url))
|
||||
.post(requestBody)
|
||||
.build()
|
||||
|
||||
var responseCode = HttpURLConnection.HTTP_INTERNAL_ERROR
|
||||
var response: Response? = null
|
||||
var errorMessage: String? = null
|
||||
|
||||
// trigger the request
|
||||
try {
|
||||
mBugReportCall = mOkHttpClient.newCall(request)
|
||||
response = mBugReportCall!!.execute()
|
||||
responseCode = response!!.code()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "response " + e.message)
|
||||
errorMessage = e.localizedMessage
|
||||
}
|
||||
|
||||
// if the upload failed, try to retrieve the reason
|
||||
if (responseCode != HttpURLConnection.HTTP_OK) {
|
||||
if (null != errorMessage) {
|
||||
serverError = "Failed with error $errorMessage"
|
||||
} else if (null == response || null == response.body()) {
|
||||
serverError = "Failed with error $responseCode"
|
||||
} else {
|
||||
var `is`: InputStream? = null
|
||||
|
||||
try {
|
||||
`is` = response.body()!!.byteStream()
|
||||
|
||||
if (null != `is`) {
|
||||
var ch = `is`.read()
|
||||
val b = StringBuilder()
|
||||
while (ch != -1) {
|
||||
b.append(ch.toChar())
|
||||
ch = `is`.read()
|
||||
}
|
||||
serverError = b.toString()
|
||||
`is`.close()
|
||||
|
||||
// check if the error message
|
||||
try {
|
||||
val responseJSON = JSONObject(serverError)
|
||||
serverError = responseJSON.getString("error")
|
||||
} catch (e: JSONException) {
|
||||
Timber.e(e, "doInBackground ; Json conversion failed " + e.message)
|
||||
}
|
||||
|
||||
// should never happen
|
||||
if (null == serverError) {
|
||||
serverError = "Failed with error $responseCode"
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## sendBugReport() : failed to parse error " + e.message)
|
||||
} finally {
|
||||
try {
|
||||
`is`?.close()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## sendBugReport() : failed to close the error stream " + e.message)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return serverError
|
||||
}
|
||||
|
||||
|
||||
override fun onProgressUpdate(vararg progress: Int?) {
|
||||
if (null != listener) {
|
||||
try {
|
||||
listener.onProgress(progress?.get(0) ?: 0)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## onProgress() : failed " + e.message)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPostExecute(reason: String?) {
|
||||
mBugReportCall = null
|
||||
|
||||
// delete when the bug report has been successfully sent
|
||||
for (file in mBugReportFiles) {
|
||||
file.delete()
|
||||
}
|
||||
|
||||
if (null != listener) {
|
||||
try {
|
||||
if (mIsCancelled) {
|
||||
listener.onUploadCancelled()
|
||||
} else if (null == reason) {
|
||||
listener.onUploadSucceed()
|
||||
} else {
|
||||
listener.onUploadFailed(reason)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## onPostExecute() : failed " + e.message)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}.execute()
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a bug report either with email or with Vector.
|
||||
*/
|
||||
fun openBugReportScreen(activity: Activity) {
|
||||
screenshot = takeScreenshot(activity)
|
||||
|
||||
val intent = Intent(activity, BugReportActivity::class.java)
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// crash report management
|
||||
//==============================================================================================================
|
||||
|
||||
/**
|
||||
* Provides the crash file
|
||||
*
|
||||
* @param context the context
|
||||
* @return the crash file
|
||||
*/
|
||||
private fun getCrashFile(context: Context): File {
|
||||
return File(context.cacheDir.absolutePath, CRASH_FILENAME)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the crash file
|
||||
*
|
||||
* @param context
|
||||
*/
|
||||
fun deleteCrashFile(context: Context) {
|
||||
val crashFile = getCrashFile(context)
|
||||
|
||||
if (crashFile.exists()) {
|
||||
crashFile.delete()
|
||||
}
|
||||
|
||||
// Also reset the screenshot
|
||||
screenshot = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the crash report
|
||||
*
|
||||
* @param context the context
|
||||
* @param crashDescription teh crash description
|
||||
*/
|
||||
fun saveCrashReport(context: Context, crashDescription: String) {
|
||||
val crashFile = getCrashFile(context)
|
||||
|
||||
if (crashFile.exists()) {
|
||||
crashFile.delete()
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(crashDescription)) {
|
||||
try {
|
||||
val fos = FileOutputStream(crashFile)
|
||||
val osw = OutputStreamWriter(fos)
|
||||
osw.write(crashDescription)
|
||||
osw.close()
|
||||
|
||||
fos.flush()
|
||||
fos.close()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## saveCrashReport() : fail to write $e")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the crash description file and return its content.
|
||||
*
|
||||
* @param context teh context
|
||||
* @return the crash description
|
||||
*/
|
||||
private fun getCrashDescription(context: Context): String? {
|
||||
var crashDescription: String? = null
|
||||
val crashFile = getCrashFile(context)
|
||||
|
||||
if (crashFile.exists()) {
|
||||
try {
|
||||
val fis = FileInputStream(crashFile)
|
||||
val isr = InputStreamReader(fis)
|
||||
|
||||
val buffer = CharArray(fis.available())
|
||||
val len = isr.read(buffer, 0, fis.available())
|
||||
crashDescription = String(buffer, 0, len)
|
||||
isr.close()
|
||||
fis.close()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## getCrashDescription() : fail to read $e")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return crashDescription
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// Screenshot management
|
||||
//==============================================================================================================
|
||||
|
||||
/**
|
||||
* Take a screenshot of the display.
|
||||
*
|
||||
* @return the screenshot
|
||||
*/
|
||||
private fun takeScreenshot(activity: Activity): Bitmap? {
|
||||
// get content view
|
||||
val contentView = activity.findViewById<View>(android.R.id.content)
|
||||
if (contentView == null) {
|
||||
Timber.e("Cannot find content view on $activity. Cannot take screenshot.")
|
||||
return null
|
||||
}
|
||||
|
||||
// get the root view to snapshot
|
||||
val rootView = contentView.rootView
|
||||
if (rootView == null) {
|
||||
Timber.e("Cannot find root view on $activity. Cannot take screenshot.")
|
||||
return null
|
||||
}
|
||||
// refresh it
|
||||
rootView.isDrawingCacheEnabled = false
|
||||
rootView.isDrawingCacheEnabled = true
|
||||
|
||||
try {
|
||||
var bitmap = rootView.drawingCache
|
||||
|
||||
// Make a copy, because if Activity is destroyed, the bitmap will be recycled
|
||||
bitmap = Bitmap.createBitmap(bitmap)
|
||||
|
||||
return bitmap
|
||||
} catch (oom: OutOfMemoryError) {
|
||||
Timber.e(oom, "Cannot get drawing cache for $activity OOM.")
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Cannot get snapshot of screen: $e")
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// Logcat management
|
||||
//==============================================================================================================
|
||||
|
||||
/**
|
||||
* Save the logcat
|
||||
*
|
||||
* @param context the context
|
||||
* @param isErrorLogcat true to save the error logcat
|
||||
* @return the file if the operation succeeds
|
||||
*/
|
||||
private fun saveLogCat(context: Context, isErrorLogcat: Boolean): File? {
|
||||
val logCatErrFile = File(context.cacheDir.absolutePath, if (isErrorLogcat) LOG_CAT_ERROR_FILENAME else LOG_CAT_FILENAME)
|
||||
|
||||
if (logCatErrFile.exists()) {
|
||||
logCatErrFile.delete()
|
||||
}
|
||||
|
||||
try {
|
||||
val fos = FileOutputStream(logCatErrFile)
|
||||
val osw = OutputStreamWriter(fos)
|
||||
getLogCatError(osw, isErrorLogcat)
|
||||
osw.close()
|
||||
|
||||
fos.flush()
|
||||
fos.close()
|
||||
|
||||
return compressFile(logCatErrFile)
|
||||
} catch (error: OutOfMemoryError) {
|
||||
Timber.e(error, "## saveLogCat() : fail to write logcat$error")
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## saveLogCat() : fail to write logcat$e")
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the logs
|
||||
*
|
||||
* @param streamWriter the stream writer
|
||||
* @param isErrorLogCat true to save the error logs
|
||||
*/
|
||||
private fun getLogCatError(streamWriter: OutputStreamWriter, isErrorLogCat: Boolean) {
|
||||
val logcatProc: Process
|
||||
|
||||
try {
|
||||
logcatProc = Runtime.getRuntime().exec(if (isErrorLogCat) LOGCAT_CMD_ERROR else LOGCAT_CMD_DEBUG)
|
||||
} catch (e1: IOException) {
|
||||
return
|
||||
}
|
||||
|
||||
var reader: BufferedReader? = null
|
||||
try {
|
||||
val separator = System.getProperty("line.separator")
|
||||
reader = BufferedReader(InputStreamReader(logcatProc.inputStream), BUFFER_SIZE)
|
||||
var line = reader.readLine()
|
||||
while (line != null) {
|
||||
streamWriter.append(line)
|
||||
streamWriter.append(separator)
|
||||
line = reader.readLine()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e, "getLog fails with " + e.localizedMessage)
|
||||
} finally {
|
||||
if (reader != null) {
|
||||
try {
|
||||
reader.close()
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e, "getLog fails with " + e.localizedMessage)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// File compression management
|
||||
//==============================================================================================================
|
||||
|
||||
/**
|
||||
* GZip a file
|
||||
*
|
||||
* @param fin the input file
|
||||
* @return the gzipped file
|
||||
*/
|
||||
private fun compressFile(fin: File): File? {
|
||||
Timber.d("## compressFile() : compress " + fin.name)
|
||||
|
||||
val dstFile = File(fin.parent, fin.name + ".gz")
|
||||
|
||||
if (dstFile.exists()) {
|
||||
dstFile.delete()
|
||||
}
|
||||
|
||||
var fos: FileOutputStream? = null
|
||||
var gos: GZIPOutputStream? = null
|
||||
var inputStream: InputStream? = null
|
||||
try {
|
||||
fos = FileOutputStream(dstFile)
|
||||
gos = GZIPOutputStream(fos)
|
||||
|
||||
inputStream = FileInputStream(fin)
|
||||
|
||||
val buffer = ByteArray(2048)
|
||||
var n = inputStream.read(buffer)
|
||||
while (n != -1) {
|
||||
gos.write(buffer, 0, n)
|
||||
n = inputStream.read(buffer)
|
||||
}
|
||||
|
||||
gos.close()
|
||||
inputStream.close()
|
||||
|
||||
Timber.d("## compressFile() : " + fin.length() + " compressed to " + dstFile.length() + " bytes")
|
||||
return dstFile
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## compressFile() failed " + e.message)
|
||||
} catch (oom: OutOfMemoryError) {
|
||||
Timber.e(oom, "## compressFile() failed " + oom.message)
|
||||
} finally {
|
||||
try {
|
||||
fos?.close()
|
||||
gos?.close()
|
||||
inputStream?.close()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## compressFile() failed to close inputStream " + e.message)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
@ -0,0 +1,300 @@
|
||||
/*
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
* Copyright 2018 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.rageshake;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import okhttp3.Headers;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.internal.Util;
|
||||
import okio.Buffer;
|
||||
import okio.BufferedSink;
|
||||
import okio.ByteString;
|
||||
|
||||
// simplified version of MultipartBody (OkHttp 3.6.0)
|
||||
public class BugReporterMultipartBody extends RequestBody {
|
||||
|
||||
/**
|
||||
* Listener
|
||||
*/
|
||||
public interface WriteListener {
|
||||
/**
|
||||
* Upload listener
|
||||
*
|
||||
* @param totalWritten total written bytes
|
||||
* @param contentLength content length
|
||||
*/
|
||||
void onWrite(long totalWritten, long contentLength);
|
||||
}
|
||||
|
||||
private static final MediaType FORM = MediaType.parse("multipart/form-data");
|
||||
|
||||
private static final byte[] COLONSPACE = {':', ' '};
|
||||
private static final byte[] CRLF = {'\r', '\n'};
|
||||
private static final byte[] DASHDASH = {'-', '-'};
|
||||
|
||||
private final ByteString mBoundary;
|
||||
private final MediaType mContentType;
|
||||
private final List<Part> mParts;
|
||||
private long mContentLength = -1L;
|
||||
|
||||
// listener
|
||||
private WriteListener mWriteListener;
|
||||
|
||||
//
|
||||
private List<Long> mContentLengthSize = null;
|
||||
|
||||
private BugReporterMultipartBody(ByteString boundary, List<Part> parts) {
|
||||
mBoundary = boundary;
|
||||
mContentType = MediaType.parse(FORM + "; boundary=" + boundary.utf8());
|
||||
mParts = Util.immutableList(parts);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaType contentType() {
|
||||
return mContentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() throws IOException {
|
||||
long result = mContentLength;
|
||||
if (result != -1L) return result;
|
||||
return mContentLength = writeOrCountBytes(null, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(BufferedSink sink) throws IOException {
|
||||
writeOrCountBytes(sink, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the listener
|
||||
*
|
||||
* @param listener the
|
||||
*/
|
||||
public void setWriteListener(WriteListener listener) {
|
||||
mWriteListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Warn the listener that some bytes have been written
|
||||
*
|
||||
* @param totalWrittenBytes the total written bytes
|
||||
*/
|
||||
private void onWrite(long totalWrittenBytes) {
|
||||
if ((null != mWriteListener) && (mContentLength > 0)) {
|
||||
mWriteListener.onWrite(totalWrittenBytes, mContentLength);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Either writes this request to {@code sink} or measures its content length. We have one method
|
||||
* do double-duty to make sure the counting and content are consistent, particularly when it comes
|
||||
* to awkward operations like measuring the encoded length of header strings, or the
|
||||
* length-in-digits of an encoded integer.
|
||||
*/
|
||||
private long writeOrCountBytes(BufferedSink sink, boolean countBytes) throws IOException {
|
||||
long byteCount = 0L;
|
||||
|
||||
Buffer byteCountBuffer = null;
|
||||
if (countBytes) {
|
||||
sink = byteCountBuffer = new Buffer();
|
||||
mContentLengthSize = new ArrayList<>();
|
||||
}
|
||||
|
||||
for (int p = 0, partCount = mParts.size(); p < partCount; p++) {
|
||||
Part part = mParts.get(p);
|
||||
Headers headers = part.headers;
|
||||
RequestBody body = part.body;
|
||||
|
||||
sink.write(DASHDASH);
|
||||
sink.write(mBoundary);
|
||||
sink.write(CRLF);
|
||||
|
||||
if (headers != null) {
|
||||
for (int h = 0, headerCount = headers.size(); h < headerCount; h++) {
|
||||
sink.writeUtf8(headers.name(h))
|
||||
.write(COLONSPACE)
|
||||
.writeUtf8(headers.value(h))
|
||||
.write(CRLF);
|
||||
}
|
||||
}
|
||||
|
||||
MediaType contentType = body.contentType();
|
||||
if (contentType != null) {
|
||||
sink.writeUtf8("Content-Type: ")
|
||||
.writeUtf8(contentType.toString())
|
||||
.write(CRLF);
|
||||
}
|
||||
|
||||
int contentLength = (int) body.contentLength();
|
||||
if (contentLength != -1) {
|
||||
sink.writeUtf8("Content-Length: ")
|
||||
.writeUtf8(contentLength + "")
|
||||
.write(CRLF);
|
||||
} else if (countBytes) {
|
||||
// We can't measure the body's size without the sizes of its components.
|
||||
byteCountBuffer.clear();
|
||||
return -1L;
|
||||
}
|
||||
|
||||
sink.write(CRLF);
|
||||
|
||||
if (countBytes) {
|
||||
byteCount += contentLength;
|
||||
mContentLengthSize.add(byteCount);
|
||||
} else {
|
||||
body.writeTo(sink);
|
||||
|
||||
// warn the listener of upload progress
|
||||
// sink.buffer().size() does not give the right value
|
||||
// assume that some data are popped
|
||||
if ((null != mContentLengthSize) && (p < mContentLengthSize.size())) {
|
||||
onWrite(mContentLengthSize.get(p));
|
||||
}
|
||||
}
|
||||
sink.write(CRLF);
|
||||
}
|
||||
|
||||
sink.write(DASHDASH);
|
||||
sink.write(mBoundary);
|
||||
sink.write(DASHDASH);
|
||||
sink.write(CRLF);
|
||||
|
||||
if (countBytes) {
|
||||
byteCount += byteCountBuffer.size();
|
||||
byteCountBuffer.clear();
|
||||
}
|
||||
|
||||
return byteCount;
|
||||
}
|
||||
|
||||
private static void appendQuotedString(StringBuilder target, String key) {
|
||||
target.append('"');
|
||||
for (int i = 0, len = key.length(); i < len; i++) {
|
||||
char ch = key.charAt(i);
|
||||
switch (ch) {
|
||||
case '\n':
|
||||
target.append("%0A");
|
||||
break;
|
||||
case '\r':
|
||||
target.append("%0D");
|
||||
break;
|
||||
case '"':
|
||||
target.append("%22");
|
||||
break;
|
||||
default:
|
||||
target.append(ch);
|
||||
break;
|
||||
}
|
||||
}
|
||||
target.append('"');
|
||||
}
|
||||
|
||||
public static final class Part {
|
||||
public static Part create(Headers headers, RequestBody body) {
|
||||
if (body == null) {
|
||||
throw new NullPointerException("body == null");
|
||||
}
|
||||
if (headers != null && headers.get("Content-Type") != null) {
|
||||
throw new IllegalArgumentException("Unexpected header: Content-Type");
|
||||
}
|
||||
if (headers != null && headers.get("Content-Length") != null) {
|
||||
throw new IllegalArgumentException("Unexpected header: Content-Length");
|
||||
}
|
||||
return new Part(headers, body);
|
||||
}
|
||||
|
||||
public static Part createFormData(String name, String value) {
|
||||
return createFormData(name, null, RequestBody.create(null, value));
|
||||
}
|
||||
|
||||
public static Part createFormData(String name, String filename, RequestBody body) {
|
||||
if (name == null) {
|
||||
throw new NullPointerException("name == null");
|
||||
}
|
||||
StringBuilder disposition = new StringBuilder("form-data; name=");
|
||||
appendQuotedString(disposition, name);
|
||||
|
||||
if (filename != null) {
|
||||
disposition.append("; filename=");
|
||||
appendQuotedString(disposition, filename);
|
||||
}
|
||||
|
||||
return create(Headers.of("Content-Disposition", disposition.toString()), body);
|
||||
}
|
||||
|
||||
final Headers headers;
|
||||
final RequestBody body;
|
||||
|
||||
private Part(Headers headers, RequestBody body) {
|
||||
this.headers = headers;
|
||||
this.body = body;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private final ByteString boundary;
|
||||
private final List<Part> parts = new ArrayList<>();
|
||||
|
||||
public Builder() {
|
||||
this(UUID.randomUUID().toString());
|
||||
}
|
||||
|
||||
public Builder(String boundary) {
|
||||
this.boundary = ByteString.encodeUtf8(boundary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a form data part to the body.
|
||||
*/
|
||||
public Builder addFormDataPart(String name, String value) {
|
||||
return addPart(Part.createFormData(name, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a form data part to the body.
|
||||
*/
|
||||
public Builder addFormDataPart(String name, String filename, RequestBody body) {
|
||||
return addPart(Part.createFormData(name, filename, body));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a part to the body.
|
||||
*/
|
||||
public Builder addPart(Part part) {
|
||||
if (part == null) throw new NullPointerException("part == null");
|
||||
parts.add(part);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assemble the specified parts into a request body.
|
||||
*/
|
||||
public BugReporterMultipartBody build() {
|
||||
if (parts.isEmpty()) {
|
||||
throw new IllegalStateException("Multipart body must have at least one part.");
|
||||
}
|
||||
return new BugReporterMultipartBody(boundary, parts);
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user