[issue-2610] Merge branch 'develop' of https://github.com/vector-im/element-android into feature/issue-2610-override-nick-color-via-user-account-data

This commit is contained in:
Péter Radics 2021-02-18 14:10:55 +01:00
commit ea01677768
29 changed files with 615 additions and 446 deletions

View file

@ -26,6 +26,7 @@
<w>pkcs</w>
<w>previewable</w>
<w>previewables</w>
<w>pstn</w>
<w>riotx</w>
<w>signin</w>
<w>signout</w>

View file

@ -14,6 +14,7 @@ Improvements 🙌:
Bugfix 🐛:
- VoIP : fix audio devices output
- Fix crash after initial sync on Dendrite
- Fix crash reported by PlayStore (#2707)
Translations 🗣:
-
@ -30,6 +31,7 @@ Test:
Other changes:
- New Dev Tools panel for developers
- Fix typos in CHANGES.md (#2811)
- Colors rework: first step: merge file `colors_riot.xml` to file `colors_riotx.xml` and rename the file to `colors.xml`
Changes in Element 1.0.17 (2021-02-09)
===================================================

View file

@ -16,12 +16,11 @@
package org.matrix.android.sdk.api.session.call
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.util.Cancelable
interface CallSignalingService {
fun getTurnServer(callback: MatrixCallback<TurnServerResponse>): Cancelable
suspend fun getTurnServer(): TurnServerResponse
fun getPSTNProtocolChecker(): PSTNProtocolChecker
/**
* Create an outgoing call

View file

@ -0,0 +1,98 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.session.call
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.thirdparty.GetThirdPartyProtocolsTask
import org.matrix.android.sdk.internal.task.TaskExecutor
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
private const val PSTN_VECTOR_KEY = "im.vector.protocol.pstn"
private const val PSTN_MATRIX_KEY = "m.protocol.pstn"
/**
* This class is responsible for checking if the HS support the PSTN protocol.
* As long as the request succeed, it'll check only once by session.
*/
@SessionScope
class PSTNProtocolChecker @Inject internal constructor(private val taskExecutor: TaskExecutor,
private val getThirdPartyProtocolsTask: GetThirdPartyProtocolsTask) {
interface Listener {
fun onPSTNSupportUpdated()
}
private var alreadyChecked = AtomicBoolean(false)
private val pstnSupportListeners = mutableListOf<Listener>()
fun addListener(listener: Listener) {
pstnSupportListeners.add(listener)
}
fun removeListener(listener: Listener) {
pstnSupportListeners.remove(listener)
}
var supportedPSTNProtocol: String? = null
private set
fun checkForPSTNSupportIfNeeded() {
if (alreadyChecked.get()) return
taskExecutor.executorScope.checkForPSTNSupport()
}
private fun CoroutineScope.checkForPSTNSupport() = launch {
try {
supportedPSTNProtocol = getSupportedPSTN(3)
alreadyChecked.set(true)
if (supportedPSTNProtocol != null) {
pstnSupportListeners.forEach {
tryOrNull { it.onPSTNSupportUpdated() }
}
}
} catch (failure: Throwable) {
Timber.v("Fail to get supported PSTN, will check again next time.")
}
}
private suspend fun getSupportedPSTN(maxTries: Int): String? {
val thirdPartyProtocols: Map<String, ThirdPartyProtocol> = try {
getThirdPartyProtocolsTask.execute(Unit)
} catch (failure: Throwable) {
if (maxTries == 1) {
throw failure
} else {
// Wait for 10s before trying again
delay(10_000L)
return getSupportedPSTN(maxTries - 1)
}
}
return when {
thirdPartyProtocols.containsKey(PSTN_VECTOR_KEY) -> PSTN_VECTOR_KEY
thirdPartyProtocols.containsKey(PSTN_MATRIX_KEY) -> PSTN_MATRIX_KEY
else -> null
}
}
}

View file

@ -16,16 +16,12 @@
package org.matrix.android.sdk.internal.session.call
import kotlinx.coroutines.Dispatchers
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.call.CallListener
import org.matrix.android.sdk.api.session.call.CallSignalingService
import org.matrix.android.sdk.api.session.call.MxCall
import org.matrix.android.sdk.api.session.call.PSTNProtocolChecker
import org.matrix.android.sdk.api.session.call.TurnServerResponse
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.launchToCallback
import timber.log.Timber
import javax.inject.Inject
@ -34,14 +30,16 @@ internal class DefaultCallSignalingService @Inject constructor(
private val callSignalingHandler: CallSignalingHandler,
private val mxCallFactory: MxCallFactory,
private val activeCallHandler: ActiveCallHandler,
private val taskExecutor: TaskExecutor,
private val turnServerDataSource: TurnServerDataSource
private val turnServerDataSource: TurnServerDataSource,
private val pstnProtocolChecker: PSTNProtocolChecker
) : CallSignalingService {
override fun getTurnServer(callback: MatrixCallback<TurnServerResponse>): Cancelable {
return taskExecutor.executorScope.launchToCallback(Dispatchers.Default, callback) {
turnServerDataSource.getTurnServer()
}
override suspend fun getTurnServer(): TurnServerResponse {
return turnServerDataSource.getTurnServer()
}
override fun getPSTNProtocolChecker(): PSTNProtocolChecker {
return pstnProtocolChecker
}
override fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall {

View file

@ -41,6 +41,9 @@ parser.add_argument('-b',
type=int,
required=True,
help='the buildkite build number.')
parser.add_argument('-f',
'--filename',
help='the filename, to download only one artifact.')
parser.add_argument('-e',
'--expecting',
type=int,
@ -148,6 +151,8 @@ for elt in data:
print(" %s: %s" % (key, str(value)))
url = elt.get("download_url")
filename = elt.get("filename")
if args.filename is not None and args.filename != filename:
continue
target = targetDir + "/" + filename
print("Downloading %s to '%s'..." % (filename, targetDir))
if not args.simulate:

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.app.core.epoxy
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents
@EpoxyModelClass(layout = R.layout.item_timeline_empty)
abstract class TimelineEmptyItem : VectorEpoxyModel<TimelineEmptyItem.Holder>(), ItemWithEvents {
@EpoxyAttribute lateinit var eventId: String
override fun getEventIds(): List<String> {
return listOf(eventId)
}
class Holder : VectorEpoxyHolder()
}

View file

@ -16,6 +16,7 @@
package im.vector.app.features.call
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxViewModelFactory
@ -29,17 +30,16 @@ import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.call.audio.CallAudioManager
import im.vector.app.features.call.webrtc.WebRtcCall
import im.vector.app.features.call.webrtc.WebRtcCallManager
import org.matrix.android.sdk.api.MatrixCallback
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.MxCall
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
import org.matrix.android.sdk.api.session.call.TurnServerResponse
import org.matrix.android.sdk.api.session.room.model.call.supportCallTransfer
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
import java.util.Timer
import java.util.TimerTask
class VectorCallViewModel @AssistedInject constructor(
@Assisted initialState: VectorCallViewState,
@ -50,7 +50,7 @@ class VectorCallViewModel @AssistedInject constructor(
private var call: WebRtcCall? = null
private var connectionTimeoutTimer: Timer? = null
private var connectionTimeoutJob: Job? = null
private var hasBeenConnectedOnce = false
private val callListener = object : WebRtcCall.Listener {
@ -92,26 +92,20 @@ class VectorCallViewModel @AssistedInject constructor(
val callState = call.state
if (callState is CallState.Connected && callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
hasBeenConnectedOnce = true
connectionTimeoutTimer?.cancel()
connectionTimeoutTimer = null
connectionTimeoutJob?.cancel()
connectionTimeoutJob = null
} else {
// do we reset as long as it's moving?
connectionTimeoutTimer?.cancel()
connectionTimeoutJob?.cancel()
if (hasBeenConnectedOnce) {
connectionTimeoutTimer = Timer().apply {
schedule(object : TimerTask() {
override fun run() {
session.callSignalingService().getTurnServer(object : MatrixCallback<TurnServerResponse> {
override fun onFailure(failure: Throwable) {
_viewEvents.post(VectorCallViewEvents.ConnectionTimeout(null))
}
override fun onSuccess(data: TurnServerResponse) {
_viewEvents.post(VectorCallViewEvents.ConnectionTimeout(data))
}
})
}
}, 30_000)
connectionTimeoutJob = viewModelScope.launch {
delay(30_000)
try {
val turn = session.callSignalingService().getTurnServer()
_viewEvents.post(VectorCallViewEvents.ConnectionTimeout(turn))
} catch (failure: Throwable) {
_viewEvents.post(VectorCallViewEvents.ConnectionTimeout(null))
}
}
}
}

View file

@ -83,12 +83,12 @@ class CallAudioManager(private val context: Context, val configChange: (() -> Un
fun setAudioDevice(device: Device) {
runInAudioThread(Runnable {
if (!_availableDevices.contains(device)) {
Timber.w(" Audio device not available: $device")
Timber.w("Audio device not available: $device")
userSelectedDevice = null
return@Runnable
}
if (mode != Mode.DEFAULT) {
Timber.i(" User selected device set to: $device")
Timber.i("User selected device set to: $device")
userSelectedDevice = device
updateAudioRoute(mode, false)
}
@ -108,7 +108,7 @@ class CallAudioManager(private val context: Context, val configChange: (() -> Un
success = updateAudioRoute(mode, false)
} catch (e: Throwable) {
success = false
Timber.e(e, " Failed to update audio route for mode: " + mode)
Timber.e(e, "Failed to update audio route for mode: $mode")
}
if (success) {
this@CallAudioManager.mode = mode
@ -124,7 +124,7 @@ class CallAudioManager(private val context: Context, val configChange: (() -> Un
* `false`, otherwise.
*/
private fun updateAudioRoute(mode: Mode, force: Boolean): Boolean {
Timber.i(" Update audio route for mode: " + mode)
Timber.i("Update audio route for mode: $mode")
if (!audioDeviceRouter?.setMode(mode).orFalse()) {
return false
}
@ -158,7 +158,7 @@ class CallAudioManager(private val context: Context, val configChange: (() -> Un
return true
}
selectedDevice = audioDevice
Timber.i(" Selected audio device: " + audioDevice)
Timber.i("Selected audio device: $audioDevice")
audioDeviceRouter?.setAudioRoute(audioDevice)
configChange?.invoke()
return true

View file

@ -22,20 +22,22 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.Session
import javax.inject.Inject
class DialPadLookup @Inject constructor(val session: Session,
val directRoomHelper: DirectRoomHelper,
val callManager: WebRtcCallManager
class DialPadLookup @Inject constructor(
private val session: Session,
private val directRoomHelper: DirectRoomHelper,
private val callManager: WebRtcCallManager
) {
class Failure : Throwable()
data class Result(val userId: String, val roomId: String)
suspend fun lookupPhoneNumber(phoneNumber: String): Result {
val supportedProtocolKey = callManager.supportedPSTNProtocol ?: throw Failure()
val thirdPartyUser = tryOrNull {
session.thirdPartyService().getThirdPartyUser(supportedProtocolKey, fields = mapOf(
"m.id.phone" to phoneNumber
)).firstOrNull()
session.thirdPartyService().getThirdPartyUser(
protocol = supportedProtocolKey,
fields = mapOf("m.id.phone" to phoneNumber)
).firstOrNull()
} ?: throw Failure()
val roomId = directRoomHelper.ensureDMExists(thirdPartyUser.userId)

View file

@ -1,43 +0,0 @@
/*
* Copyright (c) 2021 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.app.features.call.webrtc
import kotlinx.coroutines.delay
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
private const val PSTN_VECTOR_KEY = "im.vector.protocol.pstn"
private const val PSTN_MATRIX_KEY = "m.protocol.pstn"
suspend fun Session.getSupportedPSTN(maxTries: Int): String? {
val thirdPartyProtocols: Map<String, ThirdPartyProtocol> = try {
thirdPartyService().getThirdPartyProtocols()
} catch (failure: Throwable) {
if (maxTries == 1) {
return null
} else {
// Wait for 10s before trying again
delay(10_000L)
return getSupportedPSTN(maxTries - 1)
}
}
return when {
thirdPartyProtocols.containsKey(PSTN_VECTOR_KEY) -> PSTN_VECTOR_KEY
thirdPartyProtocols.containsKey(PSTN_MATRIX_KEY) -> PSTN_MATRIX_KEY
else -> null
}
}

View file

@ -53,7 +53,6 @@ import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent
import org.matrix.android.sdk.api.session.room.model.call.SdpType
import org.matrix.android.sdk.internal.util.awaitCallback
import org.threeten.bp.Duration
import org.webrtc.AudioSource
import org.webrtc.AudioTrack
@ -420,9 +419,7 @@ class WebRtcCall(val mxCall: MxCall,
private suspend fun getTurnServer(): TurnServerResponse? {
return tryOrNull {
awaitCallback {
sessionProvider.get()?.callSignalingService()?.getTurnServer(it)
}
sessionProvider.get()?.callSignalingService()?.getTurnServer()
}
}

View file

@ -26,14 +26,13 @@ import im.vector.app.features.call.VectorCallActivity
import im.vector.app.features.call.audio.CallAudioManager
import im.vector.app.features.call.utils.EglUtils
import im.vector.app.push.fcm.FcmHelper
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.call.CallListener
import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.MxCall
import org.matrix.android.sdk.api.session.call.PSTNProtocolChecker
import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
@ -65,22 +64,26 @@ class WebRtcCallManager @Inject constructor(
private val currentSession: Session?
get() = activeSessionDataSource.currentValue?.orNull()
private val pstnProtocolChecker: PSTNProtocolChecker?
get() = currentSession?.callSignalingService()?.getPSTNProtocolChecker()
interface CurrentCallListener {
fun onCurrentCallChange(call: WebRtcCall?) {}
fun onAudioDevicesChange() {}
}
interface PSTNSupportListener {
fun onPSTNSupportUpdated()
val supportedPSTNProtocol: String?
get() = pstnProtocolChecker?.supportedPSTNProtocol
val supportsPSTNProtocol: Boolean
get() = supportedPSTNProtocol != null
fun addPstnSupportListener(listener: PSTNProtocolChecker.Listener) {
pstnProtocolChecker?.addListener(listener)
}
private val pstnSupportListeners = emptyList<PSTNSupportListener>().toMutableList()
fun addPstnSupportListener(listener: PSTNSupportListener) {
pstnSupportListeners.add(listener)
}
fun removePstnSupportListener(listener: PSTNSupportListener) {
pstnSupportListeners.remove(listener)
fun removePstnSupportListener(listener: PSTNProtocolChecker.Listener) {
pstnProtocolChecker?.removeListener(listener)
}
private val currentCallsListeners = CopyOnWriteArrayList<CurrentCallListener>()
@ -104,27 +107,11 @@ class WebRtcCallManager @Inject constructor(
private var peerConnectionFactory: PeerConnectionFactory? = null
private val executor = Executors.newSingleThreadExecutor()
private val dispatcher = executor.asCoroutineDispatcher()
var supportedPSTNProtocol: String? = null
private set
val supportsPSTNProtocol: Boolean
get() = supportedPSTNProtocol != null
private val rootEglBase by lazy { EglUtils.rootEglBase }
private var isInBackground: Boolean = true
init {
GlobalScope.launch {
supportedPSTNProtocol = currentSession?.getSupportedPSTN(3)
if (supportedPSTNProtocol != null) {
pstnSupportListeners.forEach {
tryOrNull { it.onPSTNSupportUpdated() }
}
}
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun entersForeground() {
isInBackground = false
@ -167,6 +154,10 @@ class WebRtcCallManager @Inject constructor(
return callsByCallId.values.toList()
}
fun checkForPSTNSupportIfNeeded() {
pstnProtocolChecker?.checkForPSTNSupportIfNeeded()
}
/**
* @return a set of all advertised call during the lifetime of the app.
*/
@ -176,7 +167,6 @@ class WebRtcCallManager @Inject constructor(
Timber.v("## VOIP headSetButtonTapped")
val call = getCurrentCall() ?: return
if (call.mxCall.state is CallState.LocalRinging) {
// accept call
call.acceptIncomingCall()
}
if (call.mxCall.state is CallState.Connected) {

View file

@ -52,6 +52,7 @@ import androidx.core.view.ViewCompat
import androidx.core.view.forEach
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -1043,7 +1044,7 @@ class RoomDetailFragment @Inject constructor(
}
private fun updateJumpToReadMarkerViewVisibility() {
views.jumpToReadMarkerView.post {
viewLifecycleOwner.lifecycleScope.launchWhenResumed {
withState(roomDetailViewModel) {
val showJumpToUnreadBanner = when (it.unreadState) {
UnreadState.Unknown,

View file

@ -26,8 +26,8 @@ import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay
import com.jakewharton.rxrelay2.PublishRelay
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
@ -64,6 +64,7 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.call.PSTNProtocolChecker
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.LocalEcho
@ -120,7 +121,7 @@ class RoomDetailViewModel @AssistedInject constructor(
private val directRoomHelper: DirectRoomHelper,
timelineSettingsFactory: TimelineSettingsFactory
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
Timeline.Listener, ChatEffectManager.Delegate, WebRtcCallManager.PSTNSupportListener {
Timeline.Listener, ChatEffectManager.Delegate, PSTNProtocolChecker.Listener {
private val room = session.getRoom(initialState.roomId)!!
private val eventId = initialState.eventId
@ -176,6 +177,7 @@ class RoomDetailViewModel @AssistedInject constructor(
// Inform the SDK that the room is displayed
session.onRoomDisplayed(initialState.roomId)
callManager.addPstnSupportListener(this)
callManager.checkForPSTNSupportIfNeeded()
chatEffectManager.delegate = this
}
@ -231,65 +233,65 @@ class RoomDetailViewModel @AssistedInject constructor(
override fun handle(action: RoomDetailAction) {
when (action) {
is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action)
is RoomDetailAction.SaveDraft -> handleSaveDraft(action)
is RoomDetailAction.SendMessage -> handleSendMessage(action)
is RoomDetailAction.SendMedia -> handleSendMedia(action)
is RoomDetailAction.SendSticker -> handleSendSticker(action)
is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action)
is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action)
is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action)
is RoomDetailAction.SendReaction -> handleSendReaction(action)
is RoomDetailAction.AcceptInvite -> handleAcceptInvite()
is RoomDetailAction.RejectInvite -> handleRejectInvite()
is RoomDetailAction.RedactAction -> handleRedactEvent(action)
is RoomDetailAction.UndoReaction -> handleUndoReact(action)
is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
is RoomDetailAction.EnterRegularMode -> handleEnterRegularMode(action)
is RoomDetailAction.EnterEditMode -> handleEditAction(action)
is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action)
is RoomDetailAction.EnterReplyMode -> handleReplyAction(action)
is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action)
is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action)
is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action)
is RoomDetailAction.ResendMessage -> handleResendEvent(action)
is RoomDetailAction.RemoveFailedEcho -> handleRemove(action)
is RoomDetailAction.ResendAll -> handleResendAll()
is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead()
is RoomDetailAction.ReportContent -> handleReportContent(action)
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action)
is RoomDetailAction.SaveDraft -> handleSaveDraft(action)
is RoomDetailAction.SendMessage -> handleSendMessage(action)
is RoomDetailAction.SendMedia -> handleSendMedia(action)
is RoomDetailAction.SendSticker -> handleSendSticker(action)
is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action)
is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action)
is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action)
is RoomDetailAction.SendReaction -> handleSendReaction(action)
is RoomDetailAction.AcceptInvite -> handleAcceptInvite()
is RoomDetailAction.RejectInvite -> handleRejectInvite()
is RoomDetailAction.RedactAction -> handleRedactEvent(action)
is RoomDetailAction.UndoReaction -> handleUndoReact(action)
is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
is RoomDetailAction.EnterRegularMode -> handleEnterRegularMode(action)
is RoomDetailAction.EnterEditMode -> handleEditAction(action)
is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action)
is RoomDetailAction.EnterReplyMode -> handleReplyAction(action)
is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action)
is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action)
is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action)
is RoomDetailAction.ResendMessage -> handleResendEvent(action)
is RoomDetailAction.RemoveFailedEcho -> handleRemove(action)
is RoomDetailAction.ResendAll -> handleResendAll()
is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead()
is RoomDetailAction.ReportContent -> handleReportContent(action)
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages()
is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages()
is RoomDetailAction.ReplyToOptions -> handleReplyToOptions(action)
is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action)
is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
is RoomDetailAction.RequestVerification -> handleRequestVerification(action)
is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action)
is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action)
is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action)
is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment()
is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager()
is RoomDetailAction.StartCallWithPhoneNumber -> handleStartCallWithPhoneNumber(action)
is RoomDetailAction.StartCall -> handleStartCall(action)
is RoomDetailAction.AcceptCall -> handleAcceptCall(action)
is RoomDetailAction.EndCall -> handleEndCall()
is RoomDetailAction.ManageIntegrations -> handleManageIntegrations()
is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action)
is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId)
is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action)
is RoomDetailAction.CancelSend -> handleCancel(action)
is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action)
is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action)
RoomDetailAction.QuickActionInvitePeople -> handleInvitePeople()
RoomDetailAction.QuickActionSetAvatar -> handleQuickSetAvatar()
is RoomDetailAction.SetAvatarAction -> handleSetNewAvatar(action)
RoomDetailAction.QuickActionSetTopic -> _viewEvents.post(RoomDetailViewEvents.OpenRoomSettings)
is RoomDetailAction.ShowRoomAvatarFullScreen -> {
is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages()
is RoomDetailAction.ReplyToOptions -> handleReplyToOptions(action)
is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action)
is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
is RoomDetailAction.RequestVerification -> handleRequestVerification(action)
is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action)
is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action)
is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action)
is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment()
is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager()
is RoomDetailAction.StartCallWithPhoneNumber -> handleStartCallWithPhoneNumber(action)
is RoomDetailAction.StartCall -> handleStartCall(action)
is RoomDetailAction.AcceptCall -> handleAcceptCall(action)
is RoomDetailAction.EndCall -> handleEndCall()
is RoomDetailAction.ManageIntegrations -> handleManageIntegrations()
is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action)
is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId)
is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action)
is RoomDetailAction.CancelSend -> handleCancel(action)
is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action)
is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action)
RoomDetailAction.QuickActionInvitePeople -> handleInvitePeople()
RoomDetailAction.QuickActionSetAvatar -> handleQuickSetAvatar()
is RoomDetailAction.SetAvatarAction -> handleSetNewAvatar(action)
RoomDetailAction.QuickActionSetTopic -> _viewEvents.post(RoomDetailViewEvents.OpenRoomSettings)
is RoomDetailAction.ShowRoomAvatarFullScreen -> {
_viewEvents.post(
RoomDetailViewEvents.ShowRoomAvatarFullScreen(action.matrixItem, action.transitionView)
)
}
is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action)
is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action)
}.exhaustive
}
@ -618,10 +620,10 @@ class RoomDetailViewModel @AssistedInject constructor(
return@withState false
}
when (itemId) {
R.id.resend_all -> state.asyncRoomSummary()?.hasFailedSending == true
R.id.resend_all -> state.asyncRoomSummary()?.hasFailedSending == true
R.id.timeline_setting -> true
R.id.invite -> state.canInvite
R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true
R.id.invite -> state.canInvite
R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true
R.id.open_matrix_apps -> true
R.id.voice_call,
R.id.video_call -> callManager.getCallsByRoomId(state.roomId).isEmpty()
@ -741,7 +743,7 @@ class RoomDetailViewModel @AssistedInject constructor(
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.SendChatEffect -> {
is ParsedCommand.SendChatEffect -> {
sendChatEffect(slashCommandResult)
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft()
@ -774,7 +776,7 @@ class RoomDetailViewModel @AssistedInject constructor(
}
}.exhaustive
}
is SendMode.EDIT -> {
is SendMode.EDIT -> {
// is original event a reply?
val inReplyTo = state.sendMode.timelineEvent.getRelationContent()?.inReplyTo?.eventId
if (inReplyTo != null) {
@ -799,7 +801,7 @@ class RoomDetailViewModel @AssistedInject constructor(
_viewEvents.post(RoomDetailViewEvents.MessageSent)
popDraft()
}
is SendMode.QUOTE -> {
is SendMode.QUOTE -> {
val messageContent: MessageContent? =
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
@ -822,7 +824,7 @@ class RoomDetailViewModel @AssistedInject constructor(
_viewEvents.post(RoomDetailViewEvents.MessageSent)
popDraft()
}
is SendMode.REPLY -> {
is SendMode.REPLY -> {
state.sendMode.timelineEvent.let {
room.replyToMessage(it, action.text.toString(), action.autoMarkdown)
_viewEvents.post(RoomDetailViewEvents.MessageSent)
@ -1441,7 +1443,7 @@ class RoomDetailViewModel @AssistedInject constructor(
}
override fun onPSTNSupportUpdated() {
updateShowDialerOptionState()
updateShowDialerOptionState()
}
private fun updateShowDialerOptionState() {

View file

@ -19,7 +19,7 @@ package im.vector.app.features.home.room.detail
import androidx.recyclerview.widget.LinearLayoutManager
import im.vector.app.core.platform.DefaultListUpdateCallback
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.item.BaseEventItem
import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents
import timber.log.Timber
import java.util.concurrent.CopyOnWriteArrayList
@ -47,8 +47,8 @@ class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager,
if (layoutManager.findFirstVisibleItemPosition() != position) {
return
}
val firstNewItem = timelineEventController.adapter.getModelAtPosition(position) as? BaseEventItem ?: return
val firstNewItemIds = firstNewItem.getEventIds().firstOrNull()
val firstNewItem = timelineEventController.adapter.getModelAtPosition(position) as? ItemWithEvents ?: return
val firstNewItemIds = firstNewItem.getEventIds().firstOrNull() ?: return
val indexOfFirstNewItem = newTimelineEventIds.indexOf(firstNewItemIds)
if (indexOfFirstNewItem != -1) {
Timber.v("Should scroll to position: $position")

View file

@ -38,18 +38,15 @@ import im.vector.app.features.home.room.detail.timeline.factory.MergedHeaderItem
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactory
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.helper.ReadMarkerVisibilityStateChangedListener
import im.vector.app.features.home.room.detail.timeline.helper.TimelineControllerInterceptorHelper
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener
import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.app.features.home.room.detail.timeline.item.BaseEventItem
import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
import im.vector.app.features.media.ImageContentRenderer
import im.vector.app.features.media.VideoContentRenderer
@ -194,75 +191,20 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
}
}
private val interceptorHelper = TimelineControllerInterceptorHelper(
::positionOfReadMarker,
adapterPositionMapping,
vectorPreferences,
callManager
)
init {
addInterceptor(this)
requestModelBuild()
}
// Update position when we are building new items
override fun intercept(models: MutableList<EpoxyModel<*>>) = synchronized(modelCache) {
positionOfReadMarker = null
adapterPositionMapping.clear()
val callIds = mutableSetOf<String>()
val modelsIterator = models.listIterator()
val showHiddenEvents = vectorPreferences.shouldShowHiddenEvents()
modelsIterator.withIndex().forEach {
val index = it.index
val epoxyModel = it.value
if (epoxyModel is CallTileTimelineItem) {
val callId = epoxyModel.attributes.callId
// We should remove the call tile if we already have one for this call or
// if this is an active call tile without an actual call (which can happen with permalink)
val shouldRemoveCallItem = callIds.contains(callId)
|| (!callManager.getAdvertisedCalls().contains(callId) && epoxyModel.attributes.callStatus.isActive())
if (shouldRemoveCallItem && !showHiddenEvents) {
modelsIterator.remove()
return@forEach
}
callIds.add(callId)
}
if (epoxyModel is BaseEventItem) {
epoxyModel.getEventIds().forEach { eventId ->
adapterPositionMapping[eventId] = index
}
}
}
val currentUnreadState = this.unreadState
if (currentUnreadState is UnreadState.HasUnread) {
val position = adapterPositionMapping[currentUnreadState.firstUnreadEventId]?.plus(1)
positionOfReadMarker = position
if (position != null) {
val readMarker = TimelineReadMarkerItem_()
.also {
it.id("read_marker")
it.setOnVisibilityStateChanged(ReadMarkerVisibilityStateChangedListener(callback))
}
models.add(position, readMarker)
}
}
val shouldAddBackwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.BACKWARDS) ?: false
if (shouldAddBackwardPrefetch) {
val indexOfPrefetchBackward = (previousModelsSize - 1)
.coerceAtMost(models.size - DEFAULT_PREFETCH_THRESHOLD)
.coerceAtLeast(0)
val loadingItem = LoadingItem_()
.id("prefetch_backward_loading${System.currentTimeMillis()}")
.showLoader(false)
.setVisibilityStateChangedListener(Timeline.Direction.BACKWARDS)
models.add(indexOfPrefetchBackward, loadingItem)
}
val shouldAddForwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.FORWARDS) ?: false
if (shouldAddForwardPrefetch) {
val indexOfPrefetchForward = DEFAULT_PREFETCH_THRESHOLD.coerceAtMost(models.size - 1)
val loadingItem = LoadingItem_()
.id("prefetch_forward_loading${System.currentTimeMillis()}")
.showLoader(false)
.setVisibilityStateChangedListener(Timeline.Direction.FORWARDS)
models.add(indexOfPrefetchForward, loadingItem)
}
previousModelsSize = models.size
interceptorHelper.intercept(models, unreadState, timeline, callback)
}
fun update(viewState: RoomDetailViewState) {
@ -431,6 +373,14 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
}
}
private fun LoadingItem_.setVisibilityStateChangedListener(direction: Timeline.Direction): LoadingItem_ {
return onVisibilityStateChanged { _, _, visibilityState ->
if (visibilityState == VisibilityState.VISIBLE) {
callback?.onLoadMore(direction)
}
}
}
private fun updateUTDStates(event: TimelineEvent, nextEvent: TimelineEvent?) {
if (vectorPreferences.labShowCompleteHistoryInEncryptedRoom()) {
return
@ -461,14 +411,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
return shouldAdd
}
private fun LoadingItem_.setVisibilityStateChangedListener(direction: Timeline.Direction): LoadingItem_ {
return onVisibilityStateChanged { _, _, visibilityState ->
if (visibilityState == VisibilityState.VISIBLE) {
callback?.onLoadMore(direction)
}
}
}
fun searchPositionOfEvent(eventId: String?): Int? = synchronized(modelCache) {
return adapterPositionMapping[eventId]
}

View file

@ -16,7 +16,8 @@
package im.vector.app.features.home.room.detail.timeline.factory
import im.vector.app.core.epoxy.EmptyItem_
import im.vector.app.core.epoxy.TimelineEmptyItem
import im.vector.app.core.epoxy.TimelineEmptyItem_
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
@ -114,6 +115,12 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
Timber.e(throwable, "failed to create message item")
defaultItemFactory.create(event, highlight, callback, throwable)
}
return (computedModel ?: EmptyItem_())
return computedModel ?: buildEmptyItem(event)
}
private fun buildEmptyItem(timelineEvent: TimelineEvent): TimelineEmptyItem {
return TimelineEmptyItem_()
.id(timelineEvent.localId)
.eventId(timelineEvent.eventId)
}
}

View file

@ -0,0 +1,151 @@
/*
* Copyright (c) 2021 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.app.features.home.room.detail.timeline.helper
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.VisibilityState
import im.vector.app.core.epoxy.LoadingItem_
import im.vector.app.core.epoxy.TimelineEmptyItem_
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.room.detail.UnreadState
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem
import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents
import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_
import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import kotlin.reflect.KMutableProperty0
private const val DEFAULT_PREFETCH_THRESHOLD = 30
class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMutableProperty0<Int?>,
private val adapterPositionMapping: MutableMap<String, Int>,
private val vectorPreferences: VectorPreferences,
private val callManager: WebRtcCallManager
) {
private var previousModelsSize = 0
// Update position when we are building new items
fun intercept(
models: MutableList<EpoxyModel<*>>,
unreadState: UnreadState,
timeline: Timeline?,
callback: TimelineEventController.Callback?
) {
positionOfReadMarker.set(null)
adapterPositionMapping.clear()
val callIds = mutableSetOf<String>()
// Add some prefetch loader if needed
models.addBackwardPrefetchIfNeeded(timeline, callback)
models.addForwardPrefetchIfNeeded(timeline, callback)
val modelsIterator = models.listIterator()
val showHiddenEvents = vectorPreferences.shouldShowHiddenEvents()
var index = 0
val firstUnreadEventId = (unreadState as? UnreadState.HasUnread)?.firstUnreadEventId
// Then iterate on models so we have the exact positions in the adapter
modelsIterator.forEach { epoxyModel ->
if (epoxyModel is ItemWithEvents) {
epoxyModel.getEventIds().forEach { eventId ->
adapterPositionMapping[eventId] = index
if (eventId == firstUnreadEventId) {
modelsIterator.addReadMarkerItem(callback)
index++
positionOfReadMarker.set(index)
}
}
}
if (epoxyModel is CallTileTimelineItem) {
modelsIterator.removeCallItemIfNeeded(epoxyModel, callIds, showHiddenEvents)
}
index++
}
previousModelsSize = models.size
}
private fun MutableListIterator<EpoxyModel<*>>.addReadMarkerItem(callback: TimelineEventController.Callback?) {
val readMarker = TimelineReadMarkerItem_()
.also {
it.id("read_marker")
it.setOnVisibilityStateChanged(ReadMarkerVisibilityStateChangedListener(callback))
}
add(readMarker)
// Use next as we still have some process to do before the next iterator loop
next()
}
private fun MutableListIterator<EpoxyModel<*>>.removeCallItemIfNeeded(
epoxyModel: CallTileTimelineItem,
callIds: MutableSet<String>,
showHiddenEvents: Boolean
) {
val callId = epoxyModel.attributes.callId
// We should remove the call tile if we already have one for this call or
// if this is an active call tile without an actual call (which can happen with permalink)
val shouldRemoveCallItem = callIds.contains(callId)
|| (!callManager.getAdvertisedCalls().contains(callId) && epoxyModel.attributes.callStatus.isActive())
if (shouldRemoveCallItem && !showHiddenEvents) {
remove()
val emptyItem = TimelineEmptyItem_()
.id(epoxyModel.id())
.eventId(epoxyModel.attributes.informationData.eventId)
add(emptyItem)
}
callIds.add(callId)
}
private fun MutableList<EpoxyModel<*>>.addBackwardPrefetchIfNeeded(timeline: Timeline?, callback: TimelineEventController.Callback?) {
val shouldAddBackwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.BACKWARDS) ?: false
if (shouldAddBackwardPrefetch) {
val indexOfPrefetchBackward = (previousModelsSize - 1)
.coerceAtMost(size - DEFAULT_PREFETCH_THRESHOLD)
.coerceAtLeast(0)
val loadingItem = LoadingItem_()
.id("prefetch_backward_loading${System.currentTimeMillis()}")
.showLoader(false)
.setVisibilityStateChangedListener(Timeline.Direction.BACKWARDS, callback)
add(indexOfPrefetchBackward, loadingItem)
}
}
private fun MutableList<EpoxyModel<*>>.addForwardPrefetchIfNeeded(timeline: Timeline?, callback: TimelineEventController.Callback?) {
val shouldAddForwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.FORWARDS) ?: false
if (shouldAddForwardPrefetch) {
val indexOfPrefetchForward = DEFAULT_PREFETCH_THRESHOLD.coerceAtMost(size - 1)
val loadingItem = LoadingItem_()
.id("prefetch_forward_loading${System.currentTimeMillis()}")
.showLoader(false)
.setVisibilityStateChangedListener(Timeline.Direction.FORWARDS, callback)
add(indexOfPrefetchForward, loadingItem)
}
}
private fun LoadingItem_.setVisibilityStateChangedListener(
direction: Timeline.Direction,
callback: TimelineEventController.Callback?
): LoadingItem_ {
return onVisibilityStateChanged { _, _, visibilityState ->
if (visibilityState == VisibilityState.VISIBLE) {
callback?.onLoadMore(direction)
}
}
}
}

View file

@ -32,7 +32,7 @@ import im.vector.app.core.utils.DimensionConverter
/**
* Children must override getViewType()
*/
abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>() {
abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>(), ItemWithEvents {
// To use for instance when opening a permalink with an eventId
@EpoxyAttribute
@ -53,12 +53,6 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
holder.checkableBackground.isChecked = highlighted
}
/**
* Returns the eventIds associated with the EventItem.
* Will generally get only one, but it handles the merging items.
*/
abstract fun getEventIds(): List<String>
abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() {
val leftGuideline by bind<View>(R.id.messageStartGuideline)
val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground)

View file

@ -1,5 +1,5 @@
/*
* Copyright 2019 New Vector Ltd
* Copyright (c) 2021 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.
@ -14,12 +14,12 @@
* limitations under the License.
*/
package im.vector.app.core.epoxy
package im.vector.app.features.home.room.detail.timeline.item
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
@EpoxyModelClass(layout = R.layout.item_empty)
abstract class EmptyItem : VectorEpoxyModel<EmptyItem.Holder>() {
class Holder : VectorEpoxyHolder()
interface ItemWithEvents {
/**
* Returns the eventIds associated with the EventItem.
* Will generally get only one, but it handles the merged items.
*/
fun getEventIds(): List<String>
}

View file

@ -29,6 +29,7 @@ import androidx.core.view.ViewCompat
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.transition.Transition
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
@ -131,7 +132,7 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen
if (savedInstanceState == null) {
pager2.setCurrentItem(initialIndex, false)
// The page change listener is not notified of the change...
pager2.post {
lifecycleScope.launchWhenResumed {
onSelectedPositionChanged(initialIndex)
}
}

View file

@ -25,6 +25,7 @@ import android.view.ViewGroup
import android.widget.ScrollView
import androidx.core.view.forEach
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
@ -171,7 +172,7 @@ class UserListFragment @Inject constructor(
// Scroll to the bottom when adding chips. When removing chips, do not scroll
if (newNumberOfChips >= currentNumberOfChips) {
views.chipGroupScrollView.post {
viewLifecycleOwner.lifecycleScope.launchWhenResumed {
views.chipGroupScrollView.fullScroll(ScrollView.FOCUS_DOWN)
}
}

View file

@ -10,6 +10,7 @@
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:gravity="top|start"
android:hint="@string/dev_tools_event_content_hint"
android:inputType="textMultiLine"
android:scrollHorizontally="true"
android:textSize="12sp"

View file

@ -1,55 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="VectorStyles">
<!-- application bar text color -->
<attr name="vctr_toolbar_primary_text_color" format="color" />
<attr name="vctr_toolbar_secondary_text_color" format="color" />
<attr name="vctr_toolbar_link_text_color" format="color" />
<!-- application bar text color -->
<attr name="vctr_toolbar_primary_text_color" format="color" />
<attr name="vctr_toolbar_secondary_text_color" format="color" />
<attr name="vctr_toolbar_link_text_color" format="color" />
<!-- default text colors -->
<attr name="vctr_default_text_hint_color" format="color" />
<!-- default text colors -->
<attr name="vctr_default_text_hint_color" format="color" />
<!-- room message colors -->
<attr name="vctr_unsent_message_text_color" format="color" />
<attr name="vctr_message_text_color" format="color" />
<attr name="vctr_notice_text_color" format="color" />
<attr name="vctr_notice_secondary" format="color" />
<attr name="vctr_encrypting_message_text_color" format="color" />
<attr name="vctr_sending_message_text_color" format="color" />
<attr name="vctr_markdown_block_background_color" format="color" />
<attr name="vctr_spoiler_background_color" format="color" />
<!-- room message colors -->
<attr name="vctr_unsent_message_text_color" format="color" />
<attr name="vctr_message_text_color" format="color" />
<attr name="vctr_notice_text_color" format="color" />
<attr name="vctr_notice_secondary" format="color" />
<attr name="vctr_encrypting_message_text_color" format="color" />
<attr name="vctr_sending_message_text_color" format="color" />
<attr name="vctr_markdown_block_background_color" format="color" />
<attr name="vctr_spoiler_background_color" format="color" />
<!-- tab bar colors -->
<attr name="vctr_tab_bar_inverted_background_color" format="color" />
<!-- tab bar colors -->
<attr name="vctr_tab_bar_inverted_background_color" format="color" />
<!-- list colors -->
<attr name="vctr_list_header_background_color" format="color" />
<attr name="vctr_list_header_primary_text_color" format="color" />
<attr name="vctr_list_header_secondary_text_color" format="color" />
<!-- list colors -->
<attr name="vctr_list_header_background_color" format="color" />
<attr name="vctr_list_header_primary_text_color" format="color" />
<attr name="vctr_list_header_secondary_text_color" format="color" />
<attr name="vctr_list_divider_color" format="color" />
<attr name="vctr_list_divider_color" format="color" />
<!-- outgoing call background color -->
<attr name="vctr_pending_outgoing_view_background_color" format="color" />
<!-- outgoing call background color -->
<attr name="vctr_pending_outgoing_view_background_color" format="color" />
<!-- room notification text color (typing, unsent...) -->
<attr name="vctr_room_notification_text_color" format="color" />
<!-- room notification text color (typing, unsent...) -->
<attr name="vctr_room_notification_text_color" format="color" />
<!-- icon colors -->
<attr name="vctr_icon_tint_on_light_action_bar_color" format="color" />
<attr name="vctr_settings_icon_tint_color" format="color" />
<!-- icon colors -->
<attr name="vctr_icon_tint_on_light_action_bar_color" format="color" />
<attr name="vctr_settings_icon_tint_color" format="color" />
<attr name="vctr_social_login_button_google_style" format="reference" />
<attr name="vctr_social_login_button_github_style" format="reference" />
<attr name="vctr_social_login_button_facebook_style" format="reference" />
<attr name="vctr_social_login_button_twitter_style" format="reference" />
<attr name="vctr_social_login_button_apple_style" format="reference" />
<attr name="vctr_social_login_button_gitlab_style" format="reference" />
<attr name="vctr_social_login_button_google_style" format="reference" />
<attr name="vctr_social_login_button_github_style" format="reference" />
<attr name="vctr_social_login_button_facebook_style" format="reference" />
<attr name="vctr_social_login_button_twitter_style" format="reference" />
<attr name="vctr_social_login_button_apple_style" format="reference" />
<attr name="vctr_social_login_button_gitlab_style" format="reference" />
<attr name="vctr_chat_effect_snow_background" format="color" />
</declare-styleable>
<attr name="vctr_chat_effect_snow_background" format="color" />
<declare-styleable name="PollResultLineView">
<attr name="optionName" format="string" localization="suggested" />
@ -70,16 +67,16 @@
<declare-styleable name="SignOutBottomSheetActionButton">
<attr name="iconTint" format="color" />
<attr name="actionTitle"/>
<attr name="actionTitle" />
<attr name="leftIcon" />
<attr name="textColor" format="color" />
</declare-styleable>
<declare-styleable name="SocialLoginButtonsView">
<attr name="signMode" format="enum">
<enum name="signin" value="0"/>
<enum name="signup" value="1"/>
<enum name="continue_with" value="2"/>
<enum name="signin" value="0" />
<enum name="signup" value="1" />
<enum name="continue_with" value="2" />
</attr>
</declare-styleable>
</resources>

View file

@ -1,7 +1,115 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Source: https://zpl.io/aBKw9Mk -->
<!-- Error colors -->
<color name="vector_success_color">#70BF56</color>
<color name="vector_warning_color">#ff4b55</color>
<color name="vector_error_color">#ff4b55</color>
<color name="vector_info_color">#2f9edb</color>
<!-- main app colors -->
<color name="vector_fuchsia_color">#ff4b55</color>
<color name="vector_silver_color">#FFC7C7C7</color>
<color name="vector_dark_grey_color">#FF999999</color>
<!-- theses colours are requested a background cannot be set by an ?attr on android < 5 -->
<!-- dedicated drawables are created for each theme -->
<!-- Default/Background-->
<color name="riot_primary_background_color_light">#FFFFFFFF</color>
<!-- Dark/Background-->
<color name="riot_primary_background_color_dark">#FF181B21</color>
<!-- Black/Background-->
<color name="riot_primary_background_color_black">#F000</color>
<!--Default/Android Status Bar-->
<color name="primary_color_dark_light">#FF1A2027</color>
<!--Default/Base-->
<color name="primary_color_light">#03b381</color>
<!--Default/Accent-->
<color name="accent_color_light">#03b381</color>
<!--Dark/Android Status Bar-->
<color name="primary_color_dark_dark">#FF0D0E10</color>
<!--Dark/Base-->
<color name="primary_color_dark">#FF15171B</color>
<!--Dark/Accent-->
<color name="accent_color_dark">#03b381</color>
<!--Black/Android Status Bar-->
<color name="primary_color_dark_black">#000</color>
<!--Black/Base-->
<color name="primary_color_black">#FF060708</color>
<!--Default/Line break mobile-->
<attr name="list_divider_color" format="color" />
<color name="list_divider_color_light">#EEEFEF</color>
<!--Dark/Line break mobile-->
<color name="list_divider_color_dark">#FF61708B</color>
<!--Black/Line break mobile-->
<color name="list_divider_color_black">#FF22262E</color>
<attr name="tab_bar_selected_background_color" format="color" />
<color name="tab_bar_selected_background_color_light">@color/riotx_android_secondary_light</color>
<color name="tab_bar_selected_background_color_dark">@color/riotx_android_secondary_dark</color>
<attr name="tab_bar_unselected_background_color" format="color" />
<color name="tab_bar_unselected_background_color_light">@color/riotx_background_light</color>
<color name="tab_bar_unselected_background_color_dark">@color/riotx_background_dark</color>
<!-- Hint Colors -->
<color name="primary_hint_text_color_light">#FFFFFF</color>
<color name="primary_hint_text_color_dark">#FFFFFF</color>
<color name="default_text_hint_color_light">#903C3C3C</color>
<color name="default_text_hint_color_dark">#CCDDDDDD</color>
<!-- Text Colors -->
<attr name="riot_primary_text_color" format="color" />
<attr name="riot_primary_text_color_disabled" format="color" />
<!--Default/Text Primary-->
<color name="riot_primary_text_color_light">#FF2E2F32</color>
<color name="riot_primary_text_color_disabled_light">#FF9E9E9E</color>
<!--Default/Text Secondary-->
<color name="riot_secondary_text_color_light">#FF9E9E9E</color>
<color name="riot_tertiary_text_color_light">@color/riot_primary_text_color_light</color>
<!--Dark /Text Primary-->
<color name="riot_primary_text_color_dark">#FFEDF3FF</color>
<color name="riot_primary_text_color_disabled_dark">#FFA1B2D1</color>
<!--Dark /Text Secondary-->
<color name="riot_secondary_text_color_dark">#FFA1B2D1</color>
<color name="riot_tertiary_text_color_dark">@color/riot_primary_text_color_dark</color>
<!-- Notification view colors -->
<color name="soft_resource_limit_exceeded">#2f9edb</color>
<color name="hard_resource_limit_exceeded">@color/vector_fuchsia_color</color>
<!-- Password Strength bar colors -->
<color name="password_strength_bar_weak">#FFF56679</color>
<color name="password_strength_bar_low">#FFFFC666</color>
<color name="password_strength_bar_ok">#FFF8E71C</color>
<color name="password_strength_bar_strong">#FF7AC9A1</color>
<color name="password_strength_bar_undefined">#FF9E9E9E</color>
<!-- Button color -->
<color name="button_enabled_text_color">#FFFFFFFF</color>
<color name="button_disabled_text_color">#FFFFFFFF</color>
<color name="button_destructive_enabled_text_color">#FF4B55</color>
<color name="button_destructive_disabled_text_color">#FF4B55</color>
<color name="button_bot_enabled_text_color">#FF368BD6</color>
<color name="button_bot_disabled_text_color">#61708B</color>
<!-- Link color -->
<color name="link_color_light">#368BD6</color>
<color name="link_color_dark">#368BD6</color>
<!-- Notification (do not depends on theme) -->
<color name="notification_accent_color">#368BD6</color>
<color name="key_share_req_accent_color">#ff812d</color>
<!-- Source: https://zpl.io/aBKw9Mk -->
<!-- Accents -->
<color name="riotx_accent">#FF0DBD8B</color>
@ -38,7 +146,7 @@
<color name="riotx_username_7">#5c56f5</color>
<color name="riotx_username_8">#74d12c</color>
<!-- Other usefull color -->
<!-- Other useful color -->
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="black_alpha">#55000000</color>
@ -256,6 +364,4 @@
<color name="riotx_keys_backup_banner_accent_color_light">#FFF8E3</color>
<color name="riotx_keys_backup_banner_accent_color_dark">#22262E</color>
</resources>
</resources>

View file

@ -1,112 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Error colors -->
<color name="vector_success_color">#70BF56</color>
<color name="vector_warning_color">#ff4b55</color>
<color name="vector_error_color">#ff4b55</color>
<color name="vector_info_color">#2f9edb</color>
<!-- main app colors -->
<color name="vector_fuchsia_color">#ff4b55</color>
<color name="vector_silver_color">#FFC7C7C7</color>
<color name="vector_dark_grey_color">#FF999999</color>
<!-- theses colours are requested a background cannot be set by an ?attr on android < 5 -->
<!-- dedicated drawables are created for each theme -->
<!-- Default/Background-->
<color name="riot_primary_background_color_light">#FFFFFFFF</color>
<!-- Dark/Background-->
<color name="riot_primary_background_color_dark">#FF181B21</color>
<!-- Black/Background-->
<color name="riot_primary_background_color_black">#F000</color>
<!--Default/Android Status Bar-->
<color name="primary_color_dark_light">#FF1A2027</color>
<!--Default/Base-->
<color name="primary_color_light">#03b381</color>
<!--Default/Accent-->
<color name="accent_color_light">#03b381</color>
<!--Dark/Android Status Bar-->
<color name="primary_color_dark_dark">#FF0D0E10</color>
<!--Dark/Base-->
<color name="primary_color_dark">#FF15171B</color>
<!--Dark/Accent-->
<color name="accent_color_dark">#03b381</color>
<!--Black/Android Status Bar-->
<color name="primary_color_dark_black">#000</color>
<!--Black/Base-->
<color name="primary_color_black">#FF060708</color>
<!--Default/Line break mobile-->
<attr name="list_divider_color" format="color" />
<color name="list_divider_color_light">#EEEFEF</color>
<!--Dark/Line break mobile-->
<color name="list_divider_color_dark">#FF61708B</color>
<!--Black/Line break mobile-->
<color name="list_divider_color_black">#FF22262E</color>
<attr name="tab_bar_selected_background_color" format="color" />
<color name="tab_bar_selected_background_color_light">@color/riotx_android_secondary_light</color>
<color name="tab_bar_selected_background_color_dark">@color/riotx_android_secondary_dark</color>
<attr name="tab_bar_unselected_background_color" format="color" />
<color name="tab_bar_unselected_background_color_light">@color/riotx_background_light</color>
<color name="tab_bar_unselected_background_color_dark">@color/riotx_background_dark</color>
<!-- Hint Colors -->
<color name="primary_hint_text_color_light">#FFFFFF</color>
<color name="primary_hint_text_color_dark">#FFFFFF</color>
<color name="default_text_hint_color_light">#903C3C3C</color>
<color name="default_text_hint_color_dark">#CCDDDDDD</color>
<!-- Text Colors -->
<attr name="riot_primary_text_color" format="color" />
<attr name="riot_primary_text_color_disabled" format="color" />
<!--Default/Text Primary-->
<color name="riot_primary_text_color_light">#FF2E2F32</color>
<color name="riot_primary_text_color_disabled_light">#FF9E9E9E</color>
<!--Default/Text Secondary-->
<color name="riot_secondary_text_color_light">#FF9E9E9E</color>
<color name="riot_tertiary_text_color_light">@color/riot_primary_text_color_light</color>
<!--Dark /Text Primary-->
<color name="riot_primary_text_color_dark">#FFEDF3FF</color>
<color name="riot_primary_text_color_disabled_dark">#FFA1B2D1</color>
<!--Dark /Text Secondary-->
<color name="riot_secondary_text_color_dark">#FFA1B2D1</color>
<color name="riot_tertiary_text_color_dark">@color/riot_primary_text_color_dark</color>
<!-- Notification view colors -->
<color name="soft_resource_limit_exceeded">#2f9edb</color>
<color name="hard_resource_limit_exceeded">@color/vector_fuchsia_color</color>
<!-- Password Strength bar colors -->
<color name="password_strength_bar_weak">#FFF56679</color>
<color name="password_strength_bar_low">#FFFFC666</color>
<color name="password_strength_bar_ok">#FFF8E71C</color>
<color name="password_strength_bar_strong">#FF7AC9A1</color>
<color name="password_strength_bar_undefined">#FF9E9E9E</color>
<!-- Button color -->
<color name="button_enabled_text_color">#FFFFFFFF</color>
<color name="button_disabled_text_color">#FFFFFFFF</color>
<color name="button_destructive_enabled_text_color">#FF4B55</color>
<color name="button_destructive_disabled_text_color">#FF4B55</color>
<color name="button_bot_enabled_text_color">#FF368BD6</color>
<color name="button_bot_disabled_text_color">#61708B</color>
<!-- Link color -->
<color name="link_color_light">#368BD6</color>
<color name="link_color_dark">#368BD6</color>
<!-- Notification (do not depends on theme) -->
<color name="notification_accent_color">#368BD6</color>
<color name="key_share_req_accent_color">#ff812d</color>
</resources>

View file

@ -2873,4 +2873,5 @@
<string name="dev_tools_error_malformed_event">Malformed event</string>
<string name="dev_tools_success_event">Event sent!</string>
<string name="dev_tools_success_state_event">State event sent!</string>
<string name="dev_tools_event_content_hint">Event content</string>
</resources>