Merge pull request #746 from vector-im/feature/fix_various_issues

Fix 2 crashes reported by the PlayStore
This commit is contained in:
Benoit Marty 2019-12-10 02:14:23 +01:00 committed by GitHub
commit 79ef055bfb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 860 additions and 497 deletions

View File

@ -12,12 +12,14 @@ Other changes:
Bugfix 🐛: Bugfix 🐛:
- When automardown is ON, pills are sent as MD in body (#739) - When automardown is ON, pills are sent as MD in body (#739)
- "ban" event are not rendered correctly (#716)
- Fix crash when rotating screen in Room timeline
Translations 🗣: Translations 🗣:
- -
Build 🧱: Build 🧱:
- "ban" event are not rendered correctly (#716) -
Changes in RiotX 0.9.1 (2019-12-05) Changes in RiotX 0.9.1 (2019-12-05)
=================================================== ===================================================

View File

@ -22,6 +22,8 @@ interface ContentUploadStateTracker {
fun untrack(key: String, updateListener: UpdateListener) fun untrack(key: String, updateListener: UpdateListener)
fun clear()
interface UpdateListener { interface UpdateListener {
fun onUpdate(state: State) fun onUpdate(state: State)
} }

View File

@ -50,6 +50,7 @@ interface RelationService {
/** /**
* Sends a reaction (emoji) to the targetedEvent. * Sends a reaction (emoji) to the targetedEvent.
* It has no effect if the user has already added the same reaction to the event.
* @param targetEventId the id of the event being reacted * @param targetEventId the id of the event being reacted
* @param reaction the reaction (preferably emoji) * @param reaction the reaction (preferably emoji)
*/ */

View File

@ -42,6 +42,10 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU
} }
} }
override fun clear() {
listeners.clear()
}
internal fun setFailure(key: String, throwable: Throwable) { internal fun setFailure(key: String, throwable: Throwable) {
val failure = ContentUploadStateTracker.State.Failure(throwable) val failure = ContentUploadStateTracker.State.Failure(throwable)
updateState(key, failure) updateState(key, failure)

View File

@ -30,10 +30,13 @@ import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.relation.RelationService import im.vector.matrix.android.api.session.room.model.relation.RelationService
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.NoOpCancellable
import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.room.send.EncryptEventWorker import im.vector.matrix.android.internal.session.room.send.EncryptEventWorker
@ -44,6 +47,7 @@ import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEvent
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.CancelableWork import im.vector.matrix.android.internal.util.CancelableWork
import im.vector.matrix.android.internal.util.fetchCopyMap
import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import timber.log.Timber import timber.log.Timber
@ -54,6 +58,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
private val cryptoService: CryptoService, private val cryptoService: CryptoService,
private val findReactionEventForUndoTask: FindReactionEventForUndoTask, private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
private val fetchEditHistoryTask: FetchEditHistoryTask, private val fetchEditHistoryTask: FetchEditHistoryTask,
private val timelineEventMapper: TimelineEventMapper,
private val monarchy: Monarchy, private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor) private val taskExecutor: TaskExecutor)
: RelationService { : RelationService {
@ -64,11 +69,27 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
} }
override fun sendReaction(targetEventId: String, reaction: String): Cancelable { override fun sendReaction(targetEventId: String, reaction: String): Cancelable {
return if (monarchy
.fetchCopyMap(
{ realm ->
TimelineEventEntity.where(realm, roomId, targetEventId).findFirst()
},
{ entity, _ ->
timelineEventMapper.map(entity)
})
?.annotations
?.reactionsSummary
.orEmpty()
.none { it.addedByMe && it.key == reaction }) {
val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction) val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction)
.also { saveLocalEcho(it) } .also { saveLocalEcho(it) }
val sendRelationWork = createSendEventWork(event, true) val sendRelationWork = createSendEventWork(event, true)
TimelineSendEventWorkCommon.postWork(context, roomId, sendRelationWork) TimelineSendEventWorkCommon.postWork(context, roomId, sendRelationWork)
return CancelableWork(context, sendRelationWork.id) CancelableWork(context, sendRelationWork.id)
} else {
Timber.w("Reaction already added")
NoOpCancellable
}
} }
override fun undoReaction(targetEventId: String, reaction: String): Cancelable { override fun undoReaction(targetEventId: String, reaction: String): Cancelable {

View File

@ -289,6 +289,9 @@ internal class DefaultTimeline(
} }
override fun addListener(listener: Timeline.Listener) = synchronized(listeners) { override fun addListener(listener: Timeline.Listener) = synchronized(listeners) {
if (listeners.contains(listener)) {
return false
}
listeners.add(listener).also { listeners.add(listener).also {
postSnapshot() postSnapshot()
} }

View File

@ -82,3 +82,6 @@ layout_constraintLeft_
### Will crash on API < 21. Use ?colorAccent instead ### Will crash on API < 21. Use ?colorAccent instead
\?android:colorAccent \?android:colorAccent
\?android:attr/colorAccent \?android:attr/colorAccent
### Use androidx.recyclerview.widget.RecyclerView because EpoxyRecyclerViews add behavior we do not want to
<com\.airbnb\.epoxy\.EpoxyRecyclerView

View File

@ -20,16 +20,22 @@ import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import im.vector.matrix.android.api.crypto.getAllVerificationEmojis import im.vector.matrix.android.api.crypto.getAllVerificationEmojis
import im.vector.riotx.R import im.vector.riotx.R
import kotlinx.android.synthetic.main.fragment_generic_recycler_epoxy.* import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import kotlinx.android.synthetic.main.fragment_generic_recycler.*
class DebugSasEmojiActivity : AppCompatActivity() { class DebugSasEmojiActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.fragment_generic_recycler_epoxy) setContentView(R.layout.fragment_generic_recycler)
val controller = SasEmojiController() val controller = SasEmojiController()
epoxyRecyclerView.setController(controller) recyclerView.configureWith(controller)
controller.setData(SasState(getAllVerificationEmojis())) controller.setData(SasState(getAllVerificationEmojis()))
} }
override fun onDestroy() {
recyclerView.cleanup()
super.onDestroy()
}
} }

View File

@ -42,7 +42,7 @@ import im.vector.riotx.core.di.DaggerVectorComponent
import im.vector.riotx.core.di.HasVectorInjector import im.vector.riotx.core.di.HasVectorInjector
import im.vector.riotx.core.di.VectorComponent import im.vector.riotx.core.di.VectorComponent
import im.vector.riotx.core.extensions.configureAndStart import im.vector.riotx.core.extensions.configureAndStart
import im.vector.riotx.core.utils.initKnownEmojiHashSet import im.vector.riotx.core.rx.setupRxPlugin
import im.vector.riotx.features.configuration.VectorConfiguration import im.vector.riotx.features.configuration.VectorConfiguration
import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks
import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.notifications.NotificationDrawerManager
@ -55,8 +55,7 @@ import im.vector.riotx.features.version.VersionProvider
import im.vector.riotx.push.fcm.FcmHelper import im.vector.riotx.push.fcm.FcmHelper
import timber.log.Timber import timber.log.Timber
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.*
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.Provider, androidx.work.Configuration.Provider { class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.Provider, androidx.work.Configuration.Provider {
@ -79,14 +78,13 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
lateinit var vectorComponent: VectorComponent lateinit var vectorComponent: VectorComponent
private var fontThreadHandler: Handler? = null private var fontThreadHandler: Handler? = null
// var slowMode = false
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
appContext = this appContext = this
vectorComponent = DaggerVectorComponent.factory().create(this) vectorComponent = DaggerVectorComponent.factory().create(this)
vectorComponent.inject(this) vectorComponent.inject(this)
vectorUncaughtExceptionHandler.activate(this) vectorUncaughtExceptionHandler.activate(this)
setupRxPlugin()
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree()) Timber.plant(Timber.DebugTree())
@ -138,7 +136,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
}) })
ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler) ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler)
// This should be done as early as possible // This should be done as early as possible
initKnownEmojiHashSet(appContext) // initKnownEmojiHashSet(appContext)
} }
override fun providesMatrixConfiguration() = MatrixConfiguration(BuildConfig.FLAVOR_DESCRIPTION) override fun providesMatrixConfiguration() = MatrixConfiguration(BuildConfig.FLAVOR_DESCRIPTION)

View File

@ -38,6 +38,7 @@ import im.vector.riotx.features.home.room.detail.RoomDetailFragment
import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.home.room.list.RoomListFragment
import im.vector.riotx.features.login.* import im.vector.riotx.features.login.*
import im.vector.riotx.features.login.terms.LoginTermsFragment import im.vector.riotx.features.login.terms.LoginTermsFragment
import im.vector.riotx.features.reactions.EmojiChooserFragment
import im.vector.riotx.features.reactions.EmojiSearchResultFragment import im.vector.riotx.features.reactions.EmojiSearchResultFragment
import im.vector.riotx.features.roomdirectory.PublicRoomsFragment import im.vector.riotx.features.roomdirectory.PublicRoomsFragment
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment
@ -255,4 +256,9 @@ interface FragmentModule {
@IntoMap @IntoMap
@FragmentKey(BreadcrumbsFragment::class) @FragmentKey(BreadcrumbsFragment::class)
fun bindBreadcrumbsFragment(fragment: BreadcrumbsFragment): Fragment fun bindBreadcrumbsFragment(fragment: BreadcrumbsFragment): Fragment
@Binds
@IntoMap
@FragmentKey(EmojiChooserFragment::class)
fun bindEmojiChooserFragment(fragment: EmojiChooserFragment): Fragment
} }

View File

@ -0,0 +1,45 @@
/*
* 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.riotx.core.extensions
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyController
/**
* Apply a Vertical LinearLayout Manager to the recyclerView and set the adapter from the epoxy controller
*/
fun RecyclerView.configureWith(epoxyController: EpoxyController,
itemAnimator: RecyclerView.ItemAnimator? = null,
showDivider: Boolean = false,
hasFixedSize: Boolean = true) {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
itemAnimator?.let { this.itemAnimator = it }
if (showDivider) {
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
}
setHasFixedSize(hasFixedSize)
adapter = epoxyController.adapter
}
/**
* To call from Fragment.onDestroyView()
*/
fun RecyclerView.cleanup() {
adapter = null
}

View File

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

View File

@ -73,7 +73,7 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
injectWith(screenComponent) injectWith(screenComponent)
} }
protected open fun injectWith(screenComponent: ScreenComponent) = Unit protected open fun injectWith(injector: ScreenComponent) = Unit
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
mvrxViewIdProperty.restoreFrom(savedInstanceState) mvrxViewIdProperty.restoreFrom(savedInstanceState)

View File

@ -135,6 +135,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
restorables.forEach { it.onSaveInstanceState(outState) } restorables.forEach { it.onSaveInstanceState(outState) }
restorables.clear()
} }
override fun onViewStateRestored(savedInstanceState: Bundle?) { override fun onViewStateRestored(savedInstanceState: Bundle?) {

View File

@ -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.riotx.core.rx
import im.vector.riotx.BuildConfig
import io.reactivex.plugins.RxJavaPlugins
import timber.log.Timber
/**
* Make sure unhandled Rx error does not crash the app in production
*/
fun setupRxPlugin() {
RxJavaPlugins.setErrorHandler { throwable ->
Timber.e(throwable, "RxError")
// Avoid crash in production
if (BuildConfig.DEBUG) {
throw throwable
}
}
}

View File

@ -16,13 +16,6 @@
package im.vector.riotx.core.utils package im.vector.riotx.core.utils
import android.content.Context
import com.squareup.moshi.Moshi
import im.vector.riotx.R
import im.vector.riotx.features.reactions.EmojiDataSource
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.regex.Pattern import java.util.regex.Pattern
private val emojisPattern = Pattern.compile("((?:[\uD83C\uDF00-\uD83D\uDDFF]" + private val emojisPattern = Pattern.compile("((?:[\uD83C\uDF00-\uD83D\uDDFF]" +
@ -49,6 +42,7 @@ private val emojisPattern = Pattern.compile("((?:[\uD83C\uDF00-\uD83D\uDDFF]" +
"|\uD83C\uDCCF\uFE0F?" + "|\uD83C\uDCCF\uFE0F?" +
"|[\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA]\uFE0F?))") "|[\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA]\uFE0F?))")
/*
// A hashset from all supported emoji // A hashset from all supported emoji
private var knownEmojiSet: HashSet<String>? = null private var knownEmojiSet: HashSet<String>? = null
@ -56,7 +50,7 @@ fun initKnownEmojiHashSet(context: Context, done: (() -> Unit)? = null) {
GlobalScope.launch { GlobalScope.launch {
context.resources.openRawResource(R.raw.emoji_picker_datasource).use { input -> context.resources.openRawResource(R.raw.emoji_picker_datasource).use { input ->
val moshi = Moshi.Builder().build() val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(EmojiDataSource.EmojiData::class.java) val jsonAdapter = moshi.adapter(EmojiData::class.java)
val inputAsString = input.bufferedReader().use { it.readText() } val inputAsString = input.bufferedReader().use { it.readText() }
val source = jsonAdapter.fromJson(inputAsString) val source = jsonAdapter.fromJson(inputAsString)
knownEmojiSet = HashSet<String>().also { knownEmojiSet = HashSet<String>().also {
@ -77,6 +71,7 @@ fun isSingleEmoji(string: String): Boolean {
} }
return knownEmojiSet?.contains(string) ?: false return knownEmojiSet?.contains(string) ?: false
} }
*/
/** /**
* Test if a string contains emojis. * Test if a string contains emojis.

View File

@ -21,6 +21,8 @@ import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreActivity import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreActivity
import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity
@ -37,12 +39,16 @@ class KeysBackupSettingsFragment @Inject constructor(private val keysBackupSetti
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
keysBackupSettingsRecyclerView.configureWith(keysBackupSettingsRecyclerViewController)
keysBackupSettingsRecyclerView.setController(keysBackupSettingsRecyclerViewController)
keysBackupSettingsRecyclerViewController.listener = this keysBackupSettingsRecyclerViewController.listener = this
} }
override fun onDestroyView() {
keysBackupSettingsRecyclerViewController.listener = null
keysBackupSettingsRecyclerView.cleanup()
super.onDestroyView()
}
override fun invalidate() = withState(viewModel) { state -> override fun invalidate() = withState(viewModel) { state ->
keysBackupSettingsRecyclerViewController.setData(state) keysBackupSettingsRecyclerViewController.setData(state)
} }

View File

@ -25,7 +25,6 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.google.android.material.bottomnavigation.BottomNavigationItemView import com.google.android.material.bottomnavigation.BottomNavigationItemView
import com.google.android.material.bottomnavigation.BottomNavigationMenuView import com.google.android.material.bottomnavigation.BottomNavigationMenuView
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.riotx.R import im.vector.riotx.R
@ -46,7 +45,6 @@ private const val INDEX_PEOPLE = 1
private const val INDEX_ROOMS = 2 private const val INDEX_ROOMS = 2
class HomeDetailFragment @Inject constructor( class HomeDetailFragment @Inject constructor(
private val session: Session,
val homeDetailViewModelFactory: HomeDetailViewModel.Factory, val homeDetailViewModelFactory: HomeDetailViewModel.Factory,
private val avatarRenderer: AvatarRenderer private val avatarRenderer: AvatarRenderer
) : VectorBaseFragment(), KeysBackupBanner.Delegate { ) : VectorBaseFragment(), KeysBackupBanner.Delegate {
@ -56,9 +54,7 @@ class HomeDetailFragment @Inject constructor(
private val viewModel: HomeDetailViewModel by fragmentViewModel() private val viewModel: HomeDetailViewModel by fragmentViewModel()
private lateinit var sharedActionViewModel: HomeSharedActionViewModel private lateinit var sharedActionViewModel: HomeSharedActionViewModel
override fun getLayoutResId(): Int { override fun getLayoutResId() = R.layout.fragment_home_detail
return R.layout.fragment_home_detail
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)

View File

@ -23,9 +23,7 @@ import com.airbnb.mvrx.withState
import com.jakewharton.rxbinding3.widget.textChanges import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.*
import im.vector.riotx.core.extensions.setupAsSearch
import im.vector.riotx.core.extensions.showKeyboard
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import kotlinx.android.synthetic.main.fragment_create_direct_room_directory_users.* import kotlinx.android.synthetic.main.fragment_create_direct_room_directory_users.*
import javax.inject.Inject import javax.inject.Inject
@ -48,10 +46,15 @@ class CreateDirectRoomDirectoryUsersFragment @Inject constructor(
setupCloseView() setupCloseView()
} }
override fun onDestroyView() {
recyclerView.cleanup()
directRoomController.callback = null
super.onDestroyView()
}
private fun setupRecyclerView() { private fun setupRecyclerView() {
recyclerView.setHasFixedSize(true)
directRoomController.callback = this directRoomController.callback = this
recyclerView.setController(directRoomController) recyclerView.configureWith(directRoomController)
} }
private fun setupSearchByMatrixIdView() { private fun setupSearchByMatrixIdView() {

View File

@ -31,9 +31,7 @@ import com.google.android.material.chip.ChipGroup
import com.jakewharton.rxbinding3.widget.textChanges import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.*
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.setupAsSearch
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.core.utils.DimensionConverter
import kotlinx.android.synthetic.main.fragment_create_direct_room.* import kotlinx.android.synthetic.main.fragment_create_direct_room.*
@ -67,6 +65,12 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor(
} }
} }
override fun onDestroyView() {
knownUsersController.callback = null
recyclerView.cleanup()
super.onDestroyView()
}
override fun onPrepareOptionsMenu(menu: Menu) { override fun onPrepareOptionsMenu(menu: Menu) {
withState(viewModel) { withState(viewModel) {
val createMenuItem = menu.findItem(R.id.action_create_direct_room) val createMenuItem = menu.findItem(R.id.action_create_direct_room)
@ -94,11 +98,10 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor(
} }
private fun setupRecyclerView() { private fun setupRecyclerView() {
recyclerView.setHasFixedSize(true)
// Don't activate animation as we might have way to much item animation when filtering // Don't activate animation as we might have way to much item animation when filtering
recyclerView.itemAnimator = null recyclerView.itemAnimator = null
knownUsersController.callback = this knownUsersController.callback = this
recyclerView.setController(knownUsersController) recyclerView.configureWith(knownUsersController)
} }
private fun setupFilterView() { private fun setupFilterView() {

View File

@ -23,11 +23,13 @@ import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.platform.StateView import im.vector.riotx.core.platform.StateView
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.home.HomeSharedActionViewModel
import im.vector.riotx.features.home.HomeActivitySharedAction import im.vector.riotx.features.home.HomeActivitySharedAction
import im.vector.riotx.features.home.HomeSharedActionViewModel
import kotlinx.android.synthetic.main.fragment_group_list.* import kotlinx.android.synthetic.main.fragment_group_list.*
import javax.inject.Inject import javax.inject.Inject
@ -45,14 +47,20 @@ class GroupListFragment @Inject constructor(
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(HomeSharedActionViewModel::class.java) sharedActionViewModel = activityViewModelProvider.get(HomeSharedActionViewModel::class.java)
groupController.callback = this groupController.callback = this
stateView.contentView = groupListEpoxyRecyclerView stateView.contentView = groupListView
groupListEpoxyRecyclerView.setController(groupController) groupListView.configureWith(groupController)
viewModel.subscribe { renderState(it) } viewModel.subscribe { renderState(it) }
viewModel.openGroupLiveData.observeEvent(this) { viewModel.openGroupLiveData.observeEvent(this) {
sharedActionViewModel.post(HomeActivitySharedAction.OpenGroup) sharedActionViewModel.post(HomeActivitySharedAction.OpenGroup)
} }
} }
override fun onDestroyView() {
groupController.callback = null
groupListView.cleanup()
super.onDestroyView()
}
private fun renderState(state: GroupListViewState) { private fun renderState(state: GroupListViewState) {
when (state.asyncGroups) { when (state.asyncGroups) {
is Incomplete -> stateView.state = StateView.State.Loading is Incomplete -> stateView.state = StateView.State.Loading

View File

@ -18,9 +18,10 @@ package im.vector.riotx.features.home.room.breadcrumbs
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.home.room.detail.RoomDetailSharedAction import im.vector.riotx.features.home.room.detail.RoomDetailSharedAction
import im.vector.riotx.features.home.room.detail.RoomDetailSharedActionViewModel import im.vector.riotx.features.home.room.detail.RoomDetailSharedActionViewModel
@ -46,16 +47,13 @@ class BreadcrumbsFragment @Inject constructor(
} }
override fun onDestroyView() { override fun onDestroyView() {
breadcrumbsRecyclerView.cleanup()
super.onDestroyView() super.onDestroyView()
breadcrumbsRecyclerView.adapter = null
} }
private fun setupRecyclerView() { private fun setupRecyclerView() {
val layoutManager = LinearLayoutManager(context) breadcrumbsRecyclerView.configureWith(breadcrumbsController, BreadcrumbsAnimator(), hasFixedSize = false)
breadcrumbsRecyclerView.layoutManager = layoutManager
breadcrumbsRecyclerView.itemAnimator = BreadcrumbsAnimator()
breadcrumbsController.listener = this breadcrumbsController.listener = this
breadcrumbsRecyclerView.setController(breadcrumbsController)
} }
private fun renderState(state: BreadcrumbsViewState) { private fun renderState(state: BreadcrumbsViewState) {

View File

@ -46,6 +46,7 @@ import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView import butterknife.BindView
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.mvrx.* import com.airbnb.mvrx.*
import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.ImageLoader import com.github.piasy.biv.loader.ImageLoader
@ -69,10 +70,7 @@ import im.vector.riotx.R
import im.vector.riotx.core.dialogs.withColoredButton import im.vector.riotx.core.dialogs.withColoredButton
import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.*
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.extensions.showKeyboard
import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.files.addEntryToDownloadManager
import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
@ -193,6 +191,8 @@ class RoomDetailFragment @Inject constructor(
private lateinit var sharedActionViewModel: MessageSharedActionViewModel private lateinit var sharedActionViewModel: MessageSharedActionViewModel
private lateinit var layoutManager: LinearLayoutManager private lateinit var layoutManager: LinearLayoutManager
private var modelBuildListener: OnModelBuildFinishedListener? = null
private lateinit var attachmentsHelper: AttachmentsHelper private lateinit var attachmentsHelper: AttachmentsHelper
private lateinit var keyboardStateUtils: KeyboardStateUtils private lateinit var keyboardStateUtils: KeyboardStateUtils
@ -286,13 +286,16 @@ class RoomDetailFragment @Inject constructor(
} }
override fun onDestroyView() { override fun onDestroyView() {
timelineEventController.callback = null
timelineEventController.removeModelBuildListener(modelBuildListener)
modelBuildListener = null
debouncer.cancelAll()
recyclerView.cleanup()
super.onDestroyView() super.onDestroyView()
recyclerView.adapter = null
} }
override fun onDestroy() { override fun onDestroy() {
roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState) roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
debouncer.cancelAll()
super.onDestroy() super.onDestroy()
} }
@ -447,11 +450,7 @@ class RoomDetailFragment @Inject constructor(
if (!hasBeenHandled && resultCode == RESULT_OK && data != null) { if (!hasBeenHandled && resultCode == RESULT_OK && data != null) {
when (requestCode) { when (requestCode) {
REACTION_SELECT_REQUEST_CODE -> { REACTION_SELECT_REQUEST_CODE -> {
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) val (eventId, reaction) = EmojiReactionPickerActivity.getOutput(data) ?: return
?: return
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
?: return
// TODO check if already reacted with that?
roomDetailViewModel.handle(RoomDetailAction.SendReaction(eventId, reaction)) roomDetailViewModel.handle(RoomDetailAction.SendReaction(eventId, reaction))
} }
} }
@ -470,13 +469,14 @@ class RoomDetailFragment @Inject constructor(
recyclerView.layoutManager = layoutManager recyclerView.layoutManager = layoutManager
recyclerView.itemAnimator = null recyclerView.itemAnimator = null
recyclerView.setHasFixedSize(true) recyclerView.setHasFixedSize(true)
timelineEventController.addModelBuildListener { modelBuildListener = OnModelBuildFinishedListener {
it.dispatchTo(stateRestorer) it.dispatchTo(stateRestorer)
it.dispatchTo(scrollOnNewMessageCallback) it.dispatchTo(scrollOnNewMessageCallback)
it.dispatchTo(scrollOnHighlightedEventCallback) it.dispatchTo(scrollOnHighlightedEventCallback)
updateJumpToReadMarkerViewVisibility() updateJumpToReadMarkerViewVisibility()
updateJumpToBottomViewVisibility() updateJumpToBottomViewVisibility()
} }
timelineEventController.addModelBuildListener(modelBuildListener)
recyclerView.adapter = timelineEventController.adapter recyclerView.adapter = timelineEventController.adapter
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
@ -521,7 +521,8 @@ class RoomDetailFragment @Inject constructor(
} }
} }
private fun updateJumpToReadMarkerViewVisibility() = jumpToReadMarkerView.post { private fun updateJumpToReadMarkerViewVisibility() {
jumpToReadMarkerView?.post {
withState(roomDetailViewModel) { withState(roomDetailViewModel) {
val showJumpToUnreadBanner = when (it.unreadState) { val showJumpToUnreadBanner = when (it.unreadState) {
UnreadState.Unknown, UnreadState.Unknown,
@ -544,6 +545,7 @@ class RoomDetailFragment @Inject constructor(
jumpToReadMarkerView.isVisible = showJumpToUnreadBanner jumpToReadMarkerView.isVisible = showJumpToUnreadBanner
} }
} }
}
private fun updateJumpToBottomViewVisibility() { private fun updateJumpToBottomViewVisibility() {
debouncer.debounce("jump_to_bottom_visibility", 250, Runnable { debouncer.debounce("jump_to_bottom_visibility", 250, Runnable {

View File

@ -863,7 +863,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
override fun onCleared() { override fun onCleared() {
timeline.dispose() timeline.dispose()
timeline.removeAllListeners() timeline.removeListener(this)
super.onCleared() super.onCleared()
} }
} }

View File

@ -21,7 +21,6 @@ import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView import butterknife.BindView
import butterknife.ButterKnife import butterknife.ButterKnife
@ -29,6 +28,8 @@ import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
@ -52,8 +53,8 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
private val displayReadReceiptArgs: DisplayReadReceiptArgs by args() private val displayReadReceiptArgs: DisplayReadReceiptArgs by args()
override fun injectWith(screenComponent: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {
screenComponent.inject(this) injector.inject(this)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -64,12 +65,16 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) recyclerView.configureWith(epoxyController, hasFixedSize = false)
recyclerView.adapter = epoxyController.adapter
bottomSheetTitle.text = getString(R.string.seen_by) bottomSheetTitle.text = getString(R.string.seen_by)
epoxyController.setData(displayReadReceiptArgs.readReceipts) epoxyController.setData(displayReadReceiptArgs.readReceipts)
} }
override fun onDestroyView() {
recyclerView.cleanup()
super.onDestroyView()
}
// we are not using state for this one as it's static, so no need to override invalidate() // we are not using state for this one as it's static, so no need to override invalidate()
companion object { companion object {

View File

@ -25,7 +25,6 @@ import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.VisibilityState import com.airbnb.epoxy.VisibilityState
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
@ -44,7 +43,7 @@ import org.threeten.bp.LocalDateTime
import javax.inject.Inject import javax.inject.Inject
class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter, class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter,
private val session: Session, private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
private val timelineItemFactory: TimelineItemFactory, private val timelineItemFactory: TimelineItemFactory,
private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val mergedHeaderItemFactory: MergedHeaderItemFactory, private val mergedHeaderItemFactory: MergedHeaderItemFactory,
@ -209,6 +208,13 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
timelineMediaSizeProvider.recyclerView = recyclerView timelineMediaSizeProvider.recyclerView = recyclerView
} }
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
timelineMediaSizeProvider.recyclerView = null
contentUploadStateTrackerBinder.clear()
timeline?.removeListener(this)
super.onDetachedFromRecyclerView(recyclerView)
}
override fun buildModels() { override fun buildModels() {
val timestamp = System.currentTimeMillis() val timestamp = System.currentTimeMillis()
showingForwardLoader = LoadingItem_() showingForwardLoader = LoadingItem_()

View File

@ -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.riotx.features.home.room.detail.timeline.action
import androidx.recyclerview.widget.DefaultItemAnimator
private const val ANIM_DURATION_IN_MILLIS = 300L
/**
* We only want to animate the expand of the "Report content" submenu
*/
class MessageActionsAnimator : DefaultItemAnimator() {
init {
addDuration = ANIM_DURATION_IN_MILLIS
removeDuration = 0
moveDuration = 0
changeDuration = 0
}
}

View File

@ -19,7 +19,6 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView import butterknife.BindView
import butterknife.ButterKnife import butterknife.ButterKnife
@ -27,6 +26,8 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import javax.inject.Inject import javax.inject.Inject
@ -48,8 +49,8 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message
private lateinit var sharedActionViewModel: MessageSharedActionViewModel private lateinit var sharedActionViewModel: MessageSharedActionViewModel
override fun injectWith(screenComponent: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {
screenComponent.inject(this) injector.inject(this)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -61,13 +62,17 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java) sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java)
recyclerView.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false) recyclerView.configureWith(messageActionsEpoxyController, hasFixedSize = false)
recyclerView.adapter = messageActionsEpoxyController.adapter
// Disable item animation // Disable item animation
recyclerView.itemAnimator = null recyclerView.itemAnimator = null
messageActionsEpoxyController.listener = this messageActionsEpoxyController.listener = this
} }
override fun onDestroyView() {
recyclerView.cleanup()
super.onDestroyView()
}
override fun onUrlClicked(url: String): Boolean { override fun onUrlClicked(url: String): Boolean {
sharedActionViewModel.post(EventSharedAction.OnUrlClicked(url)) sharedActionViewModel.post(EventSharedAction.OnUrlClicked(url))
// Always consume // Always consume
@ -83,6 +88,10 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message
override fun didSelectMenuAction(eventAction: EventSharedAction) { override fun didSelectMenuAction(eventAction: EventSharedAction) {
if (eventAction is EventSharedAction.ReportContent) { if (eventAction is EventSharedAction.ReportContent) {
// Toggle report menu // Toggle report menu
// Enable item animation
if (recyclerView.itemAnimator == null) {
recyclerView.itemAnimator = MessageActionsAnimator()
}
viewModel.handle(MessageActionsAction.ToggleReportMenu) viewModel.handle(MessageActionsAction.ToggleReportMenu)
} else { } else {
sharedActionViewModel.post(eventAction) sharedActionViewModel.post(eventAction)

View File

@ -19,9 +19,6 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView import butterknife.BindView
import butterknife.ButterKnife import butterknife.ButterKnife
@ -30,6 +27,8 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
@ -54,8 +53,8 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() {
ViewEditHistoryEpoxyController(requireContext(), viewModel.dateFormatter, eventHtmlRenderer) ViewEditHistoryEpoxyController(requireContext(), viewModel.dateFormatter, eventHtmlRenderer)
} }
override fun injectWith(screenComponent: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {
screenComponent.inject(this) injector.inject(this)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -66,13 +65,18 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
recyclerView.adapter = epoxyController.adapter recyclerView.configureWith(
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) epoxyController,
val dividerItemDecoration = DividerItemDecoration(requireContext(), LinearLayout.VERTICAL) showDivider = true,
recyclerView.addItemDecoration(dividerItemDecoration) hasFixedSize = false)
bottomSheetTitle.text = context?.getString(R.string.message_edits) bottomSheetTitle.text = context?.getString(R.string.message_edits)
} }
override fun onDestroyView() {
recyclerView.cleanup()
super.onDestroyView()
}
override fun invalidate() = withState(viewModel) { override fun invalidate() = withState(viewModel) {
epoxyController.setData(it) epoxyController.setData(it)
super.invalidate() super.invalidate()

View File

@ -28,17 +28,16 @@ import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.extensions.localDateTime
import im.vector.riotx.core.ui.list.genericFooterItem import im.vector.riotx.core.ui.list.genericFooterItem
import im.vector.riotx.core.ui.list.genericItem import im.vector.riotx.core.ui.list.genericItem
import im.vector.riotx.core.ui.list.genericItemHeader import im.vector.riotx.core.ui.list.genericItemHeader
import im.vector.riotx.core.ui.list.genericLoaderItem import im.vector.riotx.core.ui.list.genericLoaderItem
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.EventHtmlRenderer
import me.gujun.android.span.span import me.gujun.android.span.span
import name.fraser.neil.plaintext.diff_match_patch import name.fraser.neil.plaintext.diff_match_patch
import timber.log.Timber import java.util.*
import java.util.Calendar
/** /**
* Epoxy controller for edit history list * Epoxy controller for edit history list
@ -104,9 +103,7 @@ class ViewEditHistoryEpoxyController(private val context: Context,
?: nContent.first ?: nContent.first
val dmp = diff_match_patch() val dmp = diff_match_patch()
val diff = dmp.diff_main(nextBody.toString(), body.toString()) val diff = dmp.diff_main(nextBody.toString(), body.toString())
Timber.e("#### Diff: $diff")
dmp.diff_cleanupSemantic(diff) dmp.diff_cleanupSemantic(diff)
Timber.e("#### Diff: $diff")
spannedDiff = span { spannedDiff = span {
diff.map { diff.map {
when (it.operation) { when (it.operation) {

View File

@ -25,12 +25,14 @@ import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.di.ScreenScope
import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.utils.TextUtils import im.vector.riotx.core.utils.TextUtils
import im.vector.riotx.features.ui.getMessageTextColor import im.vector.riotx.features.ui.getMessageTextColor
import javax.inject.Inject import javax.inject.Inject
@ScreenScope
class ContentUploadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, class ContentUploadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
private val colorProvider: ColorProvider, private val colorProvider: ColorProvider,
private val errorFormatter: ErrorFormatter) { private val errorFormatter: ErrorFormatter) {
@ -40,7 +42,7 @@ class ContentUploadStateTrackerBinder @Inject constructor(private val activeSess
fun bind(eventId: String, fun bind(eventId: String,
isLocalFile: Boolean, isLocalFile: Boolean,
progressLayout: ViewGroup) { progressLayout: ViewGroup) {
activeSessionHolder.getActiveSession().also { session -> activeSessionHolder.getSafeActiveSession()?.also { session ->
val uploadStateTracker = session.contentUploadProgressTracker() val uploadStateTracker = session.contentUploadProgressTracker()
val updateListener = ContentMediaProgressUpdater(progressLayout, isLocalFile, colorProvider, errorFormatter) val updateListener = ContentMediaProgressUpdater(progressLayout, isLocalFile, colorProvider, errorFormatter)
updateListeners[eventId] = updateListener updateListeners[eventId] = updateListener
@ -49,13 +51,19 @@ class ContentUploadStateTrackerBinder @Inject constructor(private val activeSess
} }
fun unbind(eventId: String) { fun unbind(eventId: String) {
activeSessionHolder.getActiveSession().also { session -> activeSessionHolder.getSafeActiveSession()?.also { session ->
val uploadStateTracker = session.contentUploadProgressTracker() val uploadStateTracker = session.contentUploadProgressTracker()
updateListeners[eventId]?.also { updateListeners[eventId]?.also {
uploadStateTracker.untrack(eventId, it) uploadStateTracker.untrack(eventId, it)
} }
} }
} }
fun clear() {
activeSessionHolder.getSafeActiveSession()?.also {
it.contentUploadProgressTracker().clear()
}
}
} }
private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,

View File

@ -19,11 +19,12 @@ package im.vector.riotx.features.home.room.detail.timeline.helper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import im.vector.riotx.core.di.ScreenScope import im.vector.riotx.core.di.ScreenScope
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.roundToInt
@ScreenScope @ScreenScope
class TimelineMediaSizeProvider @Inject constructor() { class TimelineMediaSizeProvider @Inject constructor() {
lateinit var recyclerView: RecyclerView var recyclerView: RecyclerView? = null
private var cachedSize: Pair<Int, Int>? = null private var cachedSize: Pair<Int, Int>? = null
fun getMaxSize(): Pair<Int, Int> { fun getMaxSize(): Pair<Int, Int> {
@ -31,17 +32,17 @@ class TimelineMediaSizeProvider @Inject constructor() {
} }
private fun computeMaxSize(): Pair<Int, Int> { private fun computeMaxSize(): Pair<Int, Int> {
val width = recyclerView.width val width = recyclerView?.width ?: 0
val height = recyclerView.height val height = recyclerView?.height ?: 0
val maxImageWidth: Int val maxImageWidth: Int
val maxImageHeight: Int val maxImageHeight: Int
// landscape / portrait // landscape / portrait
if (width < height) { if (width < height) {
maxImageWidth = Math.round(width * 0.7f) maxImageWidth = (width * 0.7f).roundToInt()
maxImageHeight = Math.round(height * 0.5f) maxImageHeight = (height * 0.5f).roundToInt()
} else { } else {
maxImageWidth = Math.round(width * 0.5f) maxImageWidth = (width * 0.5f).roundToInt()
maxImageHeight = Math.round(height * 0.7f) maxImageHeight = (height * 0.7f).roundToInt()
} }
return Pair(maxImageWidth, maxImageHeight) return Pair(maxImageWidth, maxImageHeight)
} }

View File

@ -20,7 +20,6 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView import butterknife.BindView
import butterknife.ButterKnife import butterknife.ButterKnife
@ -29,6 +28,8 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
@ -49,8 +50,8 @@ class ViewReactionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
@Inject lateinit var epoxyController: ViewReactionsEpoxyController @Inject lateinit var epoxyController: ViewReactionsEpoxyController
override fun injectWith(screenComponent: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {
screenComponent.inject(this) injector.inject(this)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -61,11 +62,15 @@ class ViewReactionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) recyclerView.configureWith(epoxyController, hasFixedSize = false)
recyclerView.adapter = epoxyController.adapter
bottomSheetTitle.text = context?.getString(R.string.reactions) bottomSheetTitle.text = context?.getString(R.string.reactions)
} }
override fun onDestroyView() {
recyclerView.cleanup()
super.onDestroyView()
}
override fun invalidate() = withState(viewModel) { override fun invalidate() = withState(viewModel) {
epoxyController.setData(it) epoxyController.setData(it)
super.invalidate() super.invalidate()

View File

@ -36,9 +36,7 @@ class FilteredRoomsActivity : VectorBaseActivity() {
return supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as? RoomListFragment return supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as? RoomListFragment
} }
override fun getLayoutRes(): Int { override fun getLayoutRes() = R.layout.activity_filtered_rooms
return R.layout.activity_filtered_rooms
}
override fun injectWith(injector: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {
injector.inject(this) injector.inject(this)

View File

@ -26,6 +26,7 @@ import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.mvrx.* import com.airbnb.mvrx.*
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
@ -35,13 +36,13 @@ import im.vector.matrix.android.api.session.room.notification.RoomNotificationSt
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.platform.OnBackPressed import im.vector.riotx.core.platform.OnBackPressed
import im.vector.riotx.core.platform.StateView import im.vector.riotx.core.platform.StateView
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.home.RoomListDisplayMode import im.vector.riotx.features.home.RoomListDisplayMode
import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedAction
import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsBottomSheet import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsBottomSheet
import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedAction
import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
import im.vector.riotx.features.home.room.list.widget.FabMenuView import im.vector.riotx.features.home.room.list.widget.FabMenuView
import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.notifications.NotificationDrawerManager
@ -65,6 +66,7 @@ class RoomListFragment @Inject constructor(
) : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener { ) : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener {
private var modelBuildListener: OnModelBuildFinishedListener? = null
private lateinit var sharedActionViewModel: RoomListQuickActionsSharedActionViewModel private lateinit var sharedActionViewModel: RoomListQuickActionsSharedActionViewModel
private val roomListParams: RoomListParams by args() private val roomListParams: RoomListParams by args()
private val roomListViewModel: RoomListViewModel by fragmentViewModel() private val roomListViewModel: RoomListViewModel by fragmentViewModel()
@ -118,8 +120,12 @@ class RoomListFragment @Inject constructor(
} }
override fun onDestroyView() { override fun onDestroyView() {
roomController.removeModelBuildListener(modelBuildListener)
modelBuildListener = null
roomListView.cleanup()
roomController.listener = null
createChatFabMenu.listener = null
super.onDestroyView() super.onDestroyView()
roomListView.adapter = null
} }
private fun openSelectedRoom(event: RoomListViewEvents.SelectRoom) { private fun openSelectedRoom(event: RoomListViewEvents.SelectRoom) {
@ -198,7 +204,8 @@ class RoomListFragment @Inject constructor(
roomListView.layoutManager = layoutManager roomListView.layoutManager = layoutManager
roomListView.itemAnimator = RoomListAnimator() roomListView.itemAnimator = RoomListAnimator()
roomController.listener = this roomController.listener = this
roomController.addModelBuildListener { it.dispatchTo(stateRestorer) } modelBuildListener = OnModelBuildFinishedListener { it.dispatchTo(stateRestorer) }
roomController.addModelBuildListener(modelBuildListener)
roomListView.adapter = roomController.adapter roomListView.adapter = roomController.adapter
stateView.contentView = roomListView stateView.contentView = roomListView
} }

View File

@ -21,7 +21,6 @@ import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView import butterknife.BindView
import butterknife.ButterKnife import butterknife.ButterKnife
@ -29,6 +28,8 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.features.navigation.Navigator import im.vector.riotx.features.navigation.Navigator
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
@ -56,8 +57,8 @@ class RoomListQuickActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), R
override val showExpanded = true override val showExpanded = true
override fun injectWith(screenComponent: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {
screenComponent.inject(this) injector.inject(this)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -69,13 +70,17 @@ class RoomListQuickActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), R
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java) sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java)
recyclerView.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false) recyclerView.configureWith(roomListActionsEpoxyController, hasFixedSize = false)
recyclerView.adapter = roomListActionsEpoxyController.adapter
// Disable item animation // Disable item animation
recyclerView.itemAnimator = null recyclerView.itemAnimator = null
roomListActionsEpoxyController.listener = this roomListActionsEpoxyController.listener = this
} }
override fun onDestroyView() {
recyclerView.cleanup()
super.onDestroyView()
}
override fun invalidate() = withState(viewModel) { override fun invalidate() = withState(viewModel) {
roomListActionsEpoxyController.setData(it) roomListActionsEpoxyController.setData(it)
super.invalidate() super.invalidate()

View File

@ -24,6 +24,8 @@ import butterknife.OnClick
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.utils.openUrlInExternalBrowser import im.vector.riotx.core.utils.openUrlInExternalBrowser
import im.vector.riotx.features.login.AbstractLoginFragment import im.vector.riotx.features.login.AbstractLoginFragment
import im.vector.riotx.features.login.LoginAction import im.vector.riotx.features.login.LoginAction
@ -55,8 +57,7 @@ class LoginTermsFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
loginTermsPolicyList.configureWith(policyController)
loginTermsPolicyList.setController(policyController)
policyController.listener = this policyController.listener = this
val list = ArrayList<LocalizedFlowDataLoginTermsChecked>() val list = ArrayList<LocalizedFlowDataLoginTermsChecked>()
@ -69,6 +70,12 @@ class LoginTermsFragment @Inject constructor(
loginTermsViewState = LoginTermsViewState(list) loginTermsViewState = LoginTermsViewState(list)
} }
override fun onDestroyView() {
loginTermsPolicyList.cleanup()
policyController.listener = null
super.onDestroyView()
}
private fun renderState() { private fun renderState() {
policyController.setData(loginTermsViewState.localizedFlowDataLoginTermsChecked) policyController.setData(loginTermsViewState.localizedFlowDataLoginTermsChecked)

View File

@ -17,12 +17,18 @@ package im.vector.riotx.features.reactions
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.lifecycle.observe
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import kotlinx.android.synthetic.main.emoji_chooser_fragment.*
import javax.inject.Inject import javax.inject.Inject
class EmojiChooserFragment @Inject constructor() : VectorBaseFragment() { class EmojiChooserFragment @Inject constructor(
private val emojiRecyclerAdapter: EmojiRecyclerAdapter
) : VectorBaseFragment(),
EmojiRecyclerAdapter.InteractionListener,
ReactionClickListener {
override fun getLayoutResId() = R.layout.emoji_chooser_fragment override fun getLayoutResId() = R.layout.emoji_chooser_fragment
@ -31,10 +37,29 @@ class EmojiChooserFragment @Inject constructor() : VectorBaseFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
viewModel = activityViewModelProvider.get(EmojiChooserViewModel::class.java) viewModel = activityViewModelProvider.get(EmojiChooserViewModel::class.java)
viewModel.initWithContext(context!!)
(view as? RecyclerView)?.let { emojiRecyclerAdapter.reactionClickListener = this
it.adapter = viewModel.adapter emojiRecyclerAdapter.interactionListener = this
it.adapter?.notifyDataSetChanged()
emojiRecyclerView.adapter = emojiRecyclerAdapter
viewModel.moveToSection.observe(viewLifecycleOwner) { section ->
emojiRecyclerAdapter.scrollToSection(section)
} }
} }
override fun firstVisibleSectionChange(section: Int) {
viewModel.setCurrentSection(section)
}
override fun onReactionSelected(reaction: String) {
viewModel.onReactionSelected(reaction)
}
override fun onDestroyView() {
emojiRecyclerView.cleanup()
emojiRecyclerAdapter.reactionClickListener = null
emojiRecyclerAdapter.interactionListener = null
super.onDestroyView()
}
} }

View File

@ -15,7 +15,6 @@
*/ */
package im.vector.riotx.features.reactions package im.vector.riotx.features.reactions
import android.content.Context
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.core.utils.LiveEvent
@ -23,36 +22,26 @@ import javax.inject.Inject
class EmojiChooserViewModel @Inject constructor() : ViewModel() { class EmojiChooserViewModel @Inject constructor() : ViewModel() {
var adapter: EmojiRecyclerAdapter? = null
val emojiSourceLiveData: MutableLiveData<EmojiDataSource> = MutableLiveData()
val navigateEvent: MutableLiveData<LiveEvent<String>> = MutableLiveData() val navigateEvent: MutableLiveData<LiveEvent<String>> = MutableLiveData()
var selectedReaction: String? = null var selectedReaction: String? = null
var eventId: String? = null var eventId: String? = null
val currentSection: MutableLiveData<Int> = MutableLiveData() val currentSection: MutableLiveData<Int> = MutableLiveData()
val moveToSection: MutableLiveData<Int> = MutableLiveData()
var reactionClickListener = object : ReactionClickListener { fun onReactionSelected(reaction: String) {
override fun onReactionSelected(reaction: String) {
selectedReaction = reaction selectedReaction = reaction
navigateEvent.value = LiveEvent(NAVIGATE_FINISH) navigateEvent.value = LiveEvent(NAVIGATE_FINISH)
} }
}
fun initWithContext(context: Context) { // Called by the Fragment, when the List is scrolled
// TODO load async fun setCurrentSection(section: Int) {
val emojiDataSource = EmojiDataSource(context)
emojiSourceLiveData.value = emojiDataSource
adapter = EmojiRecyclerAdapter(emojiDataSource, reactionClickListener)
adapter?.interactionListener = object : EmojiRecyclerAdapter.InteractionListener {
override fun firstVisibleSectionChange(section: Int) {
currentSection.value = section currentSection.value = section
} }
}
}
fun scrollToSection(sectionIndex: Int) { // Called by the Activity, when a tab item is clicked
adapter?.scrollToSection(sectionIndex) fun scrollToSection(section: Int) {
moveToSection.value = section
} }
companion object { companion object {

View File

@ -1,91 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.reactions
import android.content.Context
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import im.vector.riotx.R
class EmojiDataSource(val context: Context) {
var rawData: EmojiData? = null
init {
context.resources.openRawResource(R.raw.emoji_picker_datasource).use { input ->
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(EmojiData::class.java)
val inputAsString = input.bufferedReader().use { it.readText() }
this.rawData = jsonAdapter.fromJson(inputAsString)
// this.rawData = mb.fr(InputStreamReader(it), EmojiData::class.java)
}
}
@JsonClass(generateAdapter = true)
data class EmojiData(val categories: List<EmojiCategory>,
val emojis: Map<String, EmojiItem>,
val aliases: Map<String, String>)
@JsonClass(generateAdapter = true)
data class EmojiCategory(val id: String, val name: String, val emojis: List<String>)
@JsonClass(generateAdapter = true)
data class EmojiItem(
@Json(name = "a") val name: String,
@Json(name = "b") val unicode: String,
@Json(name = "j") val keywords: List<String>?,
val k: List<String>?) {
var _emojiText: String? = null
fun emojiString() : String {
if (_emojiText == null) {
val utf8Text = unicode.split("-").joinToString("") { "\\u$it" } // "\u0048\u0065\u006C\u006C\u006F World"
_emojiText = fromUnicode(utf8Text)
}
return _emojiText!!
}
}
companion object {
fun fromUnicode(unicode: String): String {
val str = unicode.replace("\\", "")
val arr = str.split("u".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val text = StringBuffer()
for (i in 1 until arr.size) {
val hexVal = Integer.parseInt(arr[i], 16)
text.append(Character.toChars(hexVal))
}
return text.toString()
}
}
// name: 'a',
// unified: 'b',
// non_qualified: 'c',
// has_img_apple: 'd',
// has_img_google: 'e',
// has_img_twitter: 'f',
// has_img_emojione: 'g',
// has_img_facebook: 'h',
// has_img_messenger: 'i',
// keywords: 'j',
// sheet: 'k',
// emoticons: 'l',
// text: 'm',
// short_names: 'n',
// added_in: 'o',
}

View File

@ -35,6 +35,7 @@ import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.reactions.data.EmojiDataSource
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.activity_emoji_reaction_picker.* import kotlinx.android.synthetic.main.activity_emoji_reaction_picker.*
import timber.log.Timber import timber.log.Timber
@ -44,7 +45,6 @@ import javax.inject.Inject
/** /**
* *
* TODO: Loading indicator while getting emoji data source? * TODO: Loading indicator while getting emoji data source?
* TODO: migrate to MvRx
* TODO: Finish Refactor to vector base activity * TODO: Finish Refactor to vector base activity
*/ */
class EmojiReactionPickerActivity : VectorBaseActivity(), class EmojiReactionPickerActivity : VectorBaseActivity(),
@ -54,13 +54,15 @@ class EmojiReactionPickerActivity : VectorBaseActivity(),
lateinit var viewModel: EmojiChooserViewModel lateinit var viewModel: EmojiChooserViewModel
override fun getMenuRes(): Int = R.menu.menu_emoji_reaction_picker override fun getMenuRes() = R.menu.menu_emoji_reaction_picker
override fun getLayoutRes(): Int = R.layout.activity_emoji_reaction_picker override fun getLayoutRes() = R.layout.activity_emoji_reaction_picker
override fun getTitleRes(): Int = R.string.title_activity_emoji_reaction_picker override fun getTitleRes() = R.string.title_activity_emoji_reaction_picker
@Inject lateinit var emojiSearchResultViewModelFactory: EmojiSearchResultViewModel.Factory
@Inject lateinit var emojiCompatFontProvider: EmojiCompatFontProvider @Inject lateinit var emojiCompatFontProvider: EmojiCompatFontProvider
@Inject lateinit var emojiDataSource: EmojiDataSource
private val searchResultViewModel: EmojiSearchResultViewModel by viewModel() private val searchResultViewModel: EmojiSearchResultViewModel by viewModel()
@ -93,13 +95,11 @@ class EmojiReactionPickerActivity : VectorBaseActivity(),
viewModel.eventId = intent.getStringExtra(EXTRA_EVENT_ID) viewModel.eventId = intent.getStringExtra(EXTRA_EVENT_ID)
viewModel.emojiSourceLiveData.observe(this, Observer { emojiDataSource.rawData.categories.forEach { category ->
it.rawData?.categories?.let { categories ->
for (category in categories) {
val s = category.emojis[0] val s = category.emojis[0]
tabLayout.newTab() tabLayout.newTab()
.also { tab -> .also { tab ->
tab.text = it.rawData!!.emojis[s]!!.emojiString() tab.text = emojiDataSource.rawData.emojis[s]!!.emoji
tab.contentDescription = category.name tab.contentDescription = category.name
} }
.also { tab -> .also { tab ->
@ -107,8 +107,6 @@ class EmojiReactionPickerActivity : VectorBaseActivity(),
} }
} }
tabLayout.addOnTabSelectedListener(tabLayoutSelectionListener) tabLayout.addOnTabSelectedListener(tabLayoutSelectionListener)
}
})
viewModel.currentSection.observe(this, Observer { section -> viewModel.currentSection.observe(this, Observer { section ->
section?.let { section?.let {
@ -136,7 +134,6 @@ class EmojiReactionPickerActivity : VectorBaseActivity(),
override fun compatibilityFontUpdate(typeface: Typeface?) { override fun compatibilityFontUpdate(typeface: Typeface?) {
EmojiDrawView.configureTextPaint(this, typeface) EmojiDrawView.configureTextPaint(this, typeface)
searchResultViewModel.dataSource
} }
override fun onDestroy() { override fun onDestroy() {
@ -206,13 +203,19 @@ class EmojiReactionPickerActivity : VectorBaseActivity(),
companion object { companion object {
const val EXTRA_EVENT_ID = "EXTRA_EVENT_ID" private const val EXTRA_EVENT_ID = "EXTRA_EVENT_ID"
const val EXTRA_REACTION_RESULT = "EXTRA_REACTION_RESULT" private const val EXTRA_REACTION_RESULT = "EXTRA_REACTION_RESULT"
fun intent(context: Context, eventId: String): Intent { fun intent(context: Context, eventId: String): Intent {
val intent = Intent(context, EmojiReactionPickerActivity::class.java) val intent = Intent(context, EmojiReactionPickerActivity::class.java)
intent.putExtra(EXTRA_EVENT_ID, eventId) intent.putExtra(EXTRA_EVENT_ID, eventId)
return intent return intent
} }
fun getOutput(data: Intent): Pair<String, String>? {
val eventId = data.getStringExtra(EXTRA_EVENT_ID) ?: return null
val reaction = data.getStringExtra(EXTRA_REACTION_RESULT) ?: return null
return eventId to reaction
}
} }
} }

View File

@ -30,22 +30,25 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.transition.AutoTransition import androidx.transition.AutoTransition
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.features.reactions.data.EmojiDataSource
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.math.abs import kotlin.math.abs
/** /**
* *
* TODO: Configure Span using available width and emoji size * TODO: Configure Span using available width and emoji size
* TODO: Search
* TODO: Performances * TODO: Performances
* TODO: Scroll to section - Find a way to snap section to the top * TODO: Scroll to section - Find a way to snap section to the top
*/ */
class EmojiRecyclerAdapter(private val dataSource: EmojiDataSource? = null, class EmojiRecyclerAdapter @Inject constructor(
private var reactionClickListener: ReactionClickListener?) : private val dataSource: EmojiDataSource
) :
RecyclerView.Adapter<EmojiRecyclerAdapter.ViewHolder>() { RecyclerView.Adapter<EmojiRecyclerAdapter.ViewHolder>() {
var reactionClickListener: ReactionClickListener? = null
var interactionListener: InteractionListener? = null var interactionListener: InteractionListener? = null
private var mRecyclerView: RecyclerView? = null private var mRecyclerView: RecyclerView? = null
@ -66,13 +69,12 @@ class EmojiRecyclerAdapter(private val dataSource: EmojiDataSource? = null,
private val itemClickListener = View.OnClickListener { view -> private val itemClickListener = View.OnClickListener { view ->
mRecyclerView?.getChildLayoutPosition(view)?.let { itemPosition -> mRecyclerView?.getChildLayoutPosition(view)?.let { itemPosition ->
if (itemPosition != RecyclerView.NO_POSITION) { if (itemPosition != RecyclerView.NO_POSITION) {
val categories = dataSource?.rawData?.categories ?: return@OnClickListener
val sectionNumber = getSectionForAbsoluteIndex(itemPosition) val sectionNumber = getSectionForAbsoluteIndex(itemPosition)
if (!isSection(itemPosition)) { if (!isSection(itemPosition)) {
val sectionMojis = categories[sectionNumber].emojis val sectionMojis = dataSource.rawData.categories[sectionNumber].emojis
val sectionOffset = getSectionOffset(sectionNumber) val sectionOffset = getSectionOffset(sectionNumber)
val emoji = sectionMojis[itemPosition - sectionOffset] val emoji = sectionMojis[itemPosition - sectionOffset]
val item = dataSource.rawData!!.emojis.getValue(emoji).emojiString() val item = dataSource.rawData.emojis.getValue(emoji).emoji
reactionClickListener?.onReactionSelected(item) reactionClickListener?.onReactionSelected(item)
} }
} }
@ -113,7 +115,7 @@ class EmojiRecyclerAdapter(private val dataSource: EmojiDataSource? = null,
} }
fun scrollToSection(section: Int) { fun scrollToSection(section: Int) {
if (section < 0 || section >= dataSource?.rawData?.categories?.size ?: 0) { if (section < 0 || section >= dataSource.rawData.categories.size) {
// ignore // ignore
return return
} }
@ -145,15 +147,13 @@ class EmojiRecyclerAdapter(private val dataSource: EmojiDataSource? = null,
} }
private fun isSection(position: Int): Boolean { private fun isSection(position: Int): Boolean {
dataSource?.rawData?.categories?.let { categories ->
var sectionOffset = 1 var sectionOffset = 1
var lastItemInSection: Int var lastItemInSection: Int
for (category in categories) { dataSource.rawData.categories.forEach { category ->
lastItemInSection = sectionOffset + category.emojis.size - 1 lastItemInSection = sectionOffset + category.emojis.size - 1
if (position == sectionOffset - 1) return true if (position == sectionOffset - 1) return true
sectionOffset = lastItemInSection + 2 sectionOffset = lastItemInSection + 2
} }
}
return false return false
} }
@ -161,14 +161,12 @@ class EmojiRecyclerAdapter(private val dataSource: EmojiDataSource? = null,
var sectionOffset = 1 var sectionOffset = 1
var lastItemInSection: Int var lastItemInSection: Int
var index = 0 var index = 0
dataSource?.rawData?.categories?.let { dataSource.rawData.categories.forEach { category ->
for (category in it) {
lastItemInSection = sectionOffset + category.emojis.size - 1 lastItemInSection = sectionOffset + category.emojis.size - 1
if (position <= lastItemInSection) return index if (position <= lastItemInSection) return index
sectionOffset = lastItemInSection + 2 sectionOffset = lastItemInSection + 2
index++ index++
} }
}
return index return index
} }
@ -176,27 +174,24 @@ class EmojiRecyclerAdapter(private val dataSource: EmojiDataSource? = null,
// Todo cache this for fast access // Todo cache this for fast access
var sectionOffset = 1 var sectionOffset = 1
var lastItemInSection: Int var lastItemInSection: Int
dataSource?.rawData?.categories?.let { dataSource.rawData.categories.forEachIndexed { index, category ->
for ((index, category) in it.withIndex()) {
lastItemInSection = sectionOffset + category.emojis.size - 1 lastItemInSection = sectionOffset + category.emojis.size - 1
if (section == index) return sectionOffset if (section == index) return sectionOffset
sectionOffset = lastItemInSection + 2 sectionOffset = lastItemInSection + 2
} }
}
return sectionOffset return sectionOffset
} }
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
beginTraceSession("MyAdapter.onBindViewHolder") beginTraceSession("MyAdapter.onBindViewHolder")
dataSource?.rawData?.categories?.let { categories ->
val sectionNumber = getSectionForAbsoluteIndex(position) val sectionNumber = getSectionForAbsoluteIndex(position)
if (isSection(position)) { if (isSection(position)) {
holder.bind(categories[sectionNumber].name) holder.bind(dataSource.rawData.categories[sectionNumber].name)
} else { } else {
val sectionMojis = categories[sectionNumber].emojis val sectionMojis = dataSource.rawData.categories[sectionNumber].emojis
val sectionOffset = getSectionOffset(sectionNumber) val sectionOffset = getSectionOffset(sectionNumber)
val emoji = sectionMojis[position - sectionOffset] val emoji = sectionMojis[position - sectionOffset]
val item = dataSource.rawData!!.emojis[emoji]!!.emojiString() val item = dataSource.rawData.emojis[emoji]!!.emoji
(holder as EmojiViewHolder).data = item (holder as EmojiViewHolder).data = item
if (scrollState != ScrollState.SETTLING || !isFastScroll) { if (scrollState != ScrollState.SETTLING || !isFastScroll) {
// Log.i("PERF","Bind with draw at position:$position") // Log.i("PERF","Bind with draw at position:$position")
@ -207,7 +202,6 @@ class EmojiRecyclerAdapter(private val dataSource: EmojiDataSource? = null,
holder.bind(null) holder.bind(null)
} }
} }
}
endTraceSession() endTraceSession()
} }
@ -226,15 +220,8 @@ class EmojiRecyclerAdapter(private val dataSource: EmojiDataSource? = null,
super.onViewRecycled(holder) super.onViewRecycled(holder)
} }
override fun getItemCount(): Int { override fun getItemCount() = dataSource.rawData.categories
return dataSource?.rawData?.categories?.let { .sumBy { emojiCategory -> 1 /* Section */ + emojiCategory.emojis.size }
var count = /*number of sections*/ it.size
for (ad in it) {
count += ad.emojis.size
}
count
} ?: 0
}
abstract class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { abstract class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
abstract fun bind(s: String?) abstract fun bind(s: String?)

View File

@ -24,9 +24,10 @@ import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.ui.list.genericFooterItem import im.vector.riotx.core.ui.list.genericFooterItem
import javax.inject.Inject import javax.inject.Inject
class EmojiSearchResultController @Inject constructor(val stringProvider: StringProvider, class EmojiSearchResultController @Inject constructor(
private val fontProvider: EmojiCompatFontProvider) private val stringProvider: StringProvider,
: TypedEpoxyController<EmojiSearchResultViewState>() { private val fontProvider: EmojiCompatFontProvider
) : TypedEpoxyController<EmojiSearchResultViewState>() {
var emojiTypeface: Typeface? = fontProvider.typeface var emojiTypeface: Typeface? = fontProvider.typeface

View File

@ -17,43 +17,42 @@ package im.vector.riotx.features.reactions
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.core.utils.LiveEvent
import kotlinx.android.synthetic.main.fragment_generic_recycler_epoxy.* import kotlinx.android.synthetic.main.fragment_generic_recycler.*
import javax.inject.Inject import javax.inject.Inject
class EmojiSearchResultFragment @Inject constructor( class EmojiSearchResultFragment @Inject constructor(
private val epoxyController: EmojiSearchResultController private val epoxyController: EmojiSearchResultController
) : VectorBaseFragment() { ) : VectorBaseFragment(), ReactionClickListener {
override fun getLayoutResId(): Int = R.layout.fragment_generic_recycler_epoxy override fun getLayoutResId() = R.layout.fragment_generic_recycler
val viewModel: EmojiSearchResultViewModel by activityViewModel() private val viewModel: EmojiSearchResultViewModel by activityViewModel()
var sharedViewModel: EmojiChooserViewModel? = null private lateinit var sharedViewModel: EmojiChooserViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
sharedViewModel = activityViewModelProvider.get(EmojiChooserViewModel::class.java) sharedViewModel = activityViewModelProvider.get(EmojiChooserViewModel::class.java)
epoxyController.listener = this
recyclerView.configureWith(epoxyController, showDivider = true)
}
override fun onDestroyView() {
epoxyController.listener = null
recyclerView.cleanup()
super.onDestroyView()
}
epoxyController.listener = object : ReactionClickListener {
override fun onReactionSelected(reaction: String) { override fun onReactionSelected(reaction: String) {
sharedViewModel?.selectedReaction = reaction sharedViewModel.selectedReaction = reaction
sharedViewModel?.navigateEvent?.value = LiveEvent(EmojiChooserViewModel.NAVIGATE_FINISH) sharedViewModel.navigateEvent.value = LiveEvent(EmojiChooserViewModel.NAVIGATE_FINISH)
}
}
val lmgr = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
epoxyRecyclerView.layoutManager = lmgr
val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context, lmgr.orientation)
epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
epoxyRecyclerView.setController(epoxyController)
} }
override fun invalidate() = withState(viewModel) { state -> override fun invalidate() = withState(viewModel) { state ->

View File

@ -22,12 +22,14 @@ import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder import com.airbnb.epoxy.EpoxyModelWithHolder
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.features.reactions.data.EmojiItem
@EpoxyModelClass(layout = R.layout.item_emoji_result) @EpoxyModelClass(layout = R.layout.item_emoji_result)
abstract class EmojiSearchResultItem : EpoxyModelWithHolder<EmojiSearchResultItem.Holder>() { abstract class EmojiSearchResultItem : EpoxyModelWithHolder<EmojiSearchResultItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
lateinit var emojiItem: EmojiDataSource.EmojiItem lateinit var emojiItem: EmojiItem
@EpoxyAttribute @EpoxyAttribute
var currentQuery: String? = null var currentQuery: String? = null
@ -41,12 +43,12 @@ abstract class EmojiSearchResultItem : EpoxyModelWithHolder<EmojiSearchResultIte
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
// TODO use query string to highlight the matched query in name and keywords? // TODO use query string to highlight the matched query in name and keywords?
holder.emojiText.text = emojiItem.emojiString() holder.emojiText.text = emojiItem.emoji
holder.emojiText.typeface = emojiTypeFace ?: Typeface.DEFAULT holder.emojiText.typeface = emojiTypeFace ?: Typeface.DEFAULT
holder.emojiNameText.text = emojiItem.name holder.emojiNameText.text = emojiItem.name
holder.emojiKeywordText.text = emojiItem.keywords?.joinToString(", ") holder.emojiKeywordText.setTextOrHide(emojiItem.keywords.joinToString())
holder.view.setOnClickListener { holder.view.setOnClickListener {
onClickListener?.onReactionSelected(emojiItem.emojiString()) onClickListener?.onReactionSelected(emojiItem.emoji)
} }
} }

View File

@ -15,19 +15,39 @@
*/ */
package im.vector.riotx.features.reactions package im.vector.riotx.features.reactions
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.reactions.data.EmojiDataSource
import im.vector.riotx.features.reactions.data.EmojiItem
data class EmojiSearchResultViewState( data class EmojiSearchResultViewState(
val query: String = "", val query: String = "",
val results: List<EmojiDataSource.EmojiItem> = emptyList() val results: List<EmojiItem> = emptyList()
) : MvRxState ) : MvRxState
class EmojiSearchResultViewModel(val dataSource: EmojiDataSource, initialState: EmojiSearchResultViewState) class EmojiSearchResultViewModel @AssistedInject constructor(
@Assisted initialState: EmojiSearchResultViewState,
private val dataSource: EmojiDataSource)
: VectorViewModel<EmojiSearchResultViewState, EmojiSearchAction>(initialState) { : VectorViewModel<EmojiSearchResultViewState, EmojiSearchAction>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: EmojiSearchResultViewState): EmojiSearchResultViewModel
}
companion object : MvRxViewModelFactory<EmojiSearchResultViewModel, EmojiSearchResultViewState> {
override fun create(viewModelContext: ViewModelContext, state: EmojiSearchResultViewState): EmojiSearchResultViewModel? {
val activity: EmojiReactionPickerActivity = (viewModelContext as ActivityViewModelContext).activity()
return activity.emojiSearchResultViewModelFactory.create(state)
}
}
override fun handle(action: EmojiSearchAction) { override fun handle(action: EmojiSearchAction) {
when (action) { when (action) {
is EmojiSearchAction.UpdateQuery -> updateQuery(action) is EmojiSearchAction.UpdateQuery -> updateQuery(action)
@ -35,26 +55,27 @@ class EmojiSearchResultViewModel(val dataSource: EmojiDataSource, initialState:
} }
private fun updateQuery(action: EmojiSearchAction.UpdateQuery) { private fun updateQuery(action: EmojiSearchAction.UpdateQuery) {
val words = action.queryString.split("\\s".toRegex())
setState { setState {
copy( copy(
query = action.queryString, query = action.queryString,
results = dataSource.rawData?.emojis?.toList() // First add emojis with name matching query, sorted by name
?.map { it.second } // Then emojis with keyword matching any of the word in the query, sorted by name
?.filter { results = dataSource.rawData.emojis
it.name.contains(action.queryString, true) .values
|| action.queryString.split("\\s".toRegex()).fold(true, { prev, q -> .filter { emojiItem ->
prev && (it.keywords?.any { it.contains(q, true) } ?: false) emojiItem.name.contains(action.queryString, true)
}
.sortedBy { it.name }
+ dataSource.rawData.emojis
.values
.filter { emojiItem ->
words.fold(true, { prev, word ->
prev && emojiItem.keywords.any { keyword -> keyword.contains(word, true) }
}) })
} ?: emptyList() }
.sortedBy { it.name }
) )
} }
} }
companion object : MvRxViewModelFactory<EmojiSearchResultViewModel, EmojiSearchResultViewState> {
override fun create(viewModelContext: ViewModelContext, state: EmojiSearchResultViewState): EmojiSearchResultViewModel? {
// TODO get the data source from activity? share it with other fragment
return EmojiSearchResultViewModel(EmojiDataSource(viewModelContext.activity), state)
}
}
} }

View File

@ -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.riotx.features.reactions.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class EmojiCategory(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String,
@Json(name = "emojis") val emojis: List<String>
)

View File

@ -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.riotx.features.reactions.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class EmojiData(
@Json(name = "categories") val categories: List<EmojiCategory>,
@Json(name = "emojis") val emojis: Map<String, EmojiItem>,
@Json(name = "aliases") val aliases: Map<String, String>
)

View File

@ -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.riotx.features.reactions.data
import android.content.res.Resources
import com.squareup.moshi.Moshi
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenScope
import javax.inject.Inject
@ScreenScope
class EmojiDataSource @Inject constructor(
resources: Resources
) {
val rawData = resources.openRawResource(R.raw.emoji_picker_datasource)
.use { input ->
Moshi.Builder()
.build()
.adapter(EmojiData::class.java)
.fromJson(input.bufferedReader().use { it.readText() })
}
?: EmojiData(emptyList(), emptyMap(), emptyMap())
}

View File

@ -0,0 +1,74 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.reactions.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* name: 'a',
* unified: 'b',
* non_qualified: 'c',
* has_img_apple: 'd',
* has_img_google: 'e',
* has_img_twitter: 'f',
* has_img_emojione: 'g',
* has_img_facebook: 'h',
* has_img_messenger: 'i',
* keywords: 'j',
* sheet: 'k',
* emoticons: 'l',
* text: 'm',
* short_names: 'n',
* added_in: 'o'
*/
@JsonClass(generateAdapter = true)
data class EmojiItem(
@Json(name = "a") val name: String,
@Json(name = "b") val unicode: String,
@Json(name = "j") val keywords: List<String> = emptyList()
) {
// Cannot be private...
var cache: String? = null
val emoji: String
get() {
cache?.let { return it }
// "\u0048\u0065\u006C\u006C\u006F World"
val utf8Text = unicode
.split("-")
.joinToString("") { "\\u$it" }
return fromUnicode(utf8Text)
.also { cache = it }
}
companion object {
private fun fromUnicode(unicode: String): String {
val arr = unicode
.replace("\\", "")
.split("u".toRegex())
.dropLastWhile { it.isEmpty() }
return buildString {
for (i in 1 until arr.size) {
val hexVal = Integer.parseInt(arr[i], 16)
append(Character.toChars(hexVal))
}
}
}
}
}

View File

@ -19,7 +19,6 @@ package im.vector.riotx.features.roomdirectory
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
@ -28,6 +27,8 @@ import com.jakewharton.rxbinding3.appcompat.queryTextChanges
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import io.reactivex.rxkotlin.subscribeBy import io.reactivex.rxkotlin.subscribeBy
@ -62,6 +63,9 @@ class PublicRoomsFragment @Inject constructor(
it.setDisplayHomeAsUpEnabled(true) it.setDisplayHomeAsUpEnabled(true)
} }
sharedActionViewModel = activityViewModelProvider.get(RoomDirectorySharedActionViewModel::class.java)
setupRecyclerView()
publicRoomsFilter.queryTextChanges() publicRoomsFilter.queryTextChanges()
.debounce(500, TimeUnit.MILLISECONDS) .debounce(500, TimeUnit.MILLISECONDS)
.subscribeBy { .subscribeBy {
@ -79,6 +83,12 @@ class PublicRoomsFragment @Inject constructor(
} }
} }
override fun onDestroyView() {
publicRoomsController.callback = null
publicRoomsList.cleanup()
super.onDestroyView()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) { return when (item.itemId) {
R.id.menu_room_directory_change_protocol -> { R.id.menu_room_directory_change_protocol -> {
@ -90,22 +100,11 @@ class PublicRoomsFragment @Inject constructor(
} }
} }
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(RoomDirectorySharedActionViewModel::class.java)
setupRecyclerView()
}
private fun setupRecyclerView() { private fun setupRecyclerView() {
val epoxyVisibilityTracker = EpoxyVisibilityTracker() val epoxyVisibilityTracker = EpoxyVisibilityTracker()
epoxyVisibilityTracker.attach(publicRoomsList) epoxyVisibilityTracker.attach(publicRoomsList)
publicRoomsList.configureWith(publicRoomsController)
val layoutManager = LinearLayoutManager(context)
publicRoomsList.layoutManager = layoutManager
publicRoomsController.callback = this publicRoomsController.callback = this
publicRoomsList.setController(publicRoomsController)
} }
override fun onPublicRoomClicked(publicRoom: PublicRoom, joinState: JoinState) { override fun onPublicRoomClicked(publicRoom: PublicRoom, joinState: JoinState) {

View File

@ -19,11 +19,12 @@ package im.vector.riotx.features.roomdirectory.createroom
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.roomdirectory.RoomDirectorySharedAction import im.vector.riotx.features.roomdirectory.RoomDirectorySharedAction
import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel
@ -50,6 +51,12 @@ class CreateRoomFragment @Inject constructor(private val createRoomController: C
} }
} }
override fun onDestroyView() {
createRoomForm.cleanup()
createRoomController.listener = null
super.onDestroyView()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) { return when (item.itemId) {
R.id.action_create_room -> { R.id.action_create_room -> {
@ -62,12 +69,8 @@ class CreateRoomFragment @Inject constructor(private val createRoomController: C
} }
private fun setupRecyclerView() { private fun setupRecyclerView() {
val layoutManager = LinearLayoutManager(context) createRoomForm.configureWith(createRoomController)
createRoomForm.layoutManager = layoutManager
createRoomController.listener = this createRoomController.listener = this
createRoomForm.setController(createRoomController)
} }
override fun onNameChange(newName: String) { override fun onNameChange(newName: String) {

View File

@ -19,12 +19,13 @@ package im.vector.riotx.features.roomdirectory.picker
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.roomdirectory.RoomDirectoryAction import im.vector.riotx.features.roomdirectory.RoomDirectoryAction
import im.vector.riotx.features.roomdirectory.RoomDirectorySharedAction import im.vector.riotx.features.roomdirectory.RoomDirectorySharedAction
@ -60,6 +61,12 @@ class RoomDirectoryPickerFragment @Inject constructor(val roomDirectoryPickerVie
setupRecyclerView() setupRecyclerView()
} }
override fun onDestroyView() {
roomDirectoryPickerList.cleanup()
roomDirectoryPickerController.callback = null
super.onDestroyView()
}
override fun getMenuRes() = R.menu.menu_directory_server_picker override fun getMenuRes() = R.menu.menu_directory_server_picker
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -73,12 +80,8 @@ class RoomDirectoryPickerFragment @Inject constructor(val roomDirectoryPickerVie
} }
private fun setupRecyclerView() { private fun setupRecyclerView() {
val layoutManager = LinearLayoutManager(context) roomDirectoryPickerList.configureWith(roomDirectoryPickerController)
roomDirectoryPickerList.layoutManager = layoutManager
roomDirectoryPickerController.callback = this roomDirectoryPickerController.callback = this
roomDirectoryPickerList.setController(roomDirectoryPickerController)
} }
override fun onRoomDirectoryClicked(roomDirectoryData: RoomDirectoryData) { override fun onRoomDirectoryClicked(roomDirectoryData: RoomDirectoryData) {

View File

@ -29,6 +29,7 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
import butterknife.BindView import butterknife.BindView
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.BugReporter
@ -136,6 +137,11 @@ class VectorSettingsNotificationsTroubleshootFragment @Inject constructor(
testManager?.runDiagnostic() testManager?.runDiagnostic()
} }
override fun onDestroyView() {
mRecyclerView.cleanup()
super.onDestroyView()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK && requestCode == NotificationTroubleshootTestManager.REQ_CODE_FIX) { if (resultCode == Activity.RESULT_OK && requestCode == NotificationTroubleshootTestManager.REQ_CODE_FIX) {
testManager?.retry() testManager?.retry()

View File

@ -26,10 +26,12 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import kotlinx.android.synthetic.main.fragment_generic_recycler_epoxy.* import kotlinx.android.synthetic.main.fragment_generic_recycler.*
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
import javax.inject.Inject import javax.inject.Inject
@ -39,7 +41,7 @@ class VectorSettingsIgnoredUsersFragment @Inject constructor(
private val errorFormatter: ErrorFormatter private val errorFormatter: ErrorFormatter
) : VectorBaseFragment(), IgnoredUsersController.Callback { ) : VectorBaseFragment(), IgnoredUsersController.Callback {
override fun getLayoutResId() = R.layout.fragment_generic_recycler_epoxy override fun getLayoutResId() = R.layout.fragment_generic_recycler
private val ignoredUsersViewModel: IgnoredUsersViewModel by fragmentViewModel() private val ignoredUsersViewModel: IgnoredUsersViewModel by fragmentViewModel()
@ -49,12 +51,18 @@ class VectorSettingsIgnoredUsersFragment @Inject constructor(
waiting_view_status_text.setText(R.string.please_wait) waiting_view_status_text.setText(R.string.please_wait)
waiting_view_status_text.isVisible = true waiting_view_status_text.isVisible = true
ignoredUsersController.callback = this ignoredUsersController.callback = this
epoxyRecyclerView.setController(ignoredUsersController) recyclerView.configureWith(ignoredUsersController)
ignoredUsersViewModel.requestErrorLiveData.observeEvent(this) { ignoredUsersViewModel.requestErrorLiveData.observeEvent(this) {
displayErrorDialog(it) displayErrorDialog(it)
} }
} }
override fun onDestroyView() {
ignoredUsersController.callback = null
recyclerView.cleanup()
super.onDestroyView()
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()

View File

@ -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.riotx.features.settings.push
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.riotx.R
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.ui.list.genericFooterItem
import javax.inject.Inject
class PushGateWayController @Inject constructor(
private val stringProvider: StringProvider
) : TypedEpoxyController<PushGatewayViewState>() {
override fun buildModels(data: PushGatewayViewState?) {
data?.pushGateways?.invoke()?.let { pushers ->
if (pushers.isEmpty()) {
genericFooterItem {
id("footer")
text(stringProvider.getString(R.string.settings_push_gateway_no_pushers))
}
} else {
pushers.forEach {
pushGatewayItem {
id("${it.pushKey}_${it.appId}")
pusher(it)
}
}
}
} ?: run {
genericFooterItem {
id("loading")
text(stringProvider.getString(R.string.loading))
}
}
}
}

View File

@ -17,71 +17,43 @@
package im.vector.riotx.features.settings.push package im.vector.riotx.features.settings.push
import android.os.Bundle import android.os.Bundle
import androidx.recyclerview.widget.DividerItemDecoration import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.resources.StringProvider import kotlinx.android.synthetic.main.fragment_generic_recycler.*
import im.vector.riotx.core.ui.list.genericFooterItem
import kotlinx.android.synthetic.main.fragment_generic_recycler_epoxy.*
import javax.inject.Inject import javax.inject.Inject
// Referenced in vector_settings_notifications.xml // Referenced in vector_settings_notifications.xml
class PushGatewaysFragment @Inject constructor( class PushGatewaysFragment @Inject constructor(
val pushGatewaysViewModelFactory: PushGatewaysViewModel.Factory val pushGatewaysViewModelFactory: PushGatewaysViewModel.Factory,
private val epoxyController: PushGateWayController
) : VectorBaseFragment() { ) : VectorBaseFragment() {
override fun getLayoutResId(): Int = R.layout.fragment_generic_recycler_epoxy override fun getLayoutResId() = R.layout.fragment_generic_recycler
private val viewModel: PushGatewaysViewModel by fragmentViewModel(PushGatewaysViewModel::class) private val viewModel: PushGatewaysViewModel by fragmentViewModel(PushGatewaysViewModel::class)
private val epoxyController by lazy { PushGateWayController(StringProvider(requireContext().resources)) }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_notifications_targets) (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_notifications_targets)
} }
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onViewCreated(view, savedInstanceState)
val lmgr = LinearLayoutManager(context, RecyclerView.VERTICAL, false) recyclerView.configureWith(epoxyController, showDivider = true)
epoxyRecyclerView.layoutManager = lmgr }
val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
lmgr.orientation) override fun onDestroyView() {
epoxyRecyclerView.addItemDecoration(dividerItemDecoration) recyclerView.cleanup()
epoxyRecyclerView.setController(epoxyController) super.onDestroyView()
} }
override fun invalidate() = withState(viewModel) { state -> override fun invalidate() = withState(viewModel) { state ->
epoxyController.setData(state) epoxyController.setData(state)
} }
class PushGateWayController(private val stringProvider: StringProvider) : TypedEpoxyController<PushGatewayViewState>() {
override fun buildModels(data: PushGatewayViewState?) {
data?.pushGateways?.invoke()?.let { pushers ->
if (pushers.isEmpty()) {
genericFooterItem {
id("footer")
text(stringProvider.getString(R.string.settings_push_gateway_no_pushers))
}
} else {
pushers.forEach {
pushGatewayItem {
id("${it.pushKey}_${it.appId}")
pusher(it)
}
}
}
} ?: run {
genericFooterItem {
id("footer")
text(stringProvider.getString(R.string.loading))
}
}
}
}
} }

View File

@ -16,23 +16,23 @@
package im.vector.riotx.features.settings.push package im.vector.riotx.features.settings.push
import android.os.Bundle import android.os.Bundle
import androidx.recyclerview.widget.DividerItemDecoration import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.ui.list.genericFooterItem import im.vector.riotx.core.ui.list.genericFooterItem
import kotlinx.android.synthetic.main.fragment_generic_recycler_epoxy.* import kotlinx.android.synthetic.main.fragment_generic_recycler.*
// Referenced in vector_settings_notifications.xml // Referenced in vector_settings_notifications.xml
class PushRulesFragment : VectorBaseFragment() { class PushRulesFragment : VectorBaseFragment() {
override fun getLayoutResId(): Int = R.layout.fragment_generic_recycler_epoxy override fun getLayoutResId() = R.layout.fragment_generic_recycler
private val viewModel: PushRulesViewModel by fragmentViewModel(PushRulesViewModel::class) private val viewModel: PushRulesViewModel by fragmentViewModel(PushRulesViewModel::class)
@ -43,14 +43,14 @@ class PushRulesFragment : VectorBaseFragment() {
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_push_rules) (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_push_rules)
} }
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onViewCreated(view, savedInstanceState)
val lmgr = LinearLayoutManager(context, RecyclerView.VERTICAL, false) recyclerView.configureWith(epoxyController, showDivider = true)
epoxyRecyclerView.layoutManager = lmgr }
val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
lmgr.orientation) override fun onDestroyView() {
epoxyRecyclerView.addItemDecoration(dividerItemDecoration) recyclerView.cleanup()
epoxyRecyclerView.setController(epoxyController) super.onDestroyView()
} }
override fun invalidate() = withState(viewModel) { state -> override fun invalidate() = withState(viewModel) { state ->

View File

@ -50,9 +50,7 @@ class IncomingShareActivity :
return supportFragmentManager.findFragmentById(R.id.shareRoomListFragmentContainer) as? RoomListFragment return supportFragmentManager.findFragmentById(R.id.shareRoomListFragmentContainer) as? RoomListFragment
} }
override fun getLayoutRes(): Int { override fun getLayoutRes() = R.layout.activity_incoming_share
return R.layout.activity_incoming_share
}
override fun injectWith(injector: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {
injector.inject(this) injector.inject(this)

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" <androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/emoji_recycler_view" android:id="@+id/emojiRecyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:scrollbars="vertical" android:scrollbars="vertical"
@ -9,6 +9,4 @@
tools:itemCount="100" tools:itemCount="100"
tools:layoutManager="androidx.recyclerview.widget.GridLayoutManager" tools:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
tools:listitem="@layout/grid_item_emoji" tools:listitem="@layout/grid_item_emoji"
tools:spanCount="10"> tools:spanCount="10" />
</androidx.recyclerview.widget.RecyclerView>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<com.airbnb.epoxy.EpoxyRecyclerView xmlns:android="http://schemas.android.com/apk/res/android" <androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/breadcrumbsRecyclerView" android:id="@+id/breadcrumbsRecyclerView"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -122,7 +122,7 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/createDirectRoomFilterDivider" /> app:layout_constraintTop_toBottomOf="@id/createDirectRoomFilterDivider" />
<com.airbnb.epoxy.EpoxyRecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView" android:id="@+id/recyclerView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"

View File

@ -89,7 +89,7 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/createDirectRoomSearchByIdContainer" /> app:layout_constraintTop_toBottomOf="@+id/createDirectRoomSearchByIdContainer" />
<com.airbnb.epoxy.EpoxyRecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView" android:id="@+id/recyclerView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"

View File

@ -55,7 +55,7 @@
</androidx.appcompat.widget.Toolbar> </androidx.appcompat.widget.Toolbar>
<com.airbnb.epoxy.EpoxyRecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/createRoomForm" android:id="@+id/createRoomForm"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"

View File

@ -5,8 +5,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.airbnb.epoxy.EpoxyRecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/epoxyRecyclerView" android:id="@+id/recyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:itemSpacing="1dp" app:itemSpacing="1dp"

View File

@ -5,8 +5,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.airbnb.epoxy.EpoxyRecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/groupListEpoxyRecyclerView" android:id="@+id/groupListView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:overScrollMode="always" android:overScrollMode="always"

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<com.airbnb.epoxy.EpoxyRecyclerView xmlns:android="http://schemas.android.com/apk/res/android" <androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/keysBackupSettingsRecyclerView" android:id="@+id/keysBackupSettingsRecyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -38,7 +38,7 @@
android:text="@string/auth_accept_policies" android:text="@string/auth_accept_policies"
app:layout_constraintTop_toBottomOf="@+id/loginTermsTitle" /> app:layout_constraintTop_toBottomOf="@+id/loginTermsTitle" />
<com.airbnb.epoxy.EpoxyRecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/loginTermsPolicyList" android:id="@+id/loginTermsPolicyList"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"

View File

@ -7,7 +7,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.airbnb.epoxy.EpoxyRecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/publicRoomsList" android:id="@+id/publicRoomsList"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@ -21,7 +21,7 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<com.airbnb.epoxy.EpoxyRecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/roomDirectoryPickerList" android:id="@+id/roomDirectoryPickerList"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"

View File

@ -3,49 +3,50 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?riotx_background"
android:foreground="?attr/selectableItemBackground"
android:gravity="center_vertical"
android:minHeight="48dp"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingEnd="8dp" android:paddingStart="@dimen/layout_horizontal_margin"
android:paddingStart="8dp" android:paddingEnd="@dimen/layout_horizontal_margin">
android:minHeight="44dp">
<!-- size in dp, because we do not want the display to be impacted by font size setting --> <!-- size in dp, because we do not want the display to be impacted by font size setting -->
<TextView <TextView
android:id="@+id/item_emoji_tv" android:id="@+id/item_emoji_tv"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:textSize="25sp" android:textColor="@color/black"
android:textSize="25dp"
tools:ignore="SpUsage" tools:ignore="SpUsage"
android:textColor="?android:textColorPrimary"
tools:text="@sample/reactions.json/data/reaction" /> tools:text="@sample/reactions.json/data/reaction" />
<LinearLayout <LinearLayout
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:layout_weight="1"
android:layout_weight="1"> android:orientation="vertical">
<TextView <TextView
android:id="@+id/item_emoji_name" android:id="@+id/item_emoji_name"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:textColor="?riotx_text_primary"
android:textStyle="bold"
android:textSize="16sp" android:textSize="16sp"
android:textColor="?android:textColorPrimary" android:textStyle="bold"
tools:text="Smiley Face" /> tools:text="Smiley Face" />
<TextView <TextView
android:id="@+id/item_emoji_keyword" android:id="@+id/item_emoji_keyword"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center"
android:textSize="14sp"
android:maxLines="2" android:maxLines="2"
android:textColor="?android:textColorPrimary" android:textColor="?riotx_text_secondary"
tools:text="Smile, foo, bar" /> android:textSize="14sp"
android:visibility="gone"
tools:text="Smile, foo, bar"
tools:visibility="visible" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>