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 🐛:
- 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 🗣:
-
Build 🧱:
- "ban" event are not rendered correctly (#716)
-
Changes in RiotX 0.9.1 (2019-12-05)
===================================================

View File

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

View File

@ -50,6 +50,7 @@ interface RelationService {
/**
* 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 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) {
val failure = ContentUploadStateTracker.State.Failure(throwable)
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.timeline.TimelineEvent
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.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.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.di.UserId
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.configureWith
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 timber.log.Timber
@ -54,6 +58,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
private val cryptoService: CryptoService,
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
private val fetchEditHistoryTask: FetchEditHistoryTask,
private val timelineEventMapper: TimelineEventMapper,
private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor)
: RelationService {
@ -64,11 +69,27 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
}
override fun sendReaction(targetEventId: String, reaction: String): Cancelable {
val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction)
.also { saveLocalEcho(it) }
val sendRelationWork = createSendEventWork(event, true)
TimelineSendEventWorkCommon.postWork(context, roomId, sendRelationWork)
return CancelableWork(context, sendRelationWork.id)
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)
.also { saveLocalEcho(it) }
val sendRelationWork = createSendEventWork(event, true)
TimelineSendEventWorkCommon.postWork(context, roomId, sendRelationWork)
CancelableWork(context, sendRelationWork.id)
} else {
Timber.w("Reaction already added")
NoOpCancellable
}
}
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) {
if (listeners.contains(listener)) {
return false
}
listeners.add(listener).also {
postSnapshot()
}
@ -494,9 +497,9 @@ internal class DefaultTimeline(
return
}
val params = PaginationTask.Params(roomId = roomId,
from = token,
direction = direction.toPaginationDirection(),
limit = limit)
from = token,
direction = direction.toPaginationDirection(),
limit = limit)
Timber.v("Should fetch $limit items $direction")
cancelableBag += paginationTask
@ -571,7 +574,7 @@ internal class DefaultTimeline(
val timelineEvent = buildTimelineEvent(eventEntity)
if (timelineEvent.isEncrypted()
&& timelineEvent.root.mxDecryptionResult == null) {
&& timelineEvent.root.mxDecryptionResult == null) {
timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) }
}

View File

@ -81,4 +81,7 @@ layout_constraintLeft_
### Will crash on API < 21. Use ?colorAccent instead
\?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 im.vector.matrix.android.api.crypto.getAllVerificationEmojis
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() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.fragment_generic_recycler_epoxy)
setContentView(R.layout.fragment_generic_recycler)
val controller = SasEmojiController()
epoxyRecyclerView.setController(controller)
recyclerView.configureWith(controller)
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.VectorComponent
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.lifecycle.VectorActivityLifecycleCallbacks
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 timber.log.Timber
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.*
import javax.inject.Inject
class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.Provider, androidx.work.Configuration.Provider {
@ -79,14 +78,13 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
lateinit var vectorComponent: VectorComponent
private var fontThreadHandler: Handler? = null
// var slowMode = false
override fun onCreate() {
super.onCreate()
appContext = this
vectorComponent = DaggerVectorComponent.factory().create(this)
vectorComponent.inject(this)
vectorUncaughtExceptionHandler.activate(this)
setupRxPlugin()
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
@ -138,7 +136,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
})
ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler)
// This should be done as early as possible
initKnownEmojiHashSet(appContext)
// initKnownEmojiHashSet(appContext)
}
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.login.*
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.roomdirectory.PublicRoomsFragment
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment
@ -255,4 +256,9 @@ interface FragmentModule {
@IntoMap
@FragmentKey(BreadcrumbsFragment::class)
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 {
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 {
eventCallback?.onRetryClicked()
}

View File

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

View File

@ -135,6 +135,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
restorables.forEach { it.onSaveInstanceState(outState) }
restorables.clear()
}
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
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
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?" +
"|[\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA]\uFE0F?))")
/*
// A hashset from all supported emoji
private var knownEmojiSet: HashSet<String>? = null
@ -56,7 +50,7 @@ fun initKnownEmojiHashSet(context: Context, done: (() -> Unit)? = null) {
GlobalScope.launch {
context.resources.openRawResource(R.raw.emoji_picker_datasource).use { input ->
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 source = jsonAdapter.fromJson(inputAsString)
knownEmojiSet = HashSet<String>().also {
@ -77,6 +71,7 @@ fun isSingleEmoji(string: String): Boolean {
}
return knownEmojiSet?.contains(string) ?: false
}
*/
/**
* 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.withState
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.features.crypto.keysbackup.restore.KeysBackupRestoreActivity
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?) {
super.onViewCreated(view, savedInstanceState)
keysBackupSettingsRecyclerView.setController(keysBackupSettingsRecyclerViewController)
keysBackupSettingsRecyclerView.configureWith(keysBackupSettingsRecyclerViewController)
keysBackupSettingsRecyclerViewController.listener = this
}
override fun onDestroyView() {
keysBackupSettingsRecyclerViewController.listener = null
keysBackupSettingsRecyclerView.cleanup()
super.onDestroyView()
}
override fun invalidate() = withState(viewModel) { state ->
keysBackupSettingsRecyclerViewController.setData(state)
}

View File

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

View File

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

View File

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

View File

@ -23,11 +23,13 @@ import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel
import im.vector.matrix.android.api.session.group.model.GroupSummary
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.platform.StateView
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.HomeSharedActionViewModel
import kotlinx.android.synthetic.main.fragment_group_list.*
import javax.inject.Inject
@ -45,14 +47,20 @@ class GroupListFragment @Inject constructor(
super.onViewCreated(view, savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(HomeSharedActionViewModel::class.java)
groupController.callback = this
stateView.contentView = groupListEpoxyRecyclerView
groupListEpoxyRecyclerView.setController(groupController)
stateView.contentView = groupListView
groupListView.configureWith(groupController)
viewModel.subscribe { renderState(it) }
viewModel.openGroupLiveData.observeEvent(this) {
sharedActionViewModel.post(HomeActivitySharedAction.OpenGroup)
}
}
override fun onDestroyView() {
groupController.callback = null
groupListView.cleanup()
super.onDestroyView()
}
private fun renderState(state: GroupListViewState) {
when (state.asyncGroups) {
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.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.mvrx.fragmentViewModel
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.features.home.room.detail.RoomDetailSharedAction
import im.vector.riotx.features.home.room.detail.RoomDetailSharedActionViewModel
@ -46,16 +47,13 @@ class BreadcrumbsFragment @Inject constructor(
}
override fun onDestroyView() {
breadcrumbsRecyclerView.cleanup()
super.onDestroyView()
breadcrumbsRecyclerView.adapter = null
}
private fun setupRecyclerView() {
val layoutManager = LinearLayoutManager(context)
breadcrumbsRecyclerView.layoutManager = layoutManager
breadcrumbsRecyclerView.itemAnimator = BreadcrumbsAnimator()
breadcrumbsRecyclerView.configureWith(breadcrumbsController, BreadcrumbsAnimator(), hasFixedSize = false)
breadcrumbsController.listener = this
breadcrumbsRecyclerView.setController(breadcrumbsController)
}
private fun renderState(state: BreadcrumbsViewState) {

View File

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

View File

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

View File

@ -21,7 +21,6 @@ import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView
import butterknife.ButterKnife
@ -29,6 +28,8 @@ import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.args
import im.vector.riotx.R
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.features.home.room.detail.timeline.item.ReadReceiptData
import kotlinx.android.parcel.Parcelize
@ -52,8 +53,8 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
private val displayReadReceiptArgs: DisplayReadReceiptArgs by args()
override fun injectWith(screenComponent: ScreenComponent) {
screenComponent.inject(this)
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -64,12 +65,16 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
recyclerView.adapter = epoxyController.adapter
recyclerView.configureWith(epoxyController, hasFixedSize = false)
bottomSheetTitle.text = getString(R.string.seen_by)
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()
companion object {

View File

@ -25,7 +25,6 @@ import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.VisibilityState
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
@ -44,7 +43,7 @@ import org.threeten.bp.LocalDateTime
import javax.inject.Inject
class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter,
private val session: Session,
private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
private val timelineItemFactory: TimelineItemFactory,
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val mergedHeaderItemFactory: MergedHeaderItemFactory,
@ -209,6 +208,13 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
timelineMediaSizeProvider.recyclerView = recyclerView
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
timelineMediaSizeProvider.recyclerView = null
contentUploadStateTrackerBinder.clear()
timeline?.removeListener(this)
super.onDetachedFromRecyclerView(recyclerView)
}
override fun buildModels() {
val timestamp = System.currentTimeMillis()
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.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView
import butterknife.ButterKnife
@ -27,6 +26,8 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
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.features.home.room.detail.timeline.item.MessageInformationData
import javax.inject.Inject
@ -48,8 +49,8 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message
private lateinit var sharedActionViewModel: MessageSharedActionViewModel
override fun injectWith(screenComponent: ScreenComponent) {
screenComponent.inject(this)
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -61,13 +62,17 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java)
recyclerView.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false)
recyclerView.adapter = messageActionsEpoxyController.adapter
recyclerView.configureWith(messageActionsEpoxyController, hasFixedSize = false)
// Disable item animation
recyclerView.itemAnimator = null
messageActionsEpoxyController.listener = this
}
override fun onDestroyView() {
recyclerView.cleanup()
super.onDestroyView()
}
override fun onUrlClicked(url: String): Boolean {
sharedActionViewModel.post(EventSharedAction.OnUrlClicked(url))
// Always consume
@ -83,6 +88,10 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message
override fun didSelectMenuAction(eventAction: EventSharedAction) {
if (eventAction is EventSharedAction.ReportContent) {
// Toggle report menu
// Enable item animation
if (recyclerView.itemAnimator == null) {
recyclerView.itemAnimator = MessageActionsAnimator()
}
viewModel.handle(MessageActionsAction.ToggleReportMenu)
} else {
sharedActionViewModel.post(eventAction)

View File

@ -19,9 +19,6 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView
import butterknife.ButterKnife
@ -30,6 +27,8 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
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.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
@ -54,8 +53,8 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() {
ViewEditHistoryEpoxyController(requireContext(), viewModel.dateFormatter, eventHtmlRenderer)
}
override fun injectWith(screenComponent: ScreenComponent) {
screenComponent.inject(this)
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -66,13 +65,18 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
recyclerView.adapter = epoxyController.adapter
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
val dividerItemDecoration = DividerItemDecoration(requireContext(), LinearLayout.VERTICAL)
recyclerView.addItemDecoration(dividerItemDecoration)
recyclerView.configureWith(
epoxyController,
showDivider = true,
hasFixedSize = false)
bottomSheetTitle.text = context?.getString(R.string.message_edits)
}
override fun onDestroyView() {
recyclerView.cleanup()
super.onDestroyView()
}
override fun invalidate() = withState(viewModel) {
epoxyController.setData(it)
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.util.ContentUtils.extractUsefulTextFromReply
import im.vector.riotx.R
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.core.extensions.localDateTime
import im.vector.riotx.core.ui.list.genericFooterItem
import im.vector.riotx.core.ui.list.genericItem
import im.vector.riotx.core.ui.list.genericItemHeader
import im.vector.riotx.core.ui.list.genericLoaderItem
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.features.html.EventHtmlRenderer
import me.gujun.android.span.span
import name.fraser.neil.plaintext.diff_match_patch
import timber.log.Timber
import java.util.Calendar
import java.util.*
/**
* Epoxy controller for edit history list
@ -104,9 +103,7 @@ class ViewEditHistoryEpoxyController(private val context: Context,
?: nContent.first
val dmp = diff_match_patch()
val diff = dmp.diff_main(nextBody.toString(), body.toString())
Timber.e("#### Diff: $diff")
dmp.diff_cleanupSemantic(diff)
Timber.e("#### Diff: $diff")
spannedDiff = span {
diff.map {
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.riotx.R
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.resources.ColorProvider
import im.vector.riotx.core.utils.TextUtils
import im.vector.riotx.features.ui.getMessageTextColor
import javax.inject.Inject
@ScreenScope
class ContentUploadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
private val colorProvider: ColorProvider,
private val errorFormatter: ErrorFormatter) {
@ -40,7 +42,7 @@ class ContentUploadStateTrackerBinder @Inject constructor(private val activeSess
fun bind(eventId: String,
isLocalFile: Boolean,
progressLayout: ViewGroup) {
activeSessionHolder.getActiveSession().also { session ->
activeSessionHolder.getSafeActiveSession()?.also { session ->
val uploadStateTracker = session.contentUploadProgressTracker()
val updateListener = ContentMediaProgressUpdater(progressLayout, isLocalFile, colorProvider, errorFormatter)
updateListeners[eventId] = updateListener
@ -49,13 +51,19 @@ class ContentUploadStateTrackerBinder @Inject constructor(private val activeSess
}
fun unbind(eventId: String) {
activeSessionHolder.getActiveSession().also { session ->
activeSessionHolder.getSafeActiveSession()?.also { session ->
val uploadStateTracker = session.contentUploadProgressTracker()
updateListeners[eventId]?.also {
uploadStateTracker.untrack(eventId, it)
}
}
}
fun clear() {
activeSessionHolder.getSafeActiveSession()?.also {
it.contentUploadProgressTracker().clear()
}
}
}
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 im.vector.riotx.core.di.ScreenScope
import javax.inject.Inject
import kotlin.math.roundToInt
@ScreenScope
class TimelineMediaSizeProvider @Inject constructor() {
lateinit var recyclerView: RecyclerView
var recyclerView: RecyclerView? = null
private var cachedSize: Pair<Int, Int>? = null
fun getMaxSize(): Pair<Int, Int> {
@ -31,17 +32,17 @@ class TimelineMediaSizeProvider @Inject constructor() {
}
private fun computeMaxSize(): Pair<Int, Int> {
val width = recyclerView.width
val height = recyclerView.height
val width = recyclerView?.width ?: 0
val height = recyclerView?.height ?: 0
val maxImageWidth: Int
val maxImageHeight: Int
// landscape / portrait
if (width < height) {
maxImageWidth = Math.round(width * 0.7f)
maxImageHeight = Math.round(height * 0.5f)
maxImageWidth = (width * 0.7f).roundToInt()
maxImageHeight = (height * 0.5f).roundToInt()
} else {
maxImageWidth = Math.round(width * 0.5f)
maxImageHeight = Math.round(height * 0.7f)
maxImageWidth = (width * 0.5f).roundToInt()
maxImageHeight = (height * 0.7f).roundToInt()
}
return Pair(maxImageWidth, maxImageHeight)
}

View File

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

View File

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

View File

@ -26,6 +26,7 @@ import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.mvrx.*
import com.google.android.material.snackbar.Snackbar
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.core.epoxy.LayoutManagerStateRestorer
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.StateView
import im.vector.riotx.core.platform.VectorBaseFragment
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.RoomListQuickActionsSharedAction
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.notifications.NotificationDrawerManager
@ -65,6 +66,7 @@ class RoomListFragment @Inject constructor(
) : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener {
private var modelBuildListener: OnModelBuildFinishedListener? = null
private lateinit var sharedActionViewModel: RoomListQuickActionsSharedActionViewModel
private val roomListParams: RoomListParams by args()
private val roomListViewModel: RoomListViewModel by fragmentViewModel()
@ -118,8 +120,12 @@ class RoomListFragment @Inject constructor(
}
override fun onDestroyView() {
roomController.removeModelBuildListener(modelBuildListener)
modelBuildListener = null
roomListView.cleanup()
roomController.listener = null
createChatFabMenu.listener = null
super.onDestroyView()
roomListView.adapter = null
}
private fun openSelectedRoom(event: RoomListViewEvents.SelectRoom) {
@ -198,7 +204,8 @@ class RoomListFragment @Inject constructor(
roomListView.layoutManager = layoutManager
roomListView.itemAnimator = RoomListAnimator()
roomController.listener = this
roomController.addModelBuildListener { it.dispatchTo(stateRestorer) }
modelBuildListener = OnModelBuildFinishedListener { it.dispatchTo(stateRestorer) }
roomController.addModelBuildListener(modelBuildListener)
roomListView.adapter = roomController.adapter
stateView.contentView = roomListView
}

View File

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

View File

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

View File

@ -17,12 +17,18 @@ package im.vector.riotx.features.reactions
import android.os.Bundle
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.lifecycle.observe
import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.platform.VectorBaseFragment
import kotlinx.android.synthetic.main.emoji_chooser_fragment.*
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
@ -31,10 +37,29 @@ class EmojiChooserFragment @Inject constructor() : VectorBaseFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = activityViewModelProvider.get(EmojiChooserViewModel::class.java)
viewModel.initWithContext(context!!)
(view as? RecyclerView)?.let {
it.adapter = viewModel.adapter
it.adapter?.notifyDataSetChanged()
emojiRecyclerAdapter.reactionClickListener = this
emojiRecyclerAdapter.interactionListener = this
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
import android.content.Context
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import im.vector.riotx.core.utils.LiveEvent
@ -23,36 +22,26 @@ import javax.inject.Inject
class EmojiChooserViewModel @Inject constructor() : ViewModel() {
var adapter: EmojiRecyclerAdapter? = null
val emojiSourceLiveData: MutableLiveData<EmojiDataSource> = MutableLiveData()
val navigateEvent: MutableLiveData<LiveEvent<String>> = MutableLiveData()
var selectedReaction: String? = null
var eventId: String? = null
val currentSection: MutableLiveData<Int> = MutableLiveData()
val moveToSection: MutableLiveData<Int> = MutableLiveData()
var reactionClickListener = object : ReactionClickListener {
override fun onReactionSelected(reaction: String) {
selectedReaction = reaction
navigateEvent.value = LiveEvent(NAVIGATE_FINISH)
}
fun onReactionSelected(reaction: String) {
selectedReaction = reaction
navigateEvent.value = LiveEvent(NAVIGATE_FINISH)
}
fun initWithContext(context: Context) {
// TODO load async
val emojiDataSource = EmojiDataSource(context)
emojiSourceLiveData.value = emojiDataSource
adapter = EmojiRecyclerAdapter(emojiDataSource, reactionClickListener)
adapter?.interactionListener = object : EmojiRecyclerAdapter.InteractionListener {
override fun firstVisibleSectionChange(section: Int) {
currentSection.value = section
}
}
// Called by the Fragment, when the List is scrolled
fun setCurrentSection(section: Int) {
currentSection.value = section
}
fun scrollToSection(sectionIndex: Int) {
adapter?.scrollToSection(sectionIndex)
// Called by the Activity, when a tab item is clicked
fun scrollToSection(section: Int) {
moveToSection.value = section
}
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.extensions.observeEvent
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.reactions.data.EmojiDataSource
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.activity_emoji_reaction_picker.*
import timber.log.Timber
@ -44,7 +45,6 @@ import javax.inject.Inject
/**
*
* TODO: Loading indicator while getting emoji data source?
* TODO: migrate to MvRx
* TODO: Finish Refactor to vector base activity
*/
class EmojiReactionPickerActivity : VectorBaseActivity(),
@ -54,13 +54,15 @@ class EmojiReactionPickerActivity : VectorBaseActivity(),
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 emojiDataSource: EmojiDataSource
private val searchResultViewModel: EmojiSearchResultViewModel by viewModel()
@ -93,22 +95,18 @@ class EmojiReactionPickerActivity : VectorBaseActivity(),
viewModel.eventId = intent.getStringExtra(EXTRA_EVENT_ID)
viewModel.emojiSourceLiveData.observe(this, Observer {
it.rawData?.categories?.let { categories ->
for (category in categories) {
val s = category.emojis[0]
tabLayout.newTab()
.also { tab ->
tab.text = it.rawData!!.emojis[s]!!.emojiString()
tab.contentDescription = category.name
}
.also { tab ->
tabLayout.addTab(tab)
}
}
tabLayout.addOnTabSelectedListener(tabLayoutSelectionListener)
}
})
emojiDataSource.rawData.categories.forEach { category ->
val s = category.emojis[0]
tabLayout.newTab()
.also { tab ->
tab.text = emojiDataSource.rawData.emojis[s]!!.emoji
tab.contentDescription = category.name
}
.also { tab ->
tabLayout.addTab(tab)
}
}
tabLayout.addOnTabSelectedListener(tabLayoutSelectionListener)
viewModel.currentSection.observe(this, Observer { section ->
section?.let {
@ -136,7 +134,6 @@ class EmojiReactionPickerActivity : VectorBaseActivity(),
override fun compatibilityFontUpdate(typeface: Typeface?) {
EmojiDrawView.configureTextPaint(this, typeface)
searchResultViewModel.dataSource
}
override fun onDestroy() {
@ -206,13 +203,19 @@ class EmojiReactionPickerActivity : VectorBaseActivity(),
companion object {
const val EXTRA_EVENT_ID = "EXTRA_EVENT_ID"
const val EXTRA_REACTION_RESULT = "EXTRA_REACTION_RESULT"
private const val EXTRA_EVENT_ID = "EXTRA_EVENT_ID"
private const val EXTRA_REACTION_RESULT = "EXTRA_REACTION_RESULT"
fun intent(context: Context, eventId: String): Intent {
val intent = Intent(context, EmojiReactionPickerActivity::class.java)
intent.putExtra(EXTRA_EVENT_ID, eventId)
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.TransitionManager
import im.vector.riotx.R
import im.vector.riotx.features.reactions.data.EmojiDataSource
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.math.abs
/**
*
* TODO: Configure Span using available width and emoji size
* TODO: Search
* TODO: Performances
* TODO: Scroll to section - Find a way to snap section to the top
*/
class EmojiRecyclerAdapter(private val dataSource: EmojiDataSource? = null,
private var reactionClickListener: ReactionClickListener?) :
class EmojiRecyclerAdapter @Inject constructor(
private val dataSource: EmojiDataSource
) :
RecyclerView.Adapter<EmojiRecyclerAdapter.ViewHolder>() {
var reactionClickListener: ReactionClickListener? = null
var interactionListener: InteractionListener? = null
private var mRecyclerView: RecyclerView? = null
@ -66,13 +69,12 @@ class EmojiRecyclerAdapter(private val dataSource: EmojiDataSource? = null,
private val itemClickListener = View.OnClickListener { view ->
mRecyclerView?.getChildLayoutPosition(view)?.let { itemPosition ->
if (itemPosition != RecyclerView.NO_POSITION) {
val categories = dataSource?.rawData?.categories ?: return@OnClickListener
val sectionNumber = getSectionForAbsoluteIndex(itemPosition)
if (!isSection(itemPosition)) {
val sectionMojis = categories[sectionNumber].emojis
val sectionMojis = dataSource.rawData.categories[sectionNumber].emojis
val sectionOffset = getSectionOffset(sectionNumber)
val emoji = sectionMojis[itemPosition - sectionOffset]
val item = dataSource.rawData!!.emojis.getValue(emoji).emojiString()
val item = dataSource.rawData.emojis.getValue(emoji).emoji
reactionClickListener?.onReactionSelected(item)
}
}
@ -113,7 +115,7 @@ class EmojiRecyclerAdapter(private val dataSource: EmojiDataSource? = null,
}
fun scrollToSection(section: Int) {
if (section < 0 || section >= dataSource?.rawData?.categories?.size ?: 0) {
if (section < 0 || section >= dataSource.rawData.categories.size) {
// ignore
return
}
@ -145,14 +147,12 @@ class EmojiRecyclerAdapter(private val dataSource: EmojiDataSource? = null,
}
private fun isSection(position: Int): Boolean {
dataSource?.rawData?.categories?.let { categories ->
var sectionOffset = 1
var lastItemInSection: Int
for (category in categories) {
lastItemInSection = sectionOffset + category.emojis.size - 1
if (position == sectionOffset - 1) return true
sectionOffset = lastItemInSection + 2
}
var sectionOffset = 1
var lastItemInSection: Int
dataSource.rawData.categories.forEach { category ->
lastItemInSection = sectionOffset + category.emojis.size - 1
if (position == sectionOffset - 1) return true
sectionOffset = lastItemInSection + 2
}
return false
}
@ -161,13 +161,11 @@ class EmojiRecyclerAdapter(private val dataSource: EmojiDataSource? = null,
var sectionOffset = 1
var lastItemInSection: Int
var index = 0
dataSource?.rawData?.categories?.let {
for (category in it) {
lastItemInSection = sectionOffset + category.emojis.size - 1
if (position <= lastItemInSection) return index
sectionOffset = lastItemInSection + 2
index++
}
dataSource.rawData.categories.forEach { category ->
lastItemInSection = sectionOffset + category.emojis.size - 1
if (position <= lastItemInSection) return index
sectionOffset = lastItemInSection + 2
index++
}
return index
}
@ -176,36 +174,32 @@ class EmojiRecyclerAdapter(private val dataSource: EmojiDataSource? = null,
// Todo cache this for fast access
var sectionOffset = 1
var lastItemInSection: Int
dataSource?.rawData?.categories?.let {
for ((index, category) in it.withIndex()) {
lastItemInSection = sectionOffset + category.emojis.size - 1
if (section == index) return sectionOffset
sectionOffset = lastItemInSection + 2
}
dataSource.rawData.categories.forEachIndexed { index, category ->
lastItemInSection = sectionOffset + category.emojis.size - 1
if (section == index) return sectionOffset
sectionOffset = lastItemInSection + 2
}
return sectionOffset
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
beginTraceSession("MyAdapter.onBindViewHolder")
dataSource?.rawData?.categories?.let { categories ->
val sectionNumber = getSectionForAbsoluteIndex(position)
if (isSection(position)) {
holder.bind(categories[sectionNumber].name)
} else {
val sectionMojis = categories[sectionNumber].emojis
val sectionOffset = getSectionOffset(sectionNumber)
val emoji = sectionMojis[position - sectionOffset]
val item = dataSource.rawData!!.emojis[emoji]!!.emojiString()
(holder as EmojiViewHolder).data = item
if (scrollState != ScrollState.SETTLING || !isFastScroll) {
val sectionNumber = getSectionForAbsoluteIndex(position)
if (isSection(position)) {
holder.bind(dataSource.rawData.categories[sectionNumber].name)
} else {
val sectionMojis = dataSource.rawData.categories[sectionNumber].emojis
val sectionOffset = getSectionOffset(sectionNumber)
val emoji = sectionMojis[position - sectionOffset]
val item = dataSource.rawData.emojis[emoji]!!.emoji
(holder as EmojiViewHolder).data = item
if (scrollState != ScrollState.SETTLING || !isFastScroll) {
// Log.i("PERF","Bind with draw at position:$position")
holder.bind(item)
} else {
holder.bind(item)
} else {
// Log.i("PERF","Bind without draw at position:$position")
toUpdateWhenNotBusy.add(item to holder)
holder.bind(null)
}
toUpdateWhenNotBusy.add(item to holder)
holder.bind(null)
}
}
endTraceSession()
@ -226,15 +220,8 @@ class EmojiRecyclerAdapter(private val dataSource: EmojiDataSource? = null,
super.onViewRecycled(holder)
}
override fun getItemCount(): Int {
return dataSource?.rawData?.categories?.let {
var count = /*number of sections*/ it.size
for (ad in it) {
count += ad.emojis.size
}
count
} ?: 0
}
override fun getItemCount() = dataSource.rawData.categories
.sumBy { emojiCategory -> 1 /* Section */ + emojiCategory.emojis.size }
abstract class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
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 javax.inject.Inject
class EmojiSearchResultController @Inject constructor(val stringProvider: StringProvider,
private val fontProvider: EmojiCompatFontProvider)
: TypedEpoxyController<EmojiSearchResultViewState>() {
class EmojiSearchResultController @Inject constructor(
private val stringProvider: StringProvider,
private val fontProvider: EmojiCompatFontProvider
) : TypedEpoxyController<EmojiSearchResultViewState>() {
var emojiTypeface: Typeface? = fontProvider.typeface

View File

@ -17,43 +17,42 @@ package im.vector.riotx.features.reactions
import android.os.Bundle
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.withState
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.utils.LiveEvent
import kotlinx.android.synthetic.main.fragment_generic_recycler_epoxy.*
import kotlinx.android.synthetic.main.fragment_generic_recycler.*
import javax.inject.Inject
class EmojiSearchResultFragment @Inject constructor(
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?) {
super.onViewCreated(view, savedInstanceState)
sharedViewModel = activityViewModelProvider.get(EmojiChooserViewModel::class.java)
epoxyController.listener = this
recyclerView.configureWith(epoxyController, showDivider = true)
}
epoxyController.listener = object : ReactionClickListener {
override fun onReactionSelected(reaction: String) {
sharedViewModel?.selectedReaction = reaction
sharedViewModel?.navigateEvent?.value = LiveEvent(EmojiChooserViewModel.NAVIGATE_FINISH)
}
}
override fun onDestroyView() {
epoxyController.listener = null
recyclerView.cleanup()
super.onDestroyView()
}
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 onReactionSelected(reaction: String) {
sharedViewModel.selectedReaction = reaction
sharedViewModel.navigateEvent.value = LiveEvent(EmojiChooserViewModel.NAVIGATE_FINISH)
}
override fun invalidate() = withState(viewModel) { state ->

View File

@ -22,12 +22,14 @@ import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder
import im.vector.riotx.R
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)
abstract class EmojiSearchResultItem : EpoxyModelWithHolder<EmojiSearchResultItem.Holder>() {
@EpoxyAttribute
lateinit var emojiItem: EmojiDataSource.EmojiItem
lateinit var emojiItem: EmojiItem
@EpoxyAttribute
var currentQuery: String? = null
@ -41,12 +43,12 @@ abstract class EmojiSearchResultItem : EpoxyModelWithHolder<EmojiSearchResultIte
override fun bind(holder: Holder) {
super.bind(holder)
// 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.emojiNameText.text = emojiItem.name
holder.emojiKeywordText.text = emojiItem.keywords?.joinToString(", ")
holder.emojiKeywordText.setTextOrHide(emojiItem.keywords.joinToString())
holder.view.setOnClickListener {
onClickListener?.onReactionSelected(emojiItem.emojiString())
onClickListener?.onReactionSelected(emojiItem.emoji)
}
}

View File

@ -15,19 +15,39 @@
*/
package im.vector.riotx.features.reactions
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
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.features.reactions.data.EmojiDataSource
import im.vector.riotx.features.reactions.data.EmojiItem
data class EmojiSearchResultViewState(
val query: String = "",
val results: List<EmojiDataSource.EmojiItem> = emptyList()
val results: List<EmojiItem> = emptyList()
) : MvRxState
class EmojiSearchResultViewModel(val dataSource: EmojiDataSource, initialState: EmojiSearchResultViewState)
class EmojiSearchResultViewModel @AssistedInject constructor(
@Assisted initialState: EmojiSearchResultViewState,
private val dataSource: EmojiDataSource)
: 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) {
when (action) {
is EmojiSearchAction.UpdateQuery -> updateQuery(action)
@ -35,26 +55,27 @@ class EmojiSearchResultViewModel(val dataSource: EmojiDataSource, initialState:
}
private fun updateQuery(action: EmojiSearchAction.UpdateQuery) {
val words = action.queryString.split("\\s".toRegex())
setState {
copy(
query = action.queryString,
results = dataSource.rawData?.emojis?.toList()
?.map { it.second }
?.filter {
it.name.contains(action.queryString, true)
|| action.queryString.split("\\s".toRegex()).fold(true, { prev, q ->
prev && (it.keywords?.any { it.contains(q, true) } ?: false)
// First add emojis with name matching query, sorted by name
// Then emojis with keyword matching any of the word in the query, sorted by name
results = dataSource.rawData.emojis
.values
.filter { emojiItem ->
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.view.MenuItem
import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.activityViewModel
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.riotx.R
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.platform.VectorBaseFragment
import io.reactivex.rxkotlin.subscribeBy
@ -62,6 +63,9 @@ class PublicRoomsFragment @Inject constructor(
it.setDisplayHomeAsUpEnabled(true)
}
sharedActionViewModel = activityViewModelProvider.get(RoomDirectorySharedActionViewModel::class.java)
setupRecyclerView()
publicRoomsFilter.queryTextChanges()
.debounce(500, TimeUnit.MILLISECONDS)
.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 {
return when (item.itemId) {
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() {
val epoxyVisibilityTracker = EpoxyVisibilityTracker()
epoxyVisibilityTracker.attach(publicRoomsList)
val layoutManager = LinearLayoutManager(context)
publicRoomsList.layoutManager = layoutManager
publicRoomsList.configureWith(publicRoomsController)
publicRoomsController.callback = this
publicRoomsList.setController(publicRoomsController)
}
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.view.MenuItem
import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
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.features.roomdirectory.RoomDirectorySharedAction
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 {
return when (item.itemId) {
R.id.action_create_room -> {
@ -62,12 +69,8 @@ class CreateRoomFragment @Inject constructor(private val createRoomController: C
}
private fun setupRecyclerView() {
val layoutManager = LinearLayoutManager(context)
createRoomForm.layoutManager = layoutManager
createRoomForm.configureWith(createRoomController)
createRoomController.listener = this
createRoomForm.setController(createRoomController)
}
override fun onNameChange(newName: String) {

View File

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

View File

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

View File

@ -26,10 +26,12 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
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.platform.VectorBaseActivity
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 javax.inject.Inject
@ -39,7 +41,7 @@ class VectorSettingsIgnoredUsersFragment @Inject constructor(
private val errorFormatter: ErrorFormatter
) : 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()
@ -49,12 +51,18 @@ class VectorSettingsIgnoredUsersFragment @Inject constructor(
waiting_view_status_text.setText(R.string.please_wait)
waiting_view_status_text.isVisible = true
ignoredUsersController.callback = this
epoxyRecyclerView.setController(ignoredUsersController)
recyclerView.configureWith(ignoredUsersController)
ignoredUsersViewModel.requestErrorLiveData.observeEvent(this) {
displayErrorDialog(it)
}
}
override fun onDestroyView() {
ignoredUsersController.callback = null
recyclerView.cleanup()
super.onDestroyView()
}
override fun 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
import android.os.Bundle
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.TypedEpoxyController
import android.view.View
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
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.VectorBaseFragment
import im.vector.riotx.core.resources.StringProvider
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.*
import javax.inject.Inject
// Referenced in vector_settings_notifications.xml
class PushGatewaysFragment @Inject constructor(
val pushGatewaysViewModelFactory: PushGatewaysViewModel.Factory
val pushGatewaysViewModelFactory: PushGatewaysViewModel.Factory,
private val epoxyController: PushGateWayController
) : 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 epoxyController by lazy { PushGateWayController(StringProvider(requireContext().resources)) }
override fun onResume() {
super.onResume()
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_notifications_targets)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
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 onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView.configureWith(epoxyController, showDivider = true)
}
override fun onDestroyView() {
recyclerView.cleanup()
super.onDestroyView()
}
override fun invalidate() = withState(viewModel) { 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
import android.os.Bundle
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import android.view.View
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
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.VectorBaseFragment
import im.vector.riotx.core.resources.StringProvider
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
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)
@ -43,14 +43,14 @@ class PushRulesFragment : VectorBaseFragment() {
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_push_rules)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
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 onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView.configureWith(epoxyController, showDivider = true)
}
override fun onDestroyView() {
recyclerView.cleanup()
super.onDestroyView()
}
override fun invalidate() = withState(viewModel) { state ->

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<?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"
android:id="@+id/breadcrumbsRecyclerView"
android:layout_width="wrap_content"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<?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"
android:id="@+id/keysBackupSettingsRecyclerView"
android:layout_width="match_parent"

View File

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

View File

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

View File

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

View File

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