mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-03-18 04:08:44 +03:00
Merge branch 'develop' into cross_signing
This commit is contained in:
commit
a7c948815c
47 changed files with 788 additions and 270 deletions
|
@ -2,10 +2,10 @@ Changes in RiotX 0.13.0 (2020-XX-XX)
|
|||
===================================================
|
||||
|
||||
Features ✨:
|
||||
-
|
||||
- Send and render typing events (#564)
|
||||
|
||||
Improvements 🙌:
|
||||
-
|
||||
- Render events m.room.encryption and m.room.guest_access in the timeline
|
||||
|
||||
Other changes:
|
||||
-
|
||||
|
@ -17,7 +17,7 @@ Translations 🗣:
|
|||
-
|
||||
|
||||
Build 🧱:
|
||||
-
|
||||
- Change the way versionCode is computed (#827)
|
||||
|
||||
Changes in RiotX 0.12.0 (2020-01-09)
|
||||
===================================================
|
||||
|
|
|
@ -93,7 +93,8 @@ object EventType {
|
|||
STATE_ROOM_CANONICAL_ALIAS,
|
||||
STATE_ROOM_HISTORY_VISIBILITY,
|
||||
STATE_ROOM_RELATED_GROUPS,
|
||||
STATE_ROOM_PINNED_EVENT
|
||||
STATE_ROOM_PINNED_EVENT,
|
||||
STATE_ROOM_ENCRYPTION
|
||||
)
|
||||
|
||||
fun isStateEvent(type: String): Boolean {
|
||||
|
|
|
@ -22,12 +22,13 @@ import im.vector.matrix.android.api.session.room.members.MembershipService
|
|||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.relation.RelationService
|
||||
import im.vector.matrix.android.api.session.room.notification.RoomPushRuleService
|
||||
import im.vector.matrix.android.api.session.room.reporting.ReportingService
|
||||
import im.vector.matrix.android.api.session.room.read.ReadService
|
||||
import im.vector.matrix.android.api.session.room.reporting.ReportingService
|
||||
import im.vector.matrix.android.api.session.room.send.DraftService
|
||||
import im.vector.matrix.android.api.session.room.send.SendService
|
||||
import im.vector.matrix.android.api.session.room.state.StateService
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineService
|
||||
import im.vector.matrix.android.api.session.room.typing.TypingService
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
|
||||
/**
|
||||
|
@ -38,6 +39,7 @@ interface Room :
|
|||
SendService,
|
||||
DraftService,
|
||||
ReadService,
|
||||
TypingService,
|
||||
MembershipService,
|
||||
StateService,
|
||||
ReportingService,
|
||||
|
|
|
@ -28,17 +28,20 @@ fun roomMemberQueryParams(init: (RoomMemberQueryParams.Builder.() -> Unit) = {})
|
|||
*/
|
||||
data class RoomMemberQueryParams(
|
||||
val displayName: QueryStringValue,
|
||||
val memberships: List<Membership>
|
||||
val memberships: List<Membership>,
|
||||
val excludeSelf: Boolean
|
||||
) {
|
||||
|
||||
class Builder {
|
||||
|
||||
var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
|
||||
var memberships: List<Membership> = Membership.all()
|
||||
var excludeSelf: Boolean = false
|
||||
|
||||
fun build() = RoomMemberQueryParams(
|
||||
displayName = displayName,
|
||||
memberships = memberships
|
||||
memberships = memberships,
|
||||
excludeSelf = excludeSelf
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.room.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Class representing the EventType.STATE_ROOM_GUEST_ACCESS state event content
|
||||
* Ref: https://matrix.org/docs/spec/client_server/latest#m-room-guest-access
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RoomGuestAccessContent(
|
||||
// Required. Whether guests can join the room. One of: ["can_join", "forbidden"]
|
||||
@Json(name = "guest_access") val guestAccess: GuestAccess? = null
|
||||
)
|
||||
|
||||
enum class GuestAccess(val value: String) {
|
||||
@Json(name = "can_join")
|
||||
CanJoin("can_join"),
|
||||
@Json(name = "forbidden")
|
||||
Forbidden("forbidden")
|
||||
}
|
|
@ -42,7 +42,8 @@ data class RoomSummary(
|
|||
val versioningState: VersioningState = VersioningState.NONE,
|
||||
val readMarkerId: String? = null,
|
||||
val userDrafts: List<UserDraft> = emptyList(),
|
||||
var isEncrypted: Boolean
|
||||
var isEncrypted: Boolean,
|
||||
val typingRoomMemberIds: List<String> = emptyList()
|
||||
) {
|
||||
|
||||
val isVersioned: Boolean
|
||||
|
|
|
@ -28,6 +28,8 @@ import im.vector.matrix.android.api.session.room.model.PowerLevels
|
|||
import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
|
||||
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
|
||||
import im.vector.matrix.android.internal.auth.data.ThreePidMedium
|
||||
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Parameter to create a room, with facilities functions to configure it
|
||||
|
@ -88,7 +90,7 @@ class CreateRoomParams {
|
|||
* A list of state events to set in the new room.
|
||||
* This allows the user to override the default state events set in the new room.
|
||||
* The expected format of the state events are an object with type, state_key and content keys set.
|
||||
* Takes precedence over events set by presets, but gets overriden by name and topic keys.
|
||||
* Takes precedence over events set by presets, but gets overridden by name and topic keys.
|
||||
*/
|
||||
@Json(name = "initial_state")
|
||||
var initialStates: MutableList<Event>? = null
|
||||
|
@ -120,14 +122,14 @@ class CreateRoomParams {
|
|||
*
|
||||
* @param algorithm the algorithm
|
||||
*/
|
||||
fun addCryptoAlgorithm(algorithm: String) {
|
||||
if (algorithm.isNotBlank()) {
|
||||
val contentMap = HashMap<String, String>()
|
||||
contentMap["algorithm"] = algorithm
|
||||
fun enableEncryptionWithAlgorithm(algorithm: String) {
|
||||
if (algorithm == MXCRYPTO_ALGORITHM_MEGOLM) {
|
||||
val contentMap = mapOf("algorithm" to algorithm)
|
||||
|
||||
val algoEvent = Event(type = EventType.STATE_ROOM_ENCRYPTION,
|
||||
stateKey = "",
|
||||
content = contentMap.toContent()
|
||||
val algoEvent = Event(
|
||||
type = EventType.STATE_ROOM_ENCRYPTION,
|
||||
stateKey = "",
|
||||
content = contentMap.toContent()
|
||||
)
|
||||
|
||||
if (null == initialStates) {
|
||||
|
@ -135,6 +137,8 @@ class CreateRoomParams {
|
|||
} else {
|
||||
initialStates!!.add(algoEvent)
|
||||
}
|
||||
} else {
|
||||
Timber.e("Unsupported algorithm: $algorithm")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -145,15 +149,15 @@ class CreateRoomParams {
|
|||
*/
|
||||
fun setHistoryVisibility(historyVisibility: RoomHistoryVisibility?) {
|
||||
// Remove the existing value if any.
|
||||
initialStates?.removeAll { it.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY }
|
||||
initialStates?.removeAll { it.type == EventType.STATE_ROOM_HISTORY_VISIBILITY }
|
||||
|
||||
if (historyVisibility != null) {
|
||||
val contentMap = HashMap<String, RoomHistoryVisibility>()
|
||||
contentMap["history_visibility"] = historyVisibility
|
||||
val contentMap = mapOf("history_visibility" to historyVisibility)
|
||||
|
||||
val historyVisibilityEvent = Event(type = EventType.STATE_ROOM_HISTORY_VISIBILITY,
|
||||
stateKey = "",
|
||||
content = contentMap.toContent())
|
||||
val historyVisibilityEvent = Event(
|
||||
type = EventType.STATE_ROOM_HISTORY_VISIBILITY,
|
||||
stateKey = "",
|
||||
content = contentMap.toContent())
|
||||
|
||||
if (null == initialStates) {
|
||||
initialStates = mutableListOf(historyVisibilityEvent)
|
||||
|
@ -192,8 +196,8 @@ class CreateRoomParams {
|
|||
*/
|
||||
fun isDirect(): Boolean {
|
||||
return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT
|
||||
&& isDirect == true
|
||||
&& (1 == getInviteCount() || 1 == getInvite3PidCount())
|
||||
&& isDirect == true
|
||||
&& (1 == getInviteCount() || 1 == getInvite3PidCount())
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -218,8 +222,8 @@ class CreateRoomParams {
|
|||
invite3pids = ArrayList()
|
||||
}
|
||||
val pid = Invite3Pid(idServer = hsConfig.identityServerUri.host!!,
|
||||
medium = ThreePidMedium.EMAIL,
|
||||
address = id)
|
||||
medium = ThreePidMedium.EMAIL,
|
||||
address = id)
|
||||
|
||||
invite3pids!!.add(pid)
|
||||
} else if (isUserId(id)) {
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright 2020 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.matrix.android.api.session.room.typing
|
||||
|
||||
/**
|
||||
* This interface defines methods to handle typing data. It's implemented at the room level.
|
||||
*/
|
||||
interface TypingService {
|
||||
|
||||
/**
|
||||
* To call when user is typing a message in the room
|
||||
* The SDK will handle the requests scheduling to the homeserver:
|
||||
* - No more than one typing request per 10s
|
||||
* - If not called after 10s, the SDK will notify the homeserver that the user is not typing anymore
|
||||
*/
|
||||
fun userIsTyping()
|
||||
|
||||
/**
|
||||
* To call when user stops typing in the room
|
||||
* Notify immediately the homeserver that the user is not typing anymore in the room, for
|
||||
* instance when user has emptied the composer, or when the user quits the timeline screen.
|
||||
*/
|
||||
fun userStopsTyping()
|
||||
}
|
|
@ -22,7 +22,7 @@ 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.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||
import java.util.UUID
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class RoomSummaryMapper @Inject constructor(
|
||||
|
@ -71,7 +71,8 @@ internal class RoomSummaryMapper @Inject constructor(
|
|||
userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList(),
|
||||
canonicalAlias = roomSummaryEntity.canonicalAlias,
|
||||
aliases = roomSummaryEntity.aliases.toList(),
|
||||
isEncrypted = roomSummaryEntity.isEncrypted
|
||||
isEncrypted = roomSummaryEntity.isEncrypted,
|
||||
typingRoomMemberIds = roomSummaryEntity.typingUserIds.toList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,7 +44,8 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
|
|||
var aliases: RealmList<String> = RealmList(),
|
||||
// this is required for querying
|
||||
var flatAliases: String = "",
|
||||
var isEncrypted: Boolean = false
|
||||
var isEncrypted: Boolean = false,
|
||||
var typingUserIds: RealmList<String> = RealmList()
|
||||
) : RealmObject() {
|
||||
|
||||
private var membershipStr: String = Membership.NONE.name
|
||||
|
|
|
@ -32,6 +32,7 @@ import im.vector.matrix.android.api.session.room.send.DraftService
|
|||
import im.vector.matrix.android.api.session.room.send.SendService
|
||||
import im.vector.matrix.android.api.session.room.state.StateService
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineService
|
||||
import im.vector.matrix.android.api.session.room.typing.TypingService
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.api.util.toOptional
|
||||
import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
|
||||
|
@ -49,6 +50,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
|
|||
private val stateService: StateService,
|
||||
private val reportingService: ReportingService,
|
||||
private val readService: ReadService,
|
||||
private val typingService: TypingService,
|
||||
private val cryptoService: CryptoService,
|
||||
private val relationService: RelationService,
|
||||
private val roomMembersService: MembershipService,
|
||||
|
@ -60,6 +62,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
|
|||
StateService by stateService,
|
||||
ReportingService by reportingService,
|
||||
ReadService by readService,
|
||||
TypingService by typingService,
|
||||
RelationService by relationService,
|
||||
MembershipService by roomMembersService,
|
||||
RoomPushRuleService by roomPushRuleService {
|
||||
|
|
|
@ -18,13 +18,13 @@ package im.vector.matrix.android.internal.session.room
|
|||
|
||||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.internal.session.room.alias.RoomAliasDescription
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse
|
||||
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams
|
||||
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsResponse
|
||||
import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol
|
||||
import im.vector.matrix.android.internal.network.NetworkConstants
|
||||
import im.vector.matrix.android.internal.session.room.alias.RoomAliasDescription
|
||||
import im.vector.matrix.android.internal.session.room.membership.RoomMembersResponse
|
||||
import im.vector.matrix.android.internal.session.room.membership.joining.InviteBody
|
||||
import im.vector.matrix.android.internal.session.room.relation.RelationsResponse
|
||||
|
@ -32,6 +32,7 @@ import im.vector.matrix.android.internal.session.room.reporting.ReportContentBod
|
|||
import im.vector.matrix.android.internal.session.room.send.SendResponse
|
||||
import im.vector.matrix.android.internal.session.room.timeline.EventContextResponse
|
||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationResponse
|
||||
import im.vector.matrix.android.internal.session.room.typing.TypingBody
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.*
|
||||
|
||||
|
@ -268,4 +269,12 @@ internal interface RoomAPI {
|
|||
*/
|
||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}")
|
||||
fun getRoomIdByAlias(@Path("roomAlias") roomAlias: String): Call<RoomAliasDescription>
|
||||
|
||||
/**
|
||||
* Inform that the user is starting to type or has stopped typing
|
||||
*/
|
||||
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/typing/{userId}")
|
||||
fun sendTypingState(@Path("roomId") roomId: String,
|
||||
@Path("userId") userId: String,
|
||||
@Body body: TypingBody): Call<Unit>
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.session.room.reporting.DefaultReporting
|
|||
import im.vector.matrix.android.internal.session.room.send.DefaultSendService
|
||||
import im.vector.matrix.android.internal.session.room.state.DefaultStateService
|
||||
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService
|
||||
import im.vector.matrix.android.internal.session.room.typing.DefaultTypingService
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface RoomFactory {
|
||||
|
@ -46,6 +47,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
|
|||
private val stateServiceFactory: DefaultStateService.Factory,
|
||||
private val reportingServiceFactory: DefaultReportingService.Factory,
|
||||
private val readServiceFactory: DefaultReadService.Factory,
|
||||
private val typingServiceFactory: DefaultTypingService.Factory,
|
||||
private val relationServiceFactory: DefaultRelationService.Factory,
|
||||
private val membershipServiceFactory: DefaultMembershipService.Factory,
|
||||
private val roomPushRuleServiceFactory: DefaultRoomPushRuleService.Factory) :
|
||||
|
@ -62,6 +64,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
|
|||
stateServiceFactory.create(roomId),
|
||||
reportingServiceFactory.create(roomId),
|
||||
readServiceFactory.create(roomId),
|
||||
typingServiceFactory.create(roomId),
|
||||
cryptoService,
|
||||
relationServiceFactory.create(roomId),
|
||||
membershipServiceFactory.create(roomId),
|
||||
|
|
|
@ -52,6 +52,8 @@ import im.vector.matrix.android.internal.session.room.reporting.ReportContentTas
|
|||
import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask
|
||||
import im.vector.matrix.android.internal.session.room.state.SendStateTask
|
||||
import im.vector.matrix.android.internal.session.room.timeline.*
|
||||
import im.vector.matrix.android.internal.session.room.typing.DefaultSendTypingTask
|
||||
import im.vector.matrix.android.internal.session.room.typing.SendTypingTask
|
||||
import retrofit2.Retrofit
|
||||
|
||||
@Module
|
||||
|
@ -68,74 +70,77 @@ internal abstract class RoomModule {
|
|||
}
|
||||
|
||||
@Binds
|
||||
abstract fun bindRoomFactory(roomFactory: DefaultRoomFactory): RoomFactory
|
||||
abstract fun bindRoomFactory(factory: DefaultRoomFactory): RoomFactory
|
||||
|
||||
@Binds
|
||||
abstract fun bindRoomService(roomService: DefaultRoomService): RoomService
|
||||
abstract fun bindRoomService(service: DefaultRoomService): RoomService
|
||||
|
||||
@Binds
|
||||
abstract fun bindRoomDirectoryService(roomDirectoryService: DefaultRoomDirectoryService): RoomDirectoryService
|
||||
abstract fun bindRoomDirectoryService(service: DefaultRoomDirectoryService): RoomDirectoryService
|
||||
|
||||
@Binds
|
||||
abstract fun bindEventRelationsAggregationTask(eventRelationsAggregationTask: DefaultEventRelationsAggregationTask): EventRelationsAggregationTask
|
||||
abstract fun bindFileService(service: DefaultFileService): FileService
|
||||
|
||||
@Binds
|
||||
abstract fun bindCreateRoomTask(createRoomTask: DefaultCreateRoomTask): CreateRoomTask
|
||||
abstract fun bindEventRelationsAggregationTask(task: DefaultEventRelationsAggregationTask): EventRelationsAggregationTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindGetPublicRoomTask(getPublicRoomTask: DefaultGetPublicRoomTask): GetPublicRoomTask
|
||||
abstract fun bindCreateRoomTask(task: DefaultCreateRoomTask): CreateRoomTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindGetThirdPartyProtocolsTask(getThirdPartyProtocolsTask: DefaultGetThirdPartyProtocolsTask): GetThirdPartyProtocolsTask
|
||||
abstract fun bindGetPublicRoomTask(task: DefaultGetPublicRoomTask): GetPublicRoomTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindInviteTask(inviteTask: DefaultInviteTask): InviteTask
|
||||
abstract fun bindGetThirdPartyProtocolsTask(task: DefaultGetThirdPartyProtocolsTask): GetThirdPartyProtocolsTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindJoinRoomTask(joinRoomTask: DefaultJoinRoomTask): JoinRoomTask
|
||||
abstract fun bindInviteTask(task: DefaultInviteTask): InviteTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindLeaveRoomTask(leaveRoomTask: DefaultLeaveRoomTask): LeaveRoomTask
|
||||
abstract fun bindJoinRoomTask(task: DefaultJoinRoomTask): JoinRoomTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindLoadRoomMembersTask(loadRoomMembersTask: DefaultLoadRoomMembersTask): LoadRoomMembersTask
|
||||
abstract fun bindLeaveRoomTask(task: DefaultLeaveRoomTask): LeaveRoomTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindPruneEventTask(pruneEventTask: DefaultPruneEventTask): PruneEventTask
|
||||
abstract fun bindLoadRoomMembersTask(task: DefaultLoadRoomMembersTask): LoadRoomMembersTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindSetReadMarkersTask(setReadMarkersTask: DefaultSetReadMarkersTask): SetReadMarkersTask
|
||||
abstract fun bindPruneEventTask(task: DefaultPruneEventTask): PruneEventTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindMarkAllRoomsReadTask(markAllRoomsReadTask: DefaultMarkAllRoomsReadTask): MarkAllRoomsReadTask
|
||||
abstract fun bindSetReadMarkersTask(task: DefaultSetReadMarkersTask): SetReadMarkersTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindFindReactionEventForUndoTask(findReactionEventForUndoTask: DefaultFindReactionEventForUndoTask): FindReactionEventForUndoTask
|
||||
abstract fun bindMarkAllRoomsReadTask(task: DefaultMarkAllRoomsReadTask): MarkAllRoomsReadTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindUpdateQuickReactionTask(updateQuickReactionTask: DefaultUpdateQuickReactionTask): UpdateQuickReactionTask
|
||||
abstract fun bindFindReactionEventForUndoTask(task: DefaultFindReactionEventForUndoTask): FindReactionEventForUndoTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindSendStateTask(sendStateTask: DefaultSendStateTask): SendStateTask
|
||||
abstract fun bindUpdateQuickReactionTask(task: DefaultUpdateQuickReactionTask): UpdateQuickReactionTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindReportContentTask(reportContentTask: DefaultReportContentTask): ReportContentTask
|
||||
abstract fun bindSendStateTask(task: DefaultSendStateTask): SendStateTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindGetContextOfEventTask(getContextOfEventTask: DefaultGetContextOfEventTask): GetContextOfEventTask
|
||||
abstract fun bindReportContentTask(task: DefaultReportContentTask): ReportContentTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindClearUnlinkedEventsTask(clearUnlinkedEventsTask: DefaultClearUnlinkedEventsTask): ClearUnlinkedEventsTask
|
||||
abstract fun bindGetContextOfEventTask(task: DefaultGetContextOfEventTask): GetContextOfEventTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindPaginationTask(paginationTask: DefaultPaginationTask): PaginationTask
|
||||
abstract fun bindClearUnlinkedEventsTask(task: DefaultClearUnlinkedEventsTask): ClearUnlinkedEventsTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindFileService(fileService: DefaultFileService): FileService
|
||||
abstract fun bindPaginationTask(task: DefaultPaginationTask): PaginationTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindFetchEditHistoryTask(fetchEditHistoryTask: DefaultFetchEditHistoryTask): FetchEditHistoryTask
|
||||
abstract fun bindFetchEditHistoryTask(task: DefaultFetchEditHistoryTask): FetchEditHistoryTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindGetRoomIdByAliasTask(getRoomIdByAliasTask: DefaultGetRoomIdByAliasTask): GetRoomIdByAliasTask
|
||||
abstract fun bindGetRoomIdByAliasTask(task: DefaultGetRoomIdByAliasTask): GetRoomIdByAliasTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindSendTypingTask(task: DefaultSendTypingTask): SendTypingTask
|
||||
}
|
||||
|
|
|
@ -24,14 +24,15 @@ import im.vector.matrix.android.api.session.room.model.RoomAliasesContent
|
|||
import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent
|
||||
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
|
||||
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
||||
import im.vector.matrix.android.internal.database.model.*
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomMemberEntityFields
|
||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||
import im.vector.matrix.android.internal.database.query.*
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.session.room.membership.RoomDisplayNameResolver
|
||||
import im.vector.matrix.android.internal.session.room.membership.RoomMembers
|
||||
import im.vector.matrix.android.internal.session.sync.RoomSyncHandler
|
||||
import im.vector.matrix.android.internal.session.sync.model.RoomSyncSummary
|
||||
import im.vector.matrix.android.internal.session.sync.model.RoomSyncUnreadNotifications
|
||||
import io.realm.Realm
|
||||
|
@ -65,7 +66,8 @@ internal class RoomSummaryUpdater @Inject constructor(
|
|||
membership: Membership? = null,
|
||||
roomSummary: RoomSyncSummary? = null,
|
||||
unreadNotifications: RoomSyncUnreadNotifications? = null,
|
||||
updateMembers: Boolean = false) {
|
||||
updateMembers: Boolean = false,
|
||||
ephemeralResult: RoomSyncHandler.EphemeralResult? = null) {
|
||||
val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
|
||||
if (roomSummary != null) {
|
||||
if (roomSummary.heroes.isNotEmpty()) {
|
||||
|
@ -93,8 +95,8 @@ internal class RoomSummaryUpdater @Inject constructor(
|
|||
val encryptionEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ENCRYPTION).prev()
|
||||
|
||||
roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0
|
||||
// avoid this call if we are sure there are unread events
|
||||
|| !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId)
|
||||
// avoid this call if we are sure there are unread events
|
||||
|| !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId)
|
||||
|
||||
roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString()
|
||||
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId)
|
||||
|
@ -104,11 +106,13 @@ internal class RoomSummaryUpdater @Inject constructor(
|
|||
?.canonicalAlias
|
||||
|
||||
val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel<RoomAliasesContent>()?.aliases
|
||||
?: emptyList()
|
||||
?: emptyList()
|
||||
roomSummaryEntity.aliases.clear()
|
||||
roomSummaryEntity.aliases.addAll(roomAliases)
|
||||
roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|")
|
||||
roomSummaryEntity.isEncrypted = encryptionEvent != null
|
||||
roomSummaryEntity.typingUserIds.clear()
|
||||
roomSummaryEntity.typingUserIds.addAll(ephemeralResult?.typingUserIds.orEmpty())
|
||||
|
||||
if (updateMembers) {
|
||||
val otherRoomMembers = RoomMembers(realm, roomId)
|
||||
|
|
|
@ -29,6 +29,7 @@ import im.vector.matrix.android.api.util.Cancelable
|
|||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.RoomMemberEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomMemberEntityFields
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.query.process
|
||||
import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask
|
||||
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
|
||||
|
@ -39,13 +40,16 @@ import im.vector.matrix.android.internal.util.fetchCopied
|
|||
import io.realm.Realm
|
||||
import io.realm.RealmQuery
|
||||
|
||||
internal class DefaultMembershipService @AssistedInject constructor(@Assisted private val roomId: String,
|
||||
private val monarchy: Monarchy,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||
private val inviteTask: InviteTask,
|
||||
private val joinTask: JoinRoomTask,
|
||||
private val leaveRoomTask: LeaveRoomTask
|
||||
internal class DefaultMembershipService @AssistedInject constructor(
|
||||
@Assisted private val roomId: String,
|
||||
private val monarchy: Monarchy,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||
private val inviteTask: InviteTask,
|
||||
private val joinTask: JoinRoomTask,
|
||||
private val leaveRoomTask: LeaveRoomTask,
|
||||
@UserId
|
||||
private val userId: String
|
||||
) : MembershipService {
|
||||
|
||||
@AssistedInject.Factory
|
||||
|
@ -95,6 +99,11 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr
|
|||
return RoomMembers(realm, roomId).queryRoomMembersEvent()
|
||||
.process(RoomMemberEntityFields.MEMBERSHIP_STR, queryParams.memberships)
|
||||
.process(RoomMemberEntityFields.DISPLAY_NAME, queryParams.displayName)
|
||||
.apply {
|
||||
if (queryParams.excludeSelf) {
|
||||
notEqualTo(RoomMemberEntityFields.USER_ID, userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getNumberOfJoinedMembers(): Int {
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* 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.matrix.android.internal.session.room.typing
|
||||
|
||||
import android.os.SystemClock
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.room.typing.TypingService
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Rules:
|
||||
* - user is typing: notify the homeserver (true), at least once every 10s
|
||||
* - user stop typing: after 10s delay: notify the homeserver (false)
|
||||
* - user empty the text composer or quit the timeline screen: notify the homeserver (false)
|
||||
*/
|
||||
internal class DefaultTypingService @AssistedInject constructor(
|
||||
@Assisted private val roomId: String,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val sendTypingTask: SendTypingTask
|
||||
) : TypingService {
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(roomId: String): TypingService
|
||||
}
|
||||
|
||||
private var currentTask: Cancelable? = null
|
||||
private var currentAutoStopTask: Cancelable? = null
|
||||
|
||||
// What the homeserver knows
|
||||
private var userIsTyping = false
|
||||
// Last time the user is typing event has been sent
|
||||
private var lastRequestTimestamp: Long = 0
|
||||
|
||||
override fun userIsTyping() {
|
||||
scheduleAutoStop()
|
||||
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
|
||||
if (userIsTyping && now < lastRequestTimestamp + MIN_DELAY_BETWEEN_TWO_USER_IS_TYPING_REQUESTS_MILLIS) {
|
||||
Timber.d("Typing: Skip start request")
|
||||
return
|
||||
}
|
||||
|
||||
Timber.d("Typing: Send start request")
|
||||
userIsTyping = true
|
||||
lastRequestTimestamp = now
|
||||
|
||||
currentTask?.cancel()
|
||||
|
||||
val params = SendTypingTask.Params(roomId, true)
|
||||
currentTask = sendTypingTask
|
||||
.configureWith(params)
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun userStopsTyping() {
|
||||
if (!userIsTyping) {
|
||||
Timber.d("Typing: Skip stop request")
|
||||
return
|
||||
}
|
||||
|
||||
Timber.d("Typing: Send stop request")
|
||||
userIsTyping = false
|
||||
lastRequestTimestamp = 0
|
||||
|
||||
currentAutoStopTask?.cancel()
|
||||
currentTask?.cancel()
|
||||
|
||||
val params = SendTypingTask.Params(roomId, false)
|
||||
currentTask = sendTypingTask
|
||||
.configureWith(params)
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
private fun scheduleAutoStop() {
|
||||
Timber.d("Typing: Schedule auto stop")
|
||||
currentAutoStopTask?.cancel()
|
||||
|
||||
val params = SendTypingTask.Params(
|
||||
roomId,
|
||||
false,
|
||||
delay = MIN_DELAY_TO_SEND_STOP_TYPING_REQUEST_WHEN_NO_USER_ACTIVITY_MILLIS)
|
||||
currentAutoStopTask = sendTypingTask
|
||||
.configureWith(params) {
|
||||
callback = object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
userIsTyping = false
|
||||
}
|
||||
}
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MIN_DELAY_BETWEEN_TWO_USER_IS_TYPING_REQUESTS_MILLIS = 10_000L
|
||||
private const val MIN_DELAY_TO_SEND_STOP_TYPING_REQUEST_WHEN_NO_USER_ACTIVITY_MILLIS = 10_000L
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright 2020 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.matrix.android.internal.session.room.typing
|
||||
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.session.room.RoomAPI
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import kotlinx.coroutines.delay
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface SendTypingTask : Task<SendTypingTask.Params, Unit> {
|
||||
|
||||
data class Params(
|
||||
val roomId: String,
|
||||
val isTyping: Boolean,
|
||||
val typingTimeoutMillis: Int? = 30_000,
|
||||
// Optional delay before sending the request to the homeserver
|
||||
val delay: Long? = null
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultSendTypingTask @Inject constructor(
|
||||
private val roomAPI: RoomAPI,
|
||||
@UserId private val userId: String,
|
||||
private val eventBus: EventBus
|
||||
) : SendTypingTask {
|
||||
|
||||
override suspend fun execute(params: SendTypingTask.Params) {
|
||||
delay(params.delay ?: -1)
|
||||
|
||||
executeRequest<Unit>(eventBus) {
|
||||
apiCall = roomAPI.sendTypingState(
|
||||
params.roomId,
|
||||
userId,
|
||||
TypingBody(params.isTyping, params.typingTimeoutMillis?.takeIf { params.isTyping })
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright 2020 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.matrix.android.internal.session.room.typing
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TypingBody(
|
||||
// Required. Whether the user is typing or not. If false, the timeout key can be omitted.
|
||||
@Json(name = "typing")
|
||||
val typing: Boolean,
|
||||
// The length of time in milliseconds to mark this user as typing.
|
||||
@Json(name = "timeout")
|
||||
val timeout: Int?
|
||||
)
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright 2020 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.matrix.android.internal.session.room.typing
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TypingEventContent(
|
||||
@Json(name = "user_ids")
|
||||
val typingUserIds: List<String> = emptyList()
|
||||
)
|
|
@ -37,6 +37,7 @@ import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
|||
import im.vector.matrix.android.internal.session.room.membership.RoomMemberEventHandler
|
||||
import im.vector.matrix.android.internal.session.room.read.FullyReadContent
|
||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
||||
import im.vector.matrix.android.internal.session.room.typing.TypingEventContent
|
||||
import im.vector.matrix.android.internal.session.sync.model.*
|
||||
import io.realm.Realm
|
||||
import io.realm.kotlin.createObject
|
||||
|
@ -99,11 +100,12 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
syncLocalTimestampMillis: Long): RoomEntity {
|
||||
Timber.v("Handle join sync for room $roomId")
|
||||
|
||||
if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) {
|
||||
handleEphemeral(realm, roomId, roomSync.ephemeral, isInitialSync)
|
||||
var ephemeralResult: EphemeralResult? = null
|
||||
if (roomSync.ephemeral?.events?.isNotEmpty() == true) {
|
||||
ephemeralResult = handleEphemeral(realm, roomId, roomSync.ephemeral, isInitialSync)
|
||||
}
|
||||
|
||||
if (roomSync.accountData != null && roomSync.accountData.events.isNullOrEmpty().not()) {
|
||||
if (roomSync.accountData?.events?.isNotEmpty() == true) {
|
||||
handleRoomAccountDataEvents(realm, roomId, roomSync.accountData)
|
||||
}
|
||||
|
||||
|
@ -116,7 +118,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
|
||||
// State event
|
||||
|
||||
if (roomSync.state != null && roomSync.state.events.isNotEmpty()) {
|
||||
if (roomSync.state?.events?.isNotEmpty() == true) {
|
||||
val minStateIndex = roomEntity.untimelinedStateEvents.where().min(EventEntityFields.STATE_INDEX)?.toInt()
|
||||
?: Int.MIN_VALUE
|
||||
val untimelinedStateIndex = minStateIndex + 1
|
||||
|
@ -127,7 +129,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
roomMemberEventHandler.handle(realm, roomId, event)
|
||||
}
|
||||
}
|
||||
if (roomSync.timeline != null && roomSync.timeline.events.isNotEmpty()) {
|
||||
if (roomSync.timeline?.events?.isNotEmpty() == true) {
|
||||
val chunkEntity = handleTimelineEvents(
|
||||
realm,
|
||||
roomEntity,
|
||||
|
@ -144,7 +146,14 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
it.type == EventType.STATE_ROOM_MEMBER
|
||||
} != null
|
||||
|
||||
roomSummaryUpdater.update(realm, roomId, Membership.JOIN, roomSync.summary, roomSync.unreadNotifications, updateMembers = hasRoomMember)
|
||||
roomSummaryUpdater.update(
|
||||
realm,
|
||||
roomId,
|
||||
Membership.JOIN,
|
||||
roomSync.summary,
|
||||
roomSync.unreadNotifications,
|
||||
updateMembers = hasRoomMember,
|
||||
ephemeralResult = ephemeralResult)
|
||||
return roomEntity
|
||||
}
|
||||
|
||||
|
@ -221,16 +230,33 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
return chunkEntity
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
data class EphemeralResult(
|
||||
val typingUserIds: List<String> = emptyList()
|
||||
)
|
||||
|
||||
private fun handleEphemeral(realm: Realm,
|
||||
roomId: String,
|
||||
ephemeral: RoomSyncEphemeral,
|
||||
isInitialSync: Boolean) {
|
||||
isInitialSync: Boolean): EphemeralResult {
|
||||
var result = EphemeralResult()
|
||||
for (event in ephemeral.events) {
|
||||
if (event.type != EventType.RECEIPT) continue
|
||||
val readReceiptContent = event.content as? ReadReceiptContent ?: continue
|
||||
readReceiptHandler.handle(realm, roomId, readReceiptContent, isInitialSync)
|
||||
when (event.type) {
|
||||
EventType.RECEIPT -> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(event.content as? ReadReceiptContent)?.let { readReceiptContent ->
|
||||
readReceiptHandler.handle(realm, roomId, readReceiptContent, isInitialSync)
|
||||
}
|
||||
}
|
||||
EventType.TYPING -> {
|
||||
event.content.toModel<TypingEventContent>()?.let { typingEventContent ->
|
||||
result = result.copy(typingUserIds = typingEventContent.typingUserIds)
|
||||
}
|
||||
}
|
||||
else -> Timber.w("Ephemeral event type '${event.type}' not yet supported")
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) {
|
||||
|
|
|
@ -272,4 +272,7 @@
|
|||
<string name="notice_room_canonical_alias_set">"%1$s set the main address for this room to %2$s."</string>
|
||||
<string name="notice_room_canonical_alias_unset">"%1$s removed the main address for this room."</string>
|
||||
|
||||
<string name="notice_room_guest_access_can_join">"%1$s has allowed guests to join the room."</string>
|
||||
<string name="notice_room_guest_access_forbidden">"%1$s has prevented guests from joining the room."</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -24,12 +24,16 @@ static def getGitTimestamp() {
|
|||
}
|
||||
|
||||
static def generateVersionCodeFromTimestamp() {
|
||||
// It's unix timestamp divided by 10: It's incremented by one every 10 seconds.
|
||||
return (getGitTimestamp() / 10).toInteger()
|
||||
// It's unix timestamp, minus timestamp of October 3rd 2018 (first commit date) divided by 100: It's incremented by one every 100 seconds.
|
||||
// plus 20_000_000 for compatibility reason with the previous way the Version Code was computed
|
||||
// Note that the result will be multiplied by 10 when adding the digit for the arch
|
||||
return ((getGitTimestamp() - 1_538_524_800 ) / 100).toInteger() + 20_000_000
|
||||
}
|
||||
|
||||
def generateVersionCodeFromVersionName() {
|
||||
return versionMajor * 1_00_00 + versionMinor * 1_00 + versionPatch
|
||||
// plus 4_000_000 for compatibility reason with the previous way the Version Code was computed
|
||||
// Note that the result will be multiplied by 10 when adding the digit for the arch
|
||||
return (versionMajor * 1_00_00 + versionMinor * 1_00 + versionPatch) + 4_000_000
|
||||
}
|
||||
|
||||
def getVersionCode() {
|
||||
|
@ -77,8 +81,8 @@ project.android.buildTypes.all { buildType ->
|
|||
]
|
||||
}
|
||||
|
||||
// map for the version codes
|
||||
// x86 must have greater values than arm, see https://software.intel.com/en-us/android/articles/google-play-supports-cpu-architecture-filtering-for-multiple-apk
|
||||
// map for the version codes last digit
|
||||
// x86 must have greater values than arm
|
||||
// 64 bits have greater value than 32 bits
|
||||
ext.abiVersionCodes = ["armeabi-v7a": 1, "arm64-v8a": 2, "x86": 3, "x86_64": 4].withDefault { 0 }
|
||||
|
||||
|
@ -144,7 +148,7 @@ android {
|
|||
variant.outputs.each { output ->
|
||||
def baseAbiVersionCode = project.ext.abiVersionCodes.get(output.getFilter(OutputFile.ABI))
|
||||
// Known limitation: it does not modify the value in the BuildConfig.java generated file
|
||||
output.versionCodeOverride = baseAbiVersionCode * 10_000_000 + variant.versionCode
|
||||
output.versionCodeOverride = variant.versionCode * 10 + baseAbiVersionCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,14 +16,13 @@
|
|||
|
||||
package im.vector.riotx.core.error
|
||||
|
||||
import im.vector.riotx.BuildConfig
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* throw in debug, only log in production. As this method does not always throw, next statement should be a return
|
||||
*/
|
||||
fun fatalError(message: String) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
fun fatalError(message: String, failFast: Boolean) {
|
||||
if (failFast) {
|
||||
error(message)
|
||||
} else {
|
||||
Timber.e(message)
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
package im.vector.riotx.core.rx
|
||||
|
||||
import im.vector.riotx.BuildConfig
|
||||
import im.vector.riotx.features.settings.VectorPreferences
|
||||
import io.reactivex.plugins.RxJavaPlugins
|
||||
import timber.log.Timber
|
||||
|
@ -33,8 +32,8 @@ class RxConfig @Inject constructor(
|
|||
RxJavaPlugins.setErrorHandler { throwable ->
|
||||
Timber.e(throwable, "RxError")
|
||||
|
||||
// Avoid crash in production
|
||||
if (BuildConfig.DEBUG || vectorPreferences.failFast()) {
|
||||
// Avoid crash in production, except if user wants it
|
||||
if (vectorPreferences.failFast()) {
|
||||
throw throwable
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,6 +63,7 @@ class AutocompleteMemberPresenter @AssistedInject constructor(context: Context,
|
|||
QueryStringValue.Contains(query.toString(), QueryStringValue.Case.INSENSITIVE)
|
||||
}
|
||||
memberships = listOf(Membership.JOIN)
|
||||
excludeSelf = true
|
||||
}
|
||||
val members = room.getRoomMembers(queryParams)
|
||||
.asSequence()
|
||||
|
|
|
@ -22,9 +22,11 @@ import im.vector.matrix.android.api.util.toMatrixItem
|
|||
import im.vector.riotx.core.epoxy.zeroItem
|
||||
import im.vector.riotx.core.utils.DebouncedClickListener
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import im.vector.riotx.features.home.room.typing.TypingHelper
|
||||
import javax.inject.Inject
|
||||
|
||||
class BreadcrumbsController @Inject constructor(
|
||||
private val typingHelper: TypingHelper,
|
||||
private val avatarRenderer: AvatarRenderer
|
||||
) : EpoxyController() {
|
||||
|
||||
|
@ -62,6 +64,7 @@ class BreadcrumbsController @Inject constructor(
|
|||
unreadNotificationCount(it.notificationCount)
|
||||
showHighlighted(it.highlightCount > 0)
|
||||
hasUnreadMessage(it.hasUnreadMessages)
|
||||
hasTypingUsers(typingHelper.excludeCurrentUser(it.typingRoomMemberIds).isNotEmpty())
|
||||
hasDraft(it.userDrafts.isNotEmpty())
|
||||
itemClickListener(
|
||||
DebouncedClickListener(View.OnClickListener { _ ->
|
||||
|
|
|
@ -37,6 +37,7 @@ abstract class BreadcrumbsItem : VectorEpoxyModel<BreadcrumbsItem.Holder>() {
|
|||
@EpoxyAttribute var unreadNotificationCount: Int = 0
|
||||
@EpoxyAttribute var showHighlighted: Boolean = false
|
||||
@EpoxyAttribute var hasUnreadMessage: Boolean = false
|
||||
@EpoxyAttribute var hasTypingUsers: Boolean = false
|
||||
@EpoxyAttribute var hasDraft: Boolean = false
|
||||
@EpoxyAttribute var itemClickListener: View.OnClickListener? = null
|
||||
|
||||
|
@ -44,6 +45,7 @@ abstract class BreadcrumbsItem : VectorEpoxyModel<BreadcrumbsItem.Holder>() {
|
|||
super.bind(holder)
|
||||
holder.rootView.setOnClickListener(itemClickListener)
|
||||
holder.unreadIndentIndicator.isVisible = hasUnreadMessage
|
||||
holder.typingIndicator.isVisible = hasTypingUsers
|
||||
avatarRenderer.render(matrixItem, holder.avatarImageView)
|
||||
holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted))
|
||||
holder.draftIndentIndicator.isVisible = hasDraft
|
||||
|
@ -53,6 +55,7 @@ abstract class BreadcrumbsItem : VectorEpoxyModel<BreadcrumbsItem.Holder>() {
|
|||
val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.breadcrumbsUnreadCounterBadgeView)
|
||||
val unreadIndentIndicator by bind<View>(R.id.breadcrumbsUnreadIndicator)
|
||||
val draftIndentIndicator by bind<View>(R.id.breadcrumbsDraftBadge)
|
||||
val typingIndicator by bind<View>(R.id.breadcrumbsTypingView)
|
||||
val avatarImageView by bind<ImageView>(R.id.breadcrumbsImageView)
|
||||
val rootView by bind<ViewGroup>(R.id.breadcrumbsRoot)
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
|||
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||
|
||||
sealed class RoomDetailAction : VectorViewModelAction {
|
||||
data class UserIsTyping(val isTyping: Boolean) : RoomDetailAction()
|
||||
data class SaveDraft(val draft: String) : RoomDetailAction()
|
||||
data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : RoomDetailAction()
|
||||
data class SendMedia(val attachments: List<ContentAttachmentData>) : RoomDetailAction()
|
||||
|
|
|
@ -20,6 +20,7 @@ import android.annotation.SuppressLint
|
|||
import android.app.Activity.RESULT_OK
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.graphics.Typeface
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
|
@ -50,6 +51,7 @@ import com.github.piasy.biv.BigImageViewer
|
|||
import com.github.piasy.biv.loader.ImageLoader
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.jakewharton.rxbinding3.widget.textChanges
|
||||
import im.vector.matrix.android.api.permalinks.PermalinkFactory
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||
|
@ -101,6 +103,7 @@ import im.vector.riotx.features.permalink.PermalinkHandler
|
|||
import im.vector.riotx.features.reactions.EmojiReactionPickerActivity
|
||||
import im.vector.riotx.features.settings.VectorPreferences
|
||||
import im.vector.riotx.features.share.SharedData
|
||||
import im.vector.riotx.features.themes.ThemeUtils
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
@ -110,6 +113,7 @@ import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
|
|||
import org.commonmark.parser.Parser
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
@Parcelize
|
||||
|
@ -247,9 +251,9 @@ class RoomDetailFragment @Inject constructor(
|
|||
roomDetailViewModel.selectSubscribe(RoomDetailViewState::sendMode) { mode ->
|
||||
when (mode) {
|
||||
is SendMode.REGULAR -> renderRegularMode(mode.text)
|
||||
is SendMode.EDIT -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text)
|
||||
is SendMode.QUOTE -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.quote, mode.text)
|
||||
is SendMode.REPLY -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text)
|
||||
is SendMode.EDIT -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text)
|
||||
is SendMode.QUOTE -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.quote, mode.text)
|
||||
is SendMode.REPLY -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -276,9 +280,9 @@ class RoomDetailFragment @Inject constructor(
|
|||
super.onActivityCreated(savedInstanceState)
|
||||
if (savedInstanceState == null) {
|
||||
when (val sharedData = roomDetailArgs.sharedData) {
|
||||
is SharedData.Text -> roomDetailViewModel.handle(RoomDetailAction.SendMessage(sharedData.text, false))
|
||||
is SharedData.Text -> roomDetailViewModel.handle(RoomDetailAction.SendMessage(sharedData.text, false))
|
||||
is SharedData.Attachments -> roomDetailViewModel.handle(RoomDetailAction.SendMedia(sharedData.attachmentData))
|
||||
null -> Timber.v("No share data to process")
|
||||
null -> Timber.v("No share data to process")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -502,7 +506,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
is MessageTextItem -> {
|
||||
return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED
|
||||
}
|
||||
else -> false
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -517,9 +521,9 @@ class RoomDetailFragment @Inject constructor(
|
|||
withState(roomDetailViewModel) {
|
||||
val showJumpToUnreadBanner = when (it.unreadState) {
|
||||
UnreadState.Unknown,
|
||||
UnreadState.HasNoUnread -> false
|
||||
UnreadState.HasNoUnread -> false
|
||||
is UnreadState.ReadMarkerNotLoaded -> true
|
||||
is UnreadState.HasUnread -> {
|
||||
is UnreadState.HasUnread -> {
|
||||
if (it.canShowJumpToReadMarker) {
|
||||
val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
|
||||
val positionOfReadMarker = timelineEventController.getPositionOfReadMarker()
|
||||
|
@ -540,6 +544,9 @@ class RoomDetailFragment @Inject constructor(
|
|||
|
||||
private fun setupComposer() {
|
||||
autoCompleter.setup(composerLayout.composerEditText)
|
||||
|
||||
observerUserTyping()
|
||||
|
||||
composerLayout.callback = object : TextComposerView.Callback {
|
||||
override fun onAddAttachment() {
|
||||
if (!::attachmentTypeSelector.isInitialized) {
|
||||
|
@ -576,6 +583,18 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun observerUserTyping() {
|
||||
composerLayout.composerEditText.textChanges()
|
||||
.skipInitialValue()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.map { it.isNotEmpty() }
|
||||
.subscribe {
|
||||
Timber.d("Typing: User is typing: $it")
|
||||
roomDetailViewModel.handle(RoomDetailAction.UserIsTyping(it))
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
}
|
||||
|
||||
private fun sendUri(uri: Uri): Boolean {
|
||||
val shareIntent = Intent(Intent.ACTION_SEND, uri)
|
||||
val isHandled = attachmentsHelper.handleShareIntent(shareIntent)
|
||||
|
@ -628,13 +647,29 @@ class RoomDetailFragment @Inject constructor(
|
|||
} else {
|
||||
roomToolbarTitleView.text = it.displayName
|
||||
avatarRenderer.render(it.toMatrixItem(), roomToolbarAvatarImageView)
|
||||
roomToolbarSubtitleView.setTextOrHide(it.topic)
|
||||
|
||||
renderSubTitle(state.typingMessage, it.topic)
|
||||
}
|
||||
jumpToBottomView.count = it.notificationCount
|
||||
jumpToBottomView.drawBadge = it.hasUnreadMessages
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderSubTitle(typingMessage: String?, topic: String) {
|
||||
// TODO Temporary place to put typing data
|
||||
roomToolbarSubtitleView.let {
|
||||
it.setTextOrHide(typingMessage ?: topic)
|
||||
|
||||
if (typingMessage == null) {
|
||||
it.setTextColor(ThemeUtils.getColor(requireContext(), R.attr.vctr_toolbar_secondary_text_color))
|
||||
it.setTypeface(null, Typeface.NORMAL)
|
||||
} else {
|
||||
it.setTextColor(ContextCompat.getColor(requireContext(), R.color.riotx_accent))
|
||||
it.setTypeface(null, Typeface.BOLD)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderTombstoneEventHandling(async: Async<String>) {
|
||||
when (async) {
|
||||
is Loading -> {
|
||||
|
@ -647,7 +682,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
navigator.openRoom(vectorBaseActivity, async())
|
||||
vectorBaseActivity.finish()
|
||||
}
|
||||
is Fail -> {
|
||||
is Fail -> {
|
||||
vectorBaseActivity.hideWaitingView()
|
||||
vectorBaseActivity.toast(errorFormatter.toHumanReadable(async.error))
|
||||
}
|
||||
|
@ -656,23 +691,23 @@ class RoomDetailFragment @Inject constructor(
|
|||
|
||||
private fun renderSendMessageResult(sendMessageResult: SendMessageResult) {
|
||||
when (sendMessageResult) {
|
||||
is SendMessageResult.MessageSent -> {
|
||||
is SendMessageResult.MessageSent -> {
|
||||
updateComposerText("")
|
||||
}
|
||||
is SendMessageResult.SlashCommandHandled -> {
|
||||
is SendMessageResult.SlashCommandHandled -> {
|
||||
sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) }
|
||||
updateComposerText("")
|
||||
}
|
||||
is SendMessageResult.SlashCommandError -> {
|
||||
is SendMessageResult.SlashCommandError -> {
|
||||
displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command))
|
||||
}
|
||||
is SendMessageResult.SlashCommandUnknown -> {
|
||||
is SendMessageResult.SlashCommandUnknown -> {
|
||||
displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command))
|
||||
}
|
||||
is SendMessageResult.SlashCommandResultOk -> {
|
||||
is SendMessageResult.SlashCommandResultOk -> {
|
||||
updateComposerText("")
|
||||
}
|
||||
is SendMessageResult.SlashCommandResultError -> {
|
||||
is SendMessageResult.SlashCommandResultError -> {
|
||||
displayCommandError(sendMessageResult.throwable.localizedMessage)
|
||||
}
|
||||
is SendMessageResult.SlashCommandNotImplemented -> {
|
||||
|
@ -710,7 +745,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
|
||||
private fun displayRoomDetailActionResult(result: Async<RoomDetailAction>) {
|
||||
when (result) {
|
||||
is Fail -> {
|
||||
is Fail -> {
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.dialog_title_error)
|
||||
.setMessage(errorFormatter.toHumanReadable(result.error))
|
||||
|
@ -721,7 +756,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
when (val data = result.invoke()) {
|
||||
is RoomDetailAction.ReportContent -> {
|
||||
when {
|
||||
data.spam -> {
|
||||
data.spam -> {
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.content_reported_as_spam_title)
|
||||
.setMessage(R.string.content_reported_as_spam_content)
|
||||
|
@ -743,7 +778,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
.show()
|
||||
.withColoredButton(DialogInterface.BUTTON_NEGATIVE)
|
||||
}
|
||||
else -> {
|
||||
else -> {
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.content_reported_title)
|
||||
.setMessage(R.string.content_reported_content)
|
||||
|
@ -871,14 +906,14 @@ class RoomDetailFragment @Inject constructor(
|
|||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
if (allGranted(grantResults)) {
|
||||
when (requestCode) {
|
||||
PERMISSION_REQUEST_CODE_DOWNLOAD_FILE -> {
|
||||
PERMISSION_REQUEST_CODE_DOWNLOAD_FILE -> {
|
||||
val action = roomDetailViewModel.pendingAction
|
||||
if (action != null) {
|
||||
roomDetailViewModel.pendingAction = null
|
||||
roomDetailViewModel.handle(action)
|
||||
}
|
||||
}
|
||||
PERMISSION_REQUEST_CODE_INCOMING_URI -> {
|
||||
PERMISSION_REQUEST_CODE_INCOMING_URI -> {
|
||||
val pendingUri = roomDetailViewModel.pendingUri
|
||||
if (pendingUri != null) {
|
||||
roomDetailViewModel.pendingUri = null
|
||||
|
@ -981,23 +1016,23 @@ class RoomDetailFragment @Inject constructor(
|
|||
|
||||
private fun handleActions(action: EventSharedAction) {
|
||||
when (action) {
|
||||
is EventSharedAction.AddReaction -> {
|
||||
is EventSharedAction.AddReaction -> {
|
||||
startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE)
|
||||
}
|
||||
is EventSharedAction.ViewReactions -> {
|
||||
is EventSharedAction.ViewReactions -> {
|
||||
ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData)
|
||||
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
|
||||
}
|
||||
is EventSharedAction.Copy -> {
|
||||
is EventSharedAction.Copy -> {
|
||||
// I need info about the current selected message :/
|
||||
copyToClipboard(requireContext(), action.content, false)
|
||||
val msg = requireContext().getString(R.string.copied_to_clipboard)
|
||||
showSnackWithMessage(msg, Snackbar.LENGTH_SHORT)
|
||||
}
|
||||
is EventSharedAction.Delete -> {
|
||||
is EventSharedAction.Delete -> {
|
||||
roomDetailViewModel.handle(RoomDetailAction.RedactAction(action.eventId, context?.getString(R.string.event_redacted_by_user_reason)))
|
||||
}
|
||||
is EventSharedAction.Share -> {
|
||||
is EventSharedAction.Share -> {
|
||||
// TODO current data communication is too limited
|
||||
// Need to now the media type
|
||||
// TODO bad, just POC
|
||||
|
@ -1025,10 +1060,10 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
)
|
||||
}
|
||||
is EventSharedAction.ViewEditHistory -> {
|
||||
is EventSharedAction.ViewEditHistory -> {
|
||||
onEditedDecorationClicked(action.messageInformationData)
|
||||
}
|
||||
is EventSharedAction.ViewSource -> {
|
||||
is EventSharedAction.ViewSource -> {
|
||||
val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null)
|
||||
view.findViewById<TextView>(R.id.event_content_text_view)?.let {
|
||||
it.text = action.content
|
||||
|
@ -1039,7 +1074,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
is EventSharedAction.ViewDecryptedSource -> {
|
||||
is EventSharedAction.ViewDecryptedSource -> {
|
||||
val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null)
|
||||
view.findViewById<TextView>(R.id.event_content_text_view)?.let {
|
||||
it.text = action.content
|
||||
|
@ -1050,31 +1085,31 @@ class RoomDetailFragment @Inject constructor(
|
|||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
is EventSharedAction.QuickReact -> {
|
||||
is EventSharedAction.QuickReact -> {
|
||||
// eventId,ClickedOn,Add
|
||||
roomDetailViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add))
|
||||
}
|
||||
is EventSharedAction.Edit -> {
|
||||
is EventSharedAction.Edit -> {
|
||||
roomDetailViewModel.handle(RoomDetailAction.EnterEditMode(action.eventId, composerLayout.text.toString()))
|
||||
}
|
||||
is EventSharedAction.Quote -> {
|
||||
is EventSharedAction.Quote -> {
|
||||
roomDetailViewModel.handle(RoomDetailAction.EnterQuoteMode(action.eventId, composerLayout.text.toString()))
|
||||
}
|
||||
is EventSharedAction.Reply -> {
|
||||
is EventSharedAction.Reply -> {
|
||||
roomDetailViewModel.handle(RoomDetailAction.EnterReplyMode(action.eventId, composerLayout.text.toString()))
|
||||
}
|
||||
is EventSharedAction.CopyPermalink -> {
|
||||
is EventSharedAction.CopyPermalink -> {
|
||||
val permalink = PermalinkFactory.createPermalink(roomDetailArgs.roomId, action.eventId)
|
||||
copyToClipboard(requireContext(), permalink, false)
|
||||
showSnackWithMessage(requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT)
|
||||
}
|
||||
is EventSharedAction.Resend -> {
|
||||
is EventSharedAction.Resend -> {
|
||||
roomDetailViewModel.handle(RoomDetailAction.ResendMessage(action.eventId))
|
||||
}
|
||||
is EventSharedAction.Remove -> {
|
||||
is EventSharedAction.Remove -> {
|
||||
roomDetailViewModel.handle(RoomDetailAction.RemoveFailedEcho(action.eventId))
|
||||
}
|
||||
is EventSharedAction.ReportContentSpam -> {
|
||||
is EventSharedAction.ReportContentSpam -> {
|
||||
roomDetailViewModel.handle(RoomDetailAction.ReportContent(
|
||||
action.eventId, action.senderId, "This message is spam", spam = true))
|
||||
}
|
||||
|
@ -1082,19 +1117,19 @@ class RoomDetailFragment @Inject constructor(
|
|||
roomDetailViewModel.handle(RoomDetailAction.ReportContent(
|
||||
action.eventId, action.senderId, "This message is inappropriate", inappropriate = true))
|
||||
}
|
||||
is EventSharedAction.ReportContentCustom -> {
|
||||
is EventSharedAction.ReportContentCustom -> {
|
||||
promptReasonToReportContent(action)
|
||||
}
|
||||
is EventSharedAction.IgnoreUser -> {
|
||||
is EventSharedAction.IgnoreUser -> {
|
||||
roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(action.senderId))
|
||||
}
|
||||
is EventSharedAction.OnUrlClicked -> {
|
||||
is EventSharedAction.OnUrlClicked -> {
|
||||
onUrlClicked(action.url)
|
||||
}
|
||||
is EventSharedAction.OnUrlLongClicked -> {
|
||||
is EventSharedAction.OnUrlLongClicked -> {
|
||||
onUrlLongClicked(action.url)
|
||||
}
|
||||
else -> {
|
||||
else -> {
|
||||
Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
@ -1202,10 +1237,10 @@ class RoomDetailFragment @Inject constructor(
|
|||
|
||||
private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) {
|
||||
when (type) {
|
||||
AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera()
|
||||
AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile()
|
||||
AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera()
|
||||
AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile()
|
||||
AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery()
|
||||
AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio()
|
||||
AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio()
|
||||
AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact()
|
||||
AttachmentTypeSelectorView.Type.STICKER -> vectorBaseActivity.notImplemented("Adding stickers")
|
||||
}
|
||||
|
|
|
@ -20,12 +20,7 @@ import android.net.Uri
|
|||
import androidx.annotation.IdRes
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.FragmentViewModelContext
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.airbnb.mvrx.*
|
||||
import com.jakewharton.rxrelay2.BehaviorRelay
|
||||
import com.jakewharton.rxrelay2.PublishRelay
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
|
@ -67,6 +62,7 @@ import im.vector.riotx.core.utils.subscribeLogError
|
|||
import im.vector.riotx.features.command.CommandParser
|
||||
import im.vector.riotx.features.command.ParsedCommand
|
||||
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
|
||||
import im.vector.riotx.features.home.room.typing.TypingHelper
|
||||
import im.vector.riotx.features.settings.VectorPreferences
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.functions.BiFunction
|
||||
|
@ -83,6 +79,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
userPreferencesProvider: UserPreferencesProvider,
|
||||
private val vectorPreferences: VectorPreferences,
|
||||
private val stringProvider: StringProvider,
|
||||
private val typingHelper: TypingHelper,
|
||||
private val session: Session
|
||||
) : VectorViewModel<RoomDetailViewState, RoomDetailAction>(initialState), Timeline.Listener {
|
||||
|
||||
|
@ -92,16 +89,16 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
private val visibleEventsObservable = BehaviorRelay.create<RoomDetailAction.TimelineEventTurnsVisible>()
|
||||
private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) {
|
||||
TimelineSettings(30,
|
||||
filterEdits = false,
|
||||
filterTypes = true,
|
||||
allowedTypes = TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES,
|
||||
buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
|
||||
filterEdits = false,
|
||||
filterTypes = true,
|
||||
allowedTypes = TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES,
|
||||
buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
|
||||
} else {
|
||||
TimelineSettings(30,
|
||||
filterEdits = true,
|
||||
filterTypes = true,
|
||||
allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES,
|
||||
buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
|
||||
filterEdits = true,
|
||||
filterTypes = true,
|
||||
allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES,
|
||||
buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
|
||||
}
|
||||
|
||||
private var timelineEvents = PublishRelay.create<List<TimelineEvent>>()
|
||||
|
@ -159,6 +156,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
|
||||
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)
|
||||
|
@ -239,32 +237,41 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
copy(
|
||||
// Create a sendMode from a draft and retrieve the TimelineEvent
|
||||
sendMode = when (draft) {
|
||||
is UserDraft.REGULAR -> SendMode.REGULAR(draft.text)
|
||||
is UserDraft.QUOTE -> {
|
||||
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
|
||||
SendMode.QUOTE(timelineEvent, draft.text)
|
||||
}
|
||||
}
|
||||
is UserDraft.REPLY -> {
|
||||
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
|
||||
SendMode.REPLY(timelineEvent, draft.text)
|
||||
}
|
||||
}
|
||||
is UserDraft.EDIT -> {
|
||||
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
|
||||
SendMode.EDIT(timelineEvent, draft.text)
|
||||
}
|
||||
}
|
||||
} ?: SendMode.REGULAR("")
|
||||
is UserDraft.REGULAR -> SendMode.REGULAR(draft.text)
|
||||
is UserDraft.QUOTE -> {
|
||||
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
|
||||
SendMode.QUOTE(timelineEvent, draft.text)
|
||||
}
|
||||
}
|
||||
is UserDraft.REPLY -> {
|
||||
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
|
||||
SendMode.REPLY(timelineEvent, draft.text)
|
||||
}
|
||||
}
|
||||
is UserDraft.EDIT -> {
|
||||
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
|
||||
SendMode.EDIT(timelineEvent, draft.text)
|
||||
}
|
||||
}
|
||||
} ?: SendMode.REGULAR("")
|
||||
)
|
||||
}
|
||||
}
|
||||
.disposeOnClear()
|
||||
}
|
||||
|
||||
private fun handleUserIsTyping(action: RoomDetailAction.UserIsTyping) {
|
||||
if (vectorPreferences.sendTypingNotifs()) {
|
||||
if (action.isTyping) {
|
||||
room.userIsTyping()
|
||||
} else {
|
||||
room.userStopsTyping()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTombstoneEvent(action: RoomDetailAction.HandleTombstoneEvent) {
|
||||
val tombstoneContent = action.event.getClearContent().toModel<RoomTombstoneContent>()
|
||||
?: return
|
||||
val tombstoneContent = action.event.getClearContent().toModel<RoomTombstoneContent>() ?: return
|
||||
|
||||
val roomId = tombstoneContent.replacementRoom ?: ""
|
||||
val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN
|
||||
|
@ -419,7 +426,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
is SendMode.EDIT -> {
|
||||
// is original event a reply?
|
||||
val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel<MessageContent>()?.relatesTo?.inReplyTo?.eventId
|
||||
?: state.sendMode.timelineEvent.root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId
|
||||
?: state.sendMode.timelineEvent.root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId
|
||||
if (inReplyTo != null) {
|
||||
// TODO check if same content?
|
||||
room.getTimeLineEvent(inReplyTo)?.let {
|
||||
|
@ -428,13 +435,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
} else {
|
||||
val messageContent: MessageContent? =
|
||||
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
|
||||
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
|
||||
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
|
||||
val existingBody = messageContent?.body ?: ""
|
||||
if (existingBody != action.text) {
|
||||
room.editTextMessage(state.sendMode.timelineEvent.root.eventId ?: "",
|
||||
messageContent?.type ?: MessageType.MSGTYPE_TEXT,
|
||||
action.text,
|
||||
action.autoMarkdown)
|
||||
messageContent?.type ?: MessageType.MSGTYPE_TEXT,
|
||||
action.text,
|
||||
action.autoMarkdown)
|
||||
} else {
|
||||
Timber.w("Same message content, do not send edition")
|
||||
}
|
||||
|
@ -445,7 +452,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
is SendMode.QUOTE -> {
|
||||
val messageContent: MessageContent? =
|
||||
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
|
||||
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
|
||||
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
|
||||
val textMsg = messageContent?.body
|
||||
|
||||
val finalText = legacyRiotQuoteText(textMsg, action.text.toString())
|
||||
|
@ -561,7 +568,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) {
|
||||
null -> room.sendMedias(attachments)
|
||||
else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name
|
||||
?: tooBigFile.path, tooBigFile.size, maxUploadFileSize)))
|
||||
?: tooBigFile.path, tooBigFile.size, maxUploadFileSize)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -750,8 +757,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
.buffer(1, TimeUnit.SECONDS)
|
||||
.filter { it.isNotEmpty() }
|
||||
.subscribeBy(onNext = { actions ->
|
||||
val bufferedMostRecentDisplayedEvent = actions.maxBy { it.event.displayIndex }?.event
|
||||
?: return@subscribeBy
|
||||
val bufferedMostRecentDisplayedEvent = actions.maxBy { it.event.displayIndex }?.event ?: return@subscribeBy
|
||||
val globalMostRecentDisplayedEvent = mostRecentDisplayedEvent
|
||||
if (trackUnreadMessages.get()) {
|
||||
if (globalMostRecentDisplayedEvent == null) {
|
||||
|
@ -834,7 +840,14 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
room.rx().liveRoomSummary()
|
||||
.unwrap()
|
||||
.execute { async ->
|
||||
copy(asyncRoomSummary = async)
|
||||
val typingRoomMembers =
|
||||
typingHelper.toTypingRoomMembers(async.invoke()?.typingRoomMemberIds.orEmpty(), room)
|
||||
|
||||
copy(
|
||||
asyncRoomSummary = async,
|
||||
typingRoomMembers = typingRoomMembers,
|
||||
typingMessage = typingHelper.toTypingMessage(typingRoomMembers)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -921,6 +934,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
override fun onCleared() {
|
||||
timeline.dispose()
|
||||
timeline.removeAllListeners()
|
||||
if (vectorPreferences.sendTypingNotifs()) {
|
||||
room.userStopsTyping()
|
||||
}
|
||||
super.onCleared()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary
|
|||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.session.sync.SyncState
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import im.vector.matrix.android.api.util.MatrixItem
|
||||
|
||||
/**
|
||||
* Describes the current send mode:
|
||||
|
@ -43,7 +44,7 @@ sealed class SendMode(open val text: String) {
|
|||
sealed class UnreadState {
|
||||
object Unknown : UnreadState()
|
||||
object HasNoUnread : UnreadState()
|
||||
data class ReadMarkerNotLoaded(val readMarkerId: String): UnreadState()
|
||||
data class ReadMarkerNotLoaded(val readMarkerId: String) : UnreadState()
|
||||
data class HasUnread(val firstUnreadEventId: String) : UnreadState()
|
||||
}
|
||||
|
||||
|
@ -52,6 +53,8 @@ data class RoomDetailViewState(
|
|||
val eventId: String?,
|
||||
val asyncInviter: Async<User> = Uninitialized,
|
||||
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
||||
val typingRoomMembers: List<MatrixItem.UserItem>? = null,
|
||||
val typingMessage: String? = null,
|
||||
val sendMode: SendMode = SendMode.REGULAR(""),
|
||||
val tombstoneEvent: Event? = null,
|
||||
val tombstoneEventHandling: Async<String> = Uninitialized,
|
||||
|
|
|
@ -47,8 +47,8 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava
|
|||
fun create(event: TimelineEvent,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?,
|
||||
exception: Exception? = null): DefaultItem {
|
||||
val text = if (exception == null) {
|
||||
throwable: Throwable? = null): DefaultItem {
|
||||
val text = if (throwable == null) {
|
||||
"${event.root.getClearType()} events are not yet handled"
|
||||
} else {
|
||||
"an exception occurred when rendering the event ${event.root.eventId}"
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.home.room.detail.timeline.factory
|
||||
|
||||
import android.view.View
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_
|
||||
import javax.inject.Inject
|
||||
|
||||
class EncryptionItemFactory @Inject constructor(private val stringProvider: StringProvider,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val avatarSizeProvider: AvatarSizeProvider,
|
||||
private val session: Session) {
|
||||
|
||||
fun create(event: TimelineEvent,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?): NoticeItem? {
|
||||
val text = buildNoticeText(event.root, event.getDisambiguatedDisplayName()) ?: return null
|
||||
val informationData = MessageInformationData(
|
||||
eventId = event.root.eventId ?: "?",
|
||||
senderId = event.root.senderId ?: "",
|
||||
sendState = event.root.sendState,
|
||||
ageLocalTS = event.root.ageLocalTs,
|
||||
avatarUrl = event.senderAvatar,
|
||||
memberName = event.getDisambiguatedDisplayName(),
|
||||
showInformation = false,
|
||||
sentByMe = event.root.senderId == session.myUserId
|
||||
)
|
||||
val attributes = NoticeItem.Attributes(
|
||||
avatarRenderer = avatarRenderer,
|
||||
informationData = informationData,
|
||||
noticeText = text,
|
||||
itemLongClickListener = View.OnLongClickListener { view ->
|
||||
callback?.onEventLongClicked(informationData, null, view) ?: false
|
||||
},
|
||||
readReceiptsCallback = callback
|
||||
)
|
||||
return NoticeItem_()
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
.highlighted(highlight)
|
||||
.attributes(attributes)
|
||||
}
|
||||
|
||||
private fun buildNoticeText(event: Event, senderName: String?): CharSequence? {
|
||||
return when {
|
||||
EventType.STATE_ROOM_ENCRYPTION == event.getClearType() -> {
|
||||
val content = event.content.toModel<EncryptionEventContent>() ?: return null
|
||||
stringProvider.getString(R.string.notice_end_to_end, senderName, content.algorithm)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -50,6 +50,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||
EventType.STATE_ROOM_CANONICAL_ALIAS,
|
||||
EventType.STATE_ROOM_JOIN_RULES,
|
||||
EventType.STATE_ROOM_HISTORY_VISIBILITY,
|
||||
EventType.STATE_ROOM_GUEST_ACCESS,
|
||||
EventType.CALL_INVITE,
|
||||
EventType.CALL_HANGUP,
|
||||
EventType.CALL_ANSWER,
|
||||
|
@ -88,9 +89,9 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||
null
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "failed to create message item")
|
||||
defaultItemFactory.create(event, highlight, callback, e)
|
||||
} catch (throwable: Throwable) {
|
||||
Timber.e(throwable, "failed to create message item")
|
||||
defaultItemFactory.create(event, highlight, callback, throwable)
|
||||
}
|
||||
return (computedModel ?: EmptyItem_())
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import im.vector.matrix.android.api.session.events.model.toModel
|
|||
import im.vector.matrix.android.api.session.room.model.*
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
|
@ -40,6 +41,8 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
|
|||
EventType.STATE_ROOM_ALIASES -> formatRoomAliasesEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
||||
EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
||||
EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
||||
EventType.STATE_ROOM_GUEST_ACCESS -> formatRoomGuestAccessEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
||||
EventType.STATE_ROOM_ENCRYPTION -> formatRoomEncryptionEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
||||
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.getDisambiguatedDisplayName())
|
||||
EventType.CALL_INVITE,
|
||||
EventType.CALL_HANGUP,
|
||||
|
@ -173,6 +176,20 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
|
|||
?: sp.getString(R.string.notice_room_canonical_alias_unset, senderName)
|
||||
}
|
||||
|
||||
private fun formatRoomGuestAccessEvent(event: Event, senderName: String?): String? {
|
||||
val eventContent: RoomGuestAccessContent? = event.getClearContent().toModel()
|
||||
return when (eventContent?.guestAccess) {
|
||||
GuestAccess.CanJoin -> sp.getString(R.string.notice_room_guest_access_can_join, senderName)
|
||||
GuestAccess.Forbidden -> sp.getString(R.string.notice_room_guest_access_forbidden, senderName)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatRoomEncryptionEvent(event: Event, senderName: String?): CharSequence? {
|
||||
val content = event.content.toModel<EncryptionEventContent>() ?: return null
|
||||
return sp.getString(R.string.notice_end_to_end, senderName, content.algorithm)
|
||||
}
|
||||
|
||||
private fun buildProfileNotice(event: Event, senderName: String?, eventContent: RoomMemberContent?, prevEventContent: RoomMemberContent?): String {
|
||||
val displayText = StringBuilder()
|
||||
// Check display name has been changed
|
||||
|
|
|
@ -35,6 +35,7 @@ object TimelineDisplayableEvents {
|
|||
EventType.CALL_ANSWER,
|
||||
EventType.ENCRYPTED,
|
||||
EventType.STATE_ROOM_ENCRYPTION,
|
||||
EventType.STATE_ROOM_GUEST_ACCESS,
|
||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
||||
EventType.STICKER,
|
||||
EventType.STATE_ROOM_CREATE,
|
||||
|
|
|
@ -20,6 +20,7 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
|
@ -27,6 +28,7 @@ import im.vector.matrix.android.api.util.MatrixItem
|
|||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.riotx.core.extensions.setTextOrHide
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_room)
|
||||
|
@ -36,6 +38,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
|
|||
@EpoxyAttribute lateinit var matrixItem: MatrixItem
|
||||
@EpoxyAttribute lateinit var lastFormattedEvent: CharSequence
|
||||
@EpoxyAttribute lateinit var lastEventTime: CharSequence
|
||||
@EpoxyAttribute var typingString: CharSequence? = null
|
||||
@EpoxyAttribute var unreadNotificationCount: Int = 0
|
||||
@EpoxyAttribute var hasUnreadMessage: Boolean = false
|
||||
@EpoxyAttribute var hasDraft: Boolean = false
|
||||
|
@ -50,6 +53,8 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
|
|||
holder.titleView.text = matrixItem.getBestName()
|
||||
holder.lastEventTimeView.text = lastEventTime
|
||||
holder.lastEventView.text = lastFormattedEvent
|
||||
holder.typingView.setTextOrHide(typingString)
|
||||
holder.lastEventView.isInvisible = holder.typingView.isVisible
|
||||
holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted))
|
||||
holder.unreadIndentIndicator.isVisible = hasUnreadMessage
|
||||
holder.draftView.isVisible = hasDraft
|
||||
|
@ -61,6 +66,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
|
|||
val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.roomUnreadCounterBadgeView)
|
||||
val unreadIndentIndicator by bind<View>(R.id.roomUnreadIndicator)
|
||||
val lastEventView by bind<TextView>(R.id.roomLastEventView)
|
||||
val typingView by bind<TextView>(R.id.roomTypingView)
|
||||
val draftView by bind<ImageView>(R.id.roomDraftBadge)
|
||||
val lastEventTimeView by bind<TextView>(R.id.roomLastEventTimeView)
|
||||
val avatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package im.vector.riotx.features.home.room.list
|
||||
|
||||
import android.view.View
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.util.toMatrixItem
|
||||
|
@ -30,12 +31,15 @@ import im.vector.riotx.core.resources.StringProvider
|
|||
import im.vector.riotx.core.utils.DebouncedClickListener
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import im.vector.riotx.features.home.room.detail.timeline.format.DisplayableEventFormatter
|
||||
import im.vector.riotx.features.home.room.typing.TypingHelper
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomSummaryItemFactory @Inject constructor(private val displayableEventFormatter: DisplayableEventFormatter,
|
||||
private val dateFormatter: VectorDateFormatter,
|
||||
private val colorProvider: ColorProvider,
|
||||
private val stringProvider: StringProvider,
|
||||
private val typingHelper: TypingHelper,
|
||||
private val session: Session,
|
||||
private val avatarRenderer: AvatarRenderer) {
|
||||
|
||||
fun create(roomSummary: RoomSummary,
|
||||
|
@ -96,11 +100,22 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
|
|||
dateFormatter.formatMessageDay(date)
|
||||
}
|
||||
}
|
||||
|
||||
val typingString = typingHelper.excludeCurrentUser(roomSummary.typingRoomMemberIds)
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.let { typingMembers ->
|
||||
// It's not ideal to get a Room and to fetch data from DB here, but let's keep it like this for the moment
|
||||
val room = session.getRoom(roomSummary.roomId)
|
||||
val typingRoomMembers = typingHelper.toTypingRoomMembers(typingMembers, room)
|
||||
typingHelper.toTypingMessage(typingRoomMembers)
|
||||
}
|
||||
|
||||
return RoomSummaryItem_()
|
||||
.id(roomSummary.roomId)
|
||||
.avatarRenderer(avatarRenderer)
|
||||
.matrixItem(roomSummary.toMatrixItem())
|
||||
.lastEventTime(latestEventTime)
|
||||
.typingString(typingString)
|
||||
.lastFormattedEvent(latestFormattedEvent)
|
||||
.showHighlighted(showHighlighted)
|
||||
.unreadNotificationCount(unreadCount)
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.home.room.typing
|
||||
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.room.members.MembershipService
|
||||
import im.vector.matrix.android.api.util.MatrixItem
|
||||
import im.vector.matrix.android.api.util.toMatrixItem
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
class TypingHelper @Inject constructor(
|
||||
private val session: Session,
|
||||
private val stringProvider: StringProvider
|
||||
) {
|
||||
/**
|
||||
* Exclude current user from the list of typing users
|
||||
*/
|
||||
fun excludeCurrentUser(
|
||||
typingUserIds: List<String>
|
||||
): List<String> {
|
||||
return typingUserIds
|
||||
.filter { it != session.myUserId }
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a list of userId to a list of maximum 3 UserItems
|
||||
*/
|
||||
fun toTypingRoomMembers(
|
||||
typingUserIds: List<String>,
|
||||
membershipService: MembershipService?
|
||||
): List<MatrixItem.UserItem> {
|
||||
return excludeCurrentUser(typingUserIds)
|
||||
.take(3)
|
||||
.mapNotNull { membershipService?.getRoomMember(it) }
|
||||
.map { it.toMatrixItem() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a list of typing UserItems to a human readable String
|
||||
*/
|
||||
fun toTypingMessage(typingUserItems: List<MatrixItem.UserItem>): String? {
|
||||
return when {
|
||||
typingUserItems.isEmpty() ->
|
||||
null
|
||||
typingUserItems.size == 1 ->
|
||||
stringProvider.getString(R.string.room_one_user_is_typing, typingUserItems[0].getBestName())
|
||||
typingUserItems.size == 2 ->
|
||||
stringProvider.getString(R.string.room_two_users_are_typing, typingUserItems[0].getBestName(), typingUserItems[1].getBestName())
|
||||
else ->
|
||||
stringProvider.getString(R.string.room_many_users_are_typing, typingUserItems[0].getBestName(), typingUserItems[1].getBestName())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -36,6 +36,7 @@ import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
|
|||
import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
|
||||
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
|
||||
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity
|
||||
import im.vector.riotx.features.settings.VectorPreferences
|
||||
import im.vector.riotx.features.settings.VectorSettingsActivity
|
||||
import im.vector.riotx.features.share.SharedData
|
||||
import timber.log.Timber
|
||||
|
@ -44,12 +45,13 @@ import javax.inject.Singleton
|
|||
|
||||
@Singleton
|
||||
class DefaultNavigator @Inject constructor(
|
||||
private val sessionHolder: ActiveSessionHolder
|
||||
private val sessionHolder: ActiveSessionHolder,
|
||||
private val vectorPreferences: VectorPreferences
|
||||
) : Navigator {
|
||||
|
||||
override fun openRoom(context: Context, roomId: String, eventId: String?, buildTask: Boolean) {
|
||||
if (sessionHolder.getSafeActiveSession()?.getRoom(roomId) == null) {
|
||||
fatalError("Trying to open an unknown room $roomId")
|
||||
fatalError("Trying to open an unknown room $roomId", vectorPreferences.failFast())
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import android.provider.MediaStore
|
|||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.squareup.seismic.ShakeDetector
|
||||
import im.vector.riotx.BuildConfig
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.features.homeserver.ServerUrlsRepository
|
||||
import im.vector.riotx.features.themes.ThemeUtils
|
||||
|
@ -268,7 +269,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
|||
}
|
||||
|
||||
fun failFast(): Boolean {
|
||||
return developerMode() && defaultPrefs.getBoolean(SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY, false)
|
||||
return BuildConfig.DEBUG || (developerMode() && defaultPrefs.getBoolean(SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY, false))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
8
vector/src/main/res/drawable/bg_breadcrumbs_typing.xml
Normal file
8
vector/src/main/res/drawable/bg_breadcrumbs_typing.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<corners android:radius="40dp" />
|
||||
|
||||
<solid android:color="@color/riotx_accent" />
|
||||
</shape>
|
|
@ -53,6 +53,23 @@
|
|||
tools:text="24"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/breadcrumbsTypingView"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:background="@drawable/bg_breadcrumbs_typing"
|
||||
android:gravity="center"
|
||||
android:text="@string/ellipsis"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="11sp"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintCircle="@+id/breadcrumbsImageView"
|
||||
app:layout_constraintCircleAngle="135"
|
||||
app:layout_constraintCircleRadius="28dp"
|
||||
tools:ignore="MissingConstraints"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/breadcrumbsDraftBadge"
|
||||
android:layout_width="20dp"
|
||||
|
@ -62,7 +79,7 @@
|
|||
android:src="@drawable/ic_edit"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintCircle="@+id/breadcrumbsImageView"
|
||||
app:layout_constraintCircleAngle="135"
|
||||
app:layout_constraintCircleAngle="225"
|
||||
app:layout_constraintCircleRadius="28dp"
|
||||
tools:ignore="MissingConstraints"
|
||||
tools:visibility="visible" />
|
||||
|
|
|
@ -15,11 +15,11 @@
|
|||
android:layout_width="4dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="?attr/colorAccent"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"/>
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/roomAvatarImageView"
|
||||
|
@ -128,6 +128,23 @@
|
|||
app:layout_constraintTop_toBottomOf="@+id/roomNameView"
|
||||
tools:text="@sample/matrix.json/data/message" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/roomTypingView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="3dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:textColor="@color/riotx_accent"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/roomNameView"
|
||||
app:layout_constraintTop_toBottomOf="@+id/roomNameView"
|
||||
tools:text="Alice is typing…" />
|
||||
|
||||
<!-- Margin bottom does not work, so I use space -->
|
||||
<Space
|
||||
android:id="@+id/roomLastEventBottomSpace"
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
<string name="debug_screen" translatable="false">Debug screen</string>
|
||||
|
||||
<string name="ellipsis" translatable="false">…</string>
|
||||
<string name="plus_sign" translatable="false">+</string>
|
||||
<string name="semicolon_sign" translatable="false">:</string>
|
||||
|
||||
|
|
|
@ -37,8 +37,7 @@
|
|||
android:defaultValue="true"
|
||||
android:key="SETTINGS_SEND_TYPING_NOTIF_KEY"
|
||||
android:summary="@string/settings_send_typing_notifs_summary"
|
||||
android:title="@string/settings_send_typing_notifs"
|
||||
app:isPreferenceVisible="@bool/false_not_implemented" />
|
||||
android:title="@string/settings_send_typing_notifs" />
|
||||
|
||||
<im.vector.riotx.core.preference.VectorSwitchPreference
|
||||
android:defaultValue="false"
|
||||
|
|
Loading…
Add table
Reference in a new issue