forked from GitHub-Mirror/riotX-android
Merge branch 'feature/room_list' into develop
This commit is contained in:
@ -11,6 +11,19 @@ androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
|
||||
def versionMajor = 0
|
||||
def versionMinor = 1
|
||||
def versionPatch = 0
|
||||
|
||||
def generateVersionCodeFromTimestamp() {
|
||||
// It's unix timestamp divided by 10: It's incremented by one every 10 seconds.
|
||||
return (System.currentTimeMillis() / 1_000 / 10).toInteger()
|
||||
}
|
||||
|
||||
def generateVersionCodeFromVersionName() {
|
||||
return versionMajor * 10000 + versionMinor * 100 + versionPatch
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
defaultConfig {
|
||||
@ -18,8 +31,8 @@ android {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 28
|
||||
multiDexEnabled true
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
versionCode generateVersionCodeFromTimestamp()
|
||||
versionName "${versionMajor}.${versionMinor}.${versionPatch}"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
buildTypes {
|
||||
|
@ -16,10 +16,11 @@
|
||||
|
||||
package im.vector.riotredesign.core.epoxy
|
||||
|
||||
import android.view.View
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.annotation.LayoutRes
|
||||
import android.view.View
|
||||
import com.airbnb.epoxy.EpoxyModel
|
||||
import com.airbnb.epoxy.OnModelVisibilityStateChangedListener
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
@ -29,6 +30,7 @@ abstract class KotlinModel(
|
||||
|
||||
private var view: View? = null
|
||||
private var onBindCallback: (() -> Unit)? = null
|
||||
private var onModelVisibilityStateChangedListener: OnModelVisibilityStateChangedListener<KotlinModel, View>? = null
|
||||
|
||||
abstract fun bind()
|
||||
|
||||
@ -47,6 +49,16 @@ abstract class KotlinModel(
|
||||
return this
|
||||
}
|
||||
|
||||
override fun onVisibilityStateChanged(visibilityState: Int, view: View) {
|
||||
onModelVisibilityStateChangedListener?.onVisibilityStateChanged(this, view, visibilityState)
|
||||
super.onVisibilityStateChanged(visibilityState, view)
|
||||
}
|
||||
|
||||
fun setOnVisibilityStateChanged(listener: OnModelVisibilityStateChangedListener<KotlinModel, View>): KotlinModel {
|
||||
this.onModelVisibilityStateChangedListener = listener
|
||||
return this
|
||||
}
|
||||
|
||||
override fun getDefaultLayout() = layoutRes
|
||||
|
||||
protected fun <V : View> bind(@IdRes id: Int) = object : ReadOnlyProperty<KotlinModel, V> {
|
||||
@ -56,7 +68,7 @@ abstract class KotlinModel(
|
||||
// be optimized with a map
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return view?.findViewById(id) as V?
|
||||
?: throw IllegalStateException("View ID $id for '${property.name}' not found.")
|
||||
?: throw IllegalStateException("View ID $id for '${property.name}' not found.")
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright 2019 New Vector Ltd
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.core.extensions
|
||||
|
||||
/**
|
||||
* Returns the last element yielding the smallest value of the given function or `null` if there are no elements.
|
||||
*/
|
||||
public inline fun <T, R : Comparable<R>> Iterable<T>.lastMinBy(selector: (T) -> R): T? {
|
||||
val iterator = iterator()
|
||||
if (!iterator.hasNext()) return null
|
||||
var minElem = iterator.next()
|
||||
var minValue = selector(minElem)
|
||||
while (iterator.hasNext()) {
|
||||
val e = iterator.next()
|
||||
val v = selector(e)
|
||||
if (minValue >= v) {
|
||||
minElem = e
|
||||
minValue = v
|
||||
}
|
||||
}
|
||||
return minElem
|
||||
}
|
@ -27,6 +27,8 @@ import im.vector.riotredesign.features.home.room.detail.timeline.TimelineDateFor
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineItemFactory
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||
import im.vector.riotredesign.features.home.room.list.RoomSummaryComparator
|
||||
import im.vector.riotredesign.features.home.room.list.RoomSummaryController
|
||||
import org.koin.dsl.module.module
|
||||
|
||||
class HomeModule {
|
||||
@ -65,6 +67,10 @@ class HomeModule {
|
||||
HomeNavigator()
|
||||
}
|
||||
|
||||
factory {
|
||||
RoomSummaryController(get())
|
||||
}
|
||||
|
||||
factory { (roomId: String) ->
|
||||
TimelineEventController(roomId, get(), get(), get())
|
||||
}
|
||||
@ -85,6 +91,10 @@ class HomeModule {
|
||||
HomePermalinkHandler(get())
|
||||
}
|
||||
|
||||
single {
|
||||
RoomSummaryComparator()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -16,9 +16,12 @@
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail
|
||||
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
|
||||
sealed class RoomDetailActions {
|
||||
|
||||
data class SendMessage(val text: String) : RoomDetailActions()
|
||||
object IsDisplayed : RoomDetailActions()
|
||||
data class EventDisplayed(val event: TimelineEvent, val index: Int) : RoomDetailActions()
|
||||
|
||||
}
|
@ -23,9 +23,11 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.airbnb.epoxy.EpoxyVisibilityTracker
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.args
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.platform.RiotFragment
|
||||
import im.vector.riotredesign.core.platform.ToolbarConfigurable
|
||||
@ -75,7 +77,7 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
roomDetailViewModel.accept(RoomDetailActions.IsDisplayed)
|
||||
roomDetailViewModel.process(RoomDetailActions.IsDisplayed)
|
||||
}
|
||||
|
||||
private fun setupToolbar() {
|
||||
@ -86,6 +88,8 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
val epoxyVisibilityTracker = EpoxyVisibilityTracker()
|
||||
epoxyVisibilityTracker.attach(recyclerView)
|
||||
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
|
||||
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager)
|
||||
recyclerView.layoutManager = layoutManager
|
||||
@ -100,7 +104,7 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
|
||||
val textMessage = composerEditText.text.toString()
|
||||
if (textMessage.isNotBlank()) {
|
||||
composerEditText.text = null
|
||||
roomDetailViewModel.accept(RoomDetailActions.SendMessage(textMessage))
|
||||
roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -143,4 +147,8 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
|
||||
homePermalinkHandler.launch(url)
|
||||
}
|
||||
|
||||
override fun onEventVisible(event: TimelineEvent, index: Int) {
|
||||
roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event, index))
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -18,14 +18,18 @@ package im.vector.riotredesign.features.home.room.detail
|
||||
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.jakewharton.rxrelay2.BehaviorRelay
|
||||
import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.rx.rx
|
||||
import im.vector.riotredesign.core.extensions.lastMinBy
|
||||
import im.vector.riotredesign.core.platform.RiotViewModel
|
||||
import im.vector.riotredesign.features.home.room.VisibleRoomHolder
|
||||
import io.reactivex.rxkotlin.subscribeBy
|
||||
import org.koin.android.ext.android.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class RoomDetailViewModel(initialState: RoomDetailViewState,
|
||||
private val session: Session,
|
||||
@ -36,6 +40,8 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
||||
private val roomId = initialState.roomId
|
||||
private val eventId = initialState.eventId
|
||||
|
||||
private val displayedEventsObservable = BehaviorRelay.create<RoomDetailActions.EventDisplayed>()
|
||||
|
||||
companion object : MvRxViewModelFactory<RoomDetailViewModel, RoomDetailViewState> {
|
||||
|
||||
@JvmStatic
|
||||
@ -49,13 +55,15 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
||||
init {
|
||||
observeRoomSummary()
|
||||
observeTimeline()
|
||||
observeDisplayedEvents()
|
||||
room.loadRoomMembersIfNeeded()
|
||||
}
|
||||
|
||||
fun accept(action: RoomDetailActions) {
|
||||
fun process(action: RoomDetailActions) {
|
||||
when (action) {
|
||||
is RoomDetailActions.SendMessage -> handleSendMessage(action)
|
||||
is RoomDetailActions.IsDisplayed -> visibleRoomHolder.setVisibleRoom(roomId)
|
||||
is RoomDetailActions.SendMessage -> handleSendMessage(action)
|
||||
is RoomDetailActions.IsDisplayed -> handleIsDisplayed()
|
||||
is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action)
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,6 +73,29 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
||||
room.sendTextMessage(action.text, callback = object : MatrixCallback<Event> {})
|
||||
}
|
||||
|
||||
private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) {
|
||||
displayedEventsObservable.accept(action)
|
||||
}
|
||||
|
||||
private fun handleIsDisplayed() {
|
||||
visibleRoomHolder.setVisibleRoom(roomId)
|
||||
}
|
||||
|
||||
private fun observeDisplayedEvents() {
|
||||
// We are buffering scroll events for one second
|
||||
// and keep the most recent one to set the read receipt on.
|
||||
displayedEventsObservable.hide()
|
||||
.buffer(1, TimeUnit.SECONDS)
|
||||
.filter { it.isNotEmpty() }
|
||||
.subscribeBy(onNext = { actions ->
|
||||
val mostRecentEvent = actions.lastMinBy { it.index }
|
||||
mostRecentEvent?.event?.root?.eventId?.let { eventId ->
|
||||
room.setReadReceipt(eventId, callback = object : MatrixCallback<Void> {})
|
||||
}
|
||||
})
|
||||
.disposeOnClear()
|
||||
}
|
||||
|
||||
private fun observeRoomSummary() {
|
||||
room.rx().liveRoomSummary()
|
||||
.execute { async ->
|
||||
|
@ -16,12 +16,16 @@
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.airbnb.epoxy.EpoxyAsyncUtil
|
||||
import com.airbnb.epoxy.EpoxyModel
|
||||
import com.airbnb.epoxy.OnModelVisibilityStateChangedListener
|
||||
import com.airbnb.epoxy.VisibilityState
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineData
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotredesign.core.epoxy.KotlinModel
|
||||
import im.vector.riotredesign.core.extensions.localDateTime
|
||||
import im.vector.riotredesign.features.home.LoadingItemModel_
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||
@ -74,6 +78,11 @@ class TimelineEventController(private val roomId: String,
|
||||
|
||||
timelineItemFactory.create(event, nextEvent, callback)?.also {
|
||||
it.id(event.localId)
|
||||
it.setOnVisibilityStateChanged(OnModelVisibilityStateChangedListener<KotlinModel, View> { model, view, visibilityState ->
|
||||
if (visibilityState == VisibilityState.VISIBLE) {
|
||||
callback?.onEventVisible(event, currentPosition)
|
||||
}
|
||||
})
|
||||
epoxyModels.add(it)
|
||||
}
|
||||
if (addDaySeparator) {
|
||||
@ -98,6 +107,7 @@ class TimelineEventController(private val roomId: String,
|
||||
|
||||
|
||||
interface Callback {
|
||||
fun onEventVisible(event: TimelineEvent, index: Int)
|
||||
fun onUrlClicked(url: String)
|
||||
}
|
||||
|
||||
|
@ -16,9 +16,9 @@
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||
|
||||
import com.airbnb.epoxy.EpoxyModel
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotredesign.core.epoxy.KotlinModel
|
||||
|
||||
class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
|
||||
private val roomNameItemFactory: RoomNameItemFactory,
|
||||
@ -28,14 +28,14 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
|
||||
|
||||
fun create(event: TimelineEvent,
|
||||
nextEvent: TimelineEvent?,
|
||||
callback: TimelineEventController.Callback?): EpoxyModel<*>? {
|
||||
callback: TimelineEventController.Callback?): KotlinModel? {
|
||||
|
||||
return when (event.root.type) {
|
||||
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, callback)
|
||||
EventType.STATE_ROOM_NAME -> roomNameItemFactory.create(event)
|
||||
EventType.STATE_ROOM_TOPIC -> roomTopicItemFactory.create(event)
|
||||
EventType.STATE_ROOM_MEMBER -> roomMemberItemFactory.create(event)
|
||||
else -> defaultItemFactory.create(event)
|
||||
else -> defaultItemFactory.create(event)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,9 +26,12 @@ import im.vector.riotredesign.core.epoxy.KotlinModel
|
||||
data class RoomCategoryItem(
|
||||
val title: CharSequence,
|
||||
val isExpanded: Boolean,
|
||||
val unreadCount: Int,
|
||||
val showHighlighted: Boolean,
|
||||
val listener: (() -> Unit)? = null
|
||||
) : KotlinModel(R.layout.item_room_category) {
|
||||
|
||||
private val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.roomCategoryUnreadCounterBadgeView)
|
||||
private val titleView by bind<TextView>(R.id.roomCategoryTitleView)
|
||||
private val rootView by bind<ViewGroup>(R.id.roomCategoryRootView)
|
||||
|
||||
@ -41,6 +44,7 @@ data class RoomCategoryItem(
|
||||
val expandedArrowDrawable = ContextCompat.getDrawable(rootView.context, expandedArrowDrawableRes)?.also {
|
||||
DrawableCompat.setTint(it, tintColor)
|
||||
}
|
||||
unreadCounterBadgeView.render(unreadCount, showHighlighted)
|
||||
titleView.setCompoundDrawablesWithIntrinsicBounds(expandedArrowDrawable, null, null, null)
|
||||
titleView.text = title
|
||||
rootView.setOnClickListener { listener?.invoke() }
|
||||
|
@ -22,7 +22,6 @@ import android.text.TextWatcher
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Incomplete
|
||||
import com.airbnb.mvrx.Success
|
||||
@ -30,7 +29,6 @@ import com.airbnb.mvrx.activityViewModel
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.extensions.hideKeyboard
|
||||
import im.vector.riotredesign.core.extensions.setupAsSearch
|
||||
import im.vector.riotredesign.core.platform.RiotFragment
|
||||
import im.vector.riotredesign.core.platform.StateView
|
||||
@ -47,8 +45,8 @@ class RoomListFragment : RiotFragment(), RoomSummaryController.Callback {
|
||||
}
|
||||
|
||||
private val homeNavigator by inject<HomeNavigator>()
|
||||
private val roomController by inject<RoomSummaryController>()
|
||||
private val homeViewModel: RoomListViewModel by activityViewModel()
|
||||
private lateinit var roomController: RoomSummaryController
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_room_list, container, false)
|
||||
@ -56,7 +54,7 @@ class RoomListFragment : RiotFragment(), RoomSummaryController.Callback {
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
roomController = RoomSummaryController(this)
|
||||
roomController.callback = this
|
||||
stateView.contentView = epoxyRecyclerView
|
||||
epoxyRecyclerView.setController(roomController)
|
||||
setupFilterView()
|
||||
|
@ -24,6 +24,7 @@ import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.tag.RoomTag
|
||||
import im.vector.matrix.rx.rx
|
||||
import im.vector.riotredesign.core.platform.RiotViewModel
|
||||
import im.vector.riotredesign.features.home.group.SelectedGroupHolder
|
||||
@ -32,6 +33,7 @@ import io.reactivex.Observable
|
||||
import io.reactivex.functions.Function3
|
||||
import io.reactivex.rxkotlin.subscribeBy
|
||||
import org.koin.android.ext.android.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
typealias RoomListFilterName = CharSequence
|
||||
|
||||
@ -39,7 +41,8 @@ class RoomListViewModel(initialState: RoomListViewState,
|
||||
private val session: Session,
|
||||
private val selectedGroupHolder: SelectedGroupHolder,
|
||||
private val visibleRoomHolder: VisibleRoomHolder,
|
||||
private val roomSelectionRepository: RoomSelectionRepository)
|
||||
private val roomSelectionRepository: RoomSelectionRepository,
|
||||
private val roomSummaryComparator: RoomSummaryComparator)
|
||||
: RiotViewModel<RoomListViewState>(initialState) {
|
||||
|
||||
companion object : MvRxViewModelFactory<RoomListViewModel, RoomListViewState> {
|
||||
@ -50,7 +53,8 @@ class RoomListViewModel(initialState: RoomListViewState,
|
||||
val roomSelectionRepository = viewModelContext.activity.get<RoomSelectionRepository>()
|
||||
val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupHolder>()
|
||||
val visibleRoomHolder = viewModelContext.activity.get<VisibleRoomHolder>()
|
||||
return RoomListViewModel(state, currentSession, selectedGroupHolder, visibleRoomHolder, roomSelectionRepository)
|
||||
val roomSummaryComparator = viewModelContext.activity.get<RoomSummaryComparator>()
|
||||
return RoomListViewModel(state, currentSession, selectedGroupHolder, visibleRoomHolder, roomSelectionRepository, roomSummaryComparator)
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,19 +96,11 @@ class RoomListViewModel(initialState: RoomListViewState,
|
||||
|
||||
private fun observeRoomSummaries() {
|
||||
Observable.combineLatest<List<RoomSummary>, Option<GroupSummary>, Option<RoomListFilterName>, RoomSummaries>(
|
||||
session.rx().liveRoomSummaries(),
|
||||
session.rx().liveRoomSummaries().throttleLast(300, TimeUnit.MILLISECONDS),
|
||||
selectedGroupHolder.selectedGroup(),
|
||||
roomListFilter,
|
||||
roomListFilter.throttleLast(300, TimeUnit.MILLISECONDS),
|
||||
Function3 { rooms, selectedGroupOption, filterRoomOption ->
|
||||
val filterRoom = filterRoomOption.orNull()
|
||||
val filteredRooms = rooms.filter {
|
||||
if (filterRoom.isNullOrBlank()) {
|
||||
true
|
||||
} else {
|
||||
it.displayName.contains(other = filterRoom, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
val filteredRooms = filterRooms(rooms, filterRoomOption)
|
||||
val selectedGroup = selectedGroupOption.orNull()
|
||||
val filteredDirectRooms = filteredRooms
|
||||
.filter { it.isDirect }
|
||||
@ -123,7 +119,7 @@ class RoomListViewModel(initialState: RoomListViewState,
|
||||
.filter {
|
||||
selectedGroup?.roomIds?.contains(it.roomId) ?: true
|
||||
}
|
||||
RoomSummaries(filteredDirectRooms, filteredGroupRooms)
|
||||
buildRoomSummaries(filteredDirectRooms + filteredGroupRooms)
|
||||
}
|
||||
)
|
||||
.execute { async ->
|
||||
@ -132,4 +128,44 @@ class RoomListViewModel(initialState: RoomListViewState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterRooms(rooms: List<RoomSummary>, filterRoomOption: Option<RoomListFilterName>): List<RoomSummary> {
|
||||
val filterRoom = filterRoomOption.orNull()
|
||||
return rooms.filter {
|
||||
if (filterRoom.isNullOrBlank()) {
|
||||
true
|
||||
} else {
|
||||
it.displayName.contains(other = filterRoom, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildRoomSummaries(rooms: List<RoomSummary>): RoomSummaries {
|
||||
val favourites = ArrayList<RoomSummary>()
|
||||
val directChats = ArrayList<RoomSummary>()
|
||||
val groupRooms = ArrayList<RoomSummary>()
|
||||
val lowPriorities = ArrayList<RoomSummary>()
|
||||
val serverNotices = ArrayList<RoomSummary>()
|
||||
|
||||
for (room in rooms) {
|
||||
val tags = room.tags.map { it.name }
|
||||
when {
|
||||
tags.contains(RoomTag.ROOM_TAG_SERVER_NOTICE) -> serverNotices.add(room)
|
||||
tags.contains(RoomTag.ROOM_TAG_FAVOURITE) -> favourites.add(room)
|
||||
tags.contains(RoomTag.ROOM_TAG_LOW_PRIORITY) -> lowPriorities.add(room)
|
||||
room.isDirect -> directChats.add(room)
|
||||
else -> groupRooms.add(room)
|
||||
}
|
||||
}
|
||||
|
||||
return RoomSummaries(
|
||||
favourites = favourites.sortedWith(roomSummaryComparator),
|
||||
directRooms = directChats.sortedWith(roomSummaryComparator),
|
||||
groupRooms = groupRooms.sortedWith(roomSummaryComparator),
|
||||
lowPriorities = lowPriorities.sortedWith(roomSummaryComparator),
|
||||
serverNotices = serverNotices.sortedWith(roomSummaryComparator)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -27,10 +27,13 @@ data class RoomListViewState(
|
||||
) : MvRxState
|
||||
|
||||
data class RoomSummaries(
|
||||
val favourites: List<RoomSummary>,
|
||||
val directRooms: List<RoomSummary>,
|
||||
val groupRooms: List<RoomSummary>
|
||||
val groupRooms: List<RoomSummary>,
|
||||
val lowPriorities: List<RoomSummary>,
|
||||
val serverNotices: List<RoomSummary>
|
||||
)
|
||||
|
||||
fun RoomSummaries?.isNullOrEmpty(): Boolean {
|
||||
return this == null || (directRooms.isEmpty() && groupRooms.isEmpty())
|
||||
return this == null || (directRooms.isEmpty() && groupRooms.isEmpty() && favourites.isEmpty() && lowPriorities.isEmpty() && serverNotices.isEmpty())
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.list
|
||||
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
|
||||
class RoomSummaryComparator
|
||||
: Comparator<RoomSummary> {
|
||||
|
||||
override fun compare(leftRoomSummary: RoomSummary?, rightRoomSummary: RoomSummary?): Int {
|
||||
val retValue: Int
|
||||
var leftHighlightCount = 0
|
||||
var rightHighlightCount = 0
|
||||
var leftNotificationCount = 0
|
||||
var rightNotificationCount = 0
|
||||
var rightTimestamp = 0L
|
||||
var leftTimestamp = 0L
|
||||
|
||||
if (null != leftRoomSummary) {
|
||||
leftHighlightCount = leftRoomSummary.highlightCount
|
||||
leftNotificationCount = leftRoomSummary.notificationCount
|
||||
leftTimestamp = leftRoomSummary.lastMessage?.originServerTs ?: 0
|
||||
}
|
||||
if (null != rightRoomSummary) {
|
||||
rightHighlightCount = rightRoomSummary.highlightCount
|
||||
rightNotificationCount = rightRoomSummary.notificationCount
|
||||
rightTimestamp = rightRoomSummary.lastMessage?.originServerTs ?: 0
|
||||
}
|
||||
|
||||
if (leftRoomSummary?.lastMessage == null) {
|
||||
retValue = 1
|
||||
} else if (rightRoomSummary?.lastMessage == null) {
|
||||
retValue = -1
|
||||
} else if (rightHighlightCount > 0 && leftHighlightCount == 0) {
|
||||
retValue = 1
|
||||
} else if (rightHighlightCount == 0 && leftHighlightCount > 0) {
|
||||
retValue = -1
|
||||
} else if (rightNotificationCount > 0 && leftNotificationCount == 0) {
|
||||
retValue = 1
|
||||
} else if (rightNotificationCount == 0 && leftNotificationCount > 0) {
|
||||
retValue = -1
|
||||
} else {
|
||||
val deltaTimestamp = rightTimestamp - leftTimestamp
|
||||
if (deltaTimestamp > 0) {
|
||||
retValue = 1
|
||||
} else if (deltaTimestamp < 0) {
|
||||
retValue = -1
|
||||
} else {
|
||||
retValue = 0
|
||||
}
|
||||
}
|
||||
return retValue
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -16,56 +16,100 @@
|
||||
|
||||
package im.vector.riotredesign.features.home.room.list
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.airbnb.epoxy.TypedEpoxyController
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.resources.StringProvider
|
||||
|
||||
class RoomSummaryController(private val callback: Callback? = null
|
||||
class RoomSummaryController(private val stringProvider: StringProvider
|
||||
) : TypedEpoxyController<RoomListViewState>() {
|
||||
|
||||
private var isDirectRoomsExpanded = true
|
||||
private var isGroupRoomsExpanded = true
|
||||
private var isFavoriteRoomsExpanded = true
|
||||
private var isDirectRoomsExpanded = false
|
||||
private var isGroupRoomsExpanded = false
|
||||
private var isLowPriorityRoomsExpanded = false
|
||||
private var isServerNoticeRoomsExpanded = false
|
||||
|
||||
var callback: Callback? = null
|
||||
|
||||
override fun buildModels(viewState: RoomListViewState) {
|
||||
val roomSummaries = viewState.asyncRooms()
|
||||
RoomCategoryItem(
|
||||
title = "DIRECT MESSAGES",
|
||||
isExpanded = isDirectRoomsExpanded,
|
||||
listener = {
|
||||
isDirectRoomsExpanded = !isDirectRoomsExpanded
|
||||
setData(viewState)
|
||||
}
|
||||
)
|
||||
.id("direct_messages")
|
||||
.addTo(this)
|
||||
|
||||
if (isDirectRoomsExpanded) {
|
||||
buildRoomModels(roomSummaries?.directRooms ?: emptyList(), viewState.selectedRoomId)
|
||||
val favourites = roomSummaries?.favourites ?: emptyList()
|
||||
buildRoomCategory(viewState, favourites, R.string.room_list_favourites, isFavoriteRoomsExpanded) {
|
||||
isFavoriteRoomsExpanded = !isFavoriteRoomsExpanded
|
||||
}
|
||||
if (isFavoriteRoomsExpanded) {
|
||||
buildRoomModels(favourites, viewState.selectedRoomId)
|
||||
}
|
||||
|
||||
RoomCategoryItem(
|
||||
title = "GROUPS",
|
||||
isExpanded = isGroupRoomsExpanded,
|
||||
listener = {
|
||||
isGroupRoomsExpanded = !isGroupRoomsExpanded
|
||||
setData(viewState)
|
||||
}
|
||||
)
|
||||
.id("group_messages")
|
||||
.addTo(this)
|
||||
val directRooms = roomSummaries?.directRooms ?: emptyList()
|
||||
buildRoomCategory(viewState, directRooms, R.string.room_list_direct, isDirectRoomsExpanded) {
|
||||
isDirectRoomsExpanded = !isDirectRoomsExpanded
|
||||
}
|
||||
if (isDirectRoomsExpanded) {
|
||||
buildRoomModels(directRooms, viewState.selectedRoomId)
|
||||
}
|
||||
|
||||
val groupRooms = roomSummaries?.groupRooms ?: emptyList()
|
||||
buildRoomCategory(viewState, groupRooms, R.string.room_list_group, isGroupRoomsExpanded) {
|
||||
isGroupRoomsExpanded = !isGroupRoomsExpanded
|
||||
}
|
||||
if (isGroupRoomsExpanded) {
|
||||
buildRoomModels(roomSummaries?.groupRooms ?: emptyList(), viewState.selectedRoomId)
|
||||
buildRoomModels(groupRooms, viewState.selectedRoomId)
|
||||
}
|
||||
|
||||
val lowPriorities = roomSummaries?.lowPriorities ?: emptyList()
|
||||
buildRoomCategory(viewState, lowPriorities, R.string.room_list_low_priority, isLowPriorityRoomsExpanded) {
|
||||
isLowPriorityRoomsExpanded = !isLowPriorityRoomsExpanded
|
||||
}
|
||||
if (isLowPriorityRoomsExpanded) {
|
||||
buildRoomModels(lowPriorities, viewState.selectedRoomId)
|
||||
}
|
||||
|
||||
val serverNotices = roomSummaries?.serverNotices ?: emptyList()
|
||||
buildRoomCategory(viewState, serverNotices, R.string.room_list_system_alert, isServerNoticeRoomsExpanded) {
|
||||
isServerNoticeRoomsExpanded = !isServerNoticeRoomsExpanded
|
||||
}
|
||||
if (isServerNoticeRoomsExpanded) {
|
||||
buildRoomModels(serverNotices, viewState.selectedRoomId)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun buildRoomCategory(viewState: RoomListViewState, summaries: List<RoomSummary>, @StringRes titleRes: Int, isExpanded: Boolean, mutateExpandedState: () -> Unit) {
|
||||
//TODO should add some business logic later
|
||||
val unreadCount = if (summaries.isEmpty()) {
|
||||
0
|
||||
} else {
|
||||
summaries.map { it.notificationCount }.reduce { acc, i -> acc + i }
|
||||
}
|
||||
val showHighlighted = summaries.any { it.highlightCount > 0 }
|
||||
RoomCategoryItem(
|
||||
title = stringProvider.getString(titleRes).toUpperCase(),
|
||||
isExpanded = isExpanded,
|
||||
unreadCount = unreadCount,
|
||||
showHighlighted = showHighlighted,
|
||||
listener = {
|
||||
mutateExpandedState()
|
||||
setData(viewState)
|
||||
}
|
||||
)
|
||||
.id(titleRes)
|
||||
.addTo(this)
|
||||
}
|
||||
|
||||
private fun buildRoomModels(summaries: List<RoomSummary>, selectedRoomId: String?) {
|
||||
summaries.forEach { roomSummary ->
|
||||
val unreadCount = roomSummary.notificationCount
|
||||
val showHighlighted = roomSummary.highlightCount > 0
|
||||
val isSelected = roomSummary.roomId == selectedRoomId
|
||||
RoomSummaryItem(
|
||||
roomName = roomSummary.displayName,
|
||||
avatarUrl = roomSummary.avatarUrl,
|
||||
isSelected = isSelected,
|
||||
showHighlighted = showHighlighted,
|
||||
unreadCount = unreadCount,
|
||||
listener = { callback?.onRoomSelected(roomSummary) }
|
||||
)
|
||||
.id(roomSummary.roomId)
|
||||
|
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotredesign.features.home.room.list
|
||||
|
||||
object RoomSummaryFormatter {
|
||||
|
||||
/**
|
||||
* Format the unread messages counter.
|
||||
*
|
||||
* @param count the count
|
||||
* @return the formatted value
|
||||
*/
|
||||
fun formatUnreadMessagesCounter(count: Int): String {
|
||||
return if (count > 999) {
|
||||
"${count / 1000}.${count % 1000 / 100}K"
|
||||
} else {
|
||||
count.toString()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -28,14 +28,18 @@ data class RoomSummaryItem(
|
||||
val roomName: CharSequence,
|
||||
val avatarUrl: String?,
|
||||
val isSelected: Boolean,
|
||||
val unreadCount: Int,
|
||||
val showHighlighted: Boolean,
|
||||
val listener: (() -> Unit)? = null
|
||||
) : KotlinModel(R.layout.item_room) {
|
||||
|
||||
private val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.roomUnreadCounterBadgeView)
|
||||
private val titleView by bind<TextView>(R.id.roomNameView)
|
||||
private val avatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
|
||||
private val rootView by bind<CheckableFrameLayout>(R.id.itemRoomLayout)
|
||||
|
||||
override fun bind() {
|
||||
unreadCounterBadgeView.render(unreadCount, showHighlighted)
|
||||
rootView.isChecked = isSelected
|
||||
rootView.setOnClickListener { listener?.invoke() }
|
||||
titleView.text = roomName
|
||||
|
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotredesign.features.home.room.list
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import im.vector.riotredesign.R
|
||||
|
||||
class UnreadCounterBadgeView : AppCompatTextView {
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
|
||||
fun render(count: Int, highlighted: Boolean) {
|
||||
if (count == 0) {
|
||||
visibility = View.INVISIBLE
|
||||
} else {
|
||||
visibility = View.VISIBLE
|
||||
val bgRes = if (highlighted) {
|
||||
R.drawable.bg_unread_highlight
|
||||
} else {
|
||||
R.drawable.bg_unread_notification
|
||||
}
|
||||
setBackgroundResource(bgRes)
|
||||
text = RoomSummaryFormatter.formatUnreadMessagesCounter(count)
|
||||
}
|
||||
}
|
||||
|
||||
enum class Status {
|
||||
NOTIFICATION,
|
||||
HIGHLIGHT
|
||||
}
|
||||
|
||||
}
|
5
app/src/main/res/drawable/bg_unread_highlight.xml
Normal file
5
app/src/main/res/drawable/bg_unread_highlight.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="@color/rosy_pink" />
|
||||
</shape>
|
6
app/src/main/res/drawable/bg_unread_notification.xml
Normal file
6
app/src/main/res/drawable/bg_unread_notification.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="@color/grey_lynch" />
|
||||
</shape>
|
@ -1,18 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<im.vector.riotredesign.core.platform.CheckableFrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<im.vector.riotredesign.core.platform.CheckableFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/itemRoomLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingRight="16dp"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:background="@drawable/bg_room_item"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
android:focusable="true"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingRight="16dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
@ -35,15 +34,34 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:duplicateParentState="true"
|
||||
android:textColor="@color/color_room_title"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/roomUnreadCounterBadgeView"
|
||||
app:layout_constraintStart_toEndOf="@id/roomAvatarImageView"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="@tools:sample/full_names" />
|
||||
|
||||
<im.vector.riotredesign.features.home.room.list.UnreadCounterBadgeView
|
||||
android:id="@+id/roomUnreadCounterBadgeView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:minWidth="24dp"
|
||||
android:minHeight="24dp"
|
||||
android:paddingLeft="4dp"
|
||||
android:paddingRight="4dp"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:background="@drawable/bg_unread_highlight"
|
||||
tools:text="115" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</im.vector.riotredesign.core.platform.CheckableFrameLayout>
|
@ -11,7 +11,7 @@
|
||||
android:gravity="center_vertical"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingRight="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
tools:background="@color/pale_grey">
|
||||
|
||||
@ -26,16 +26,32 @@
|
||||
android:text="DIRECT MESSAGES"
|
||||
android:textColor="@color/bluey_grey_two"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/roomCategoryAddButton"
|
||||
app:layout_constraintEnd_toStartOf="@+id/roomCategoryUnreadCounterBadgeView"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<im.vector.riotredesign.features.home.room.list.UnreadCounterBadgeView
|
||||
android:id="@+id/roomCategoryUnreadCounterBadgeView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:minWidth="24dp"
|
||||
android:minHeight="24dp"
|
||||
android:paddingLeft="4dp"
|
||||
android:paddingRight="4dp"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/roomCategoryAddButton"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:background="@drawable/bg_unread_highlight"
|
||||
tools:text="4" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/roomCategoryAddButton"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:scaleType="centerInside"
|
||||
android:src="@drawable/ic_add_circle_white"
|
||||
android:tint="@color/bluey_grey_two"
|
||||
|
@ -16,4 +16,5 @@
|
||||
<color name="cool_grey">#a5aab2</color>
|
||||
<color name="pale_grey_two">#ebedf8</color>
|
||||
<color name="brown_grey">#a5a5a5</color>
|
||||
<color name="grey_lynch">#61708B</color>
|
||||
</resources>
|
||||
|
@ -1,9 +1,15 @@
|
||||
<resources>
|
||||
<string name="app_name">Riot X</string>
|
||||
<string name="app_name">"Riot X"</string>
|
||||
|
||||
<string name="global_retry">Réessayer</string>
|
||||
<string name="error_no_network">Pas de connexion internet</string>
|
||||
<string name="error_common">Une erreur est survenue</string>
|
||||
<string name="room_list_empty">Rejoignez une room pour commencer à utiliser l\'application</string>
|
||||
<string name="global_retry">"Retry"</string>
|
||||
<string name="error_no_network">"No network connection"</string>
|
||||
<string name="error_common">"An error occurred"</string>
|
||||
|
||||
<string name="room_list_empty">"Join a room to start using the app."</string>
|
||||
<string name="room_list_favourites">"Favourites"</string>
|
||||
<string name="room_list_direct">"People"</string>
|
||||
<string name="room_list_group">"Rooms"</string>
|
||||
<string name="room_list_low_priority">"Low priority"</string>
|
||||
<string name="room_list_system_alert">"System Alerts"</string>
|
||||
|
||||
</resources>
|
||||
|
Reference in New Issue
Block a user