diff --git a/CHANGES.md b/CHANGES.md index 9f9d49d0a0..a893257516 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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) =================================================== diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt index 41548710eb..8878930de0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt @@ -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 { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt index 3221c355e8..0c3316e802 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt @@ -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, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMemberQueryParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMemberQueryParams.kt index 19003632ca..b8ad55213c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMemberQueryParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMemberQueryParams.kt @@ -28,17 +28,20 @@ fun roomMemberQueryParams(init: (RoomMemberQueryParams.Builder.() -> Unit) = {}) */ data class RoomMemberQueryParams( val displayName: QueryStringValue, - val memberships: List + val memberships: List, + val excludeSelf: Boolean ) { class Builder { var displayName: QueryStringValue = QueryStringValue.IsNotEmpty var memberships: List = Membership.all() + var excludeSelf: Boolean = false fun build() = RoomMemberQueryParams( displayName = displayName, - memberships = memberships + memberships = memberships, + excludeSelf = excludeSelf ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomGuestAccessContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomGuestAccessContent.kt new file mode 100644 index 0000000000..4c814f7914 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomGuestAccessContent.kt @@ -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") +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt index c18645ddbd..2f420de164 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt @@ -42,7 +42,8 @@ data class RoomSummary( val versioningState: VersioningState = VersioningState.NONE, val readMarkerId: String? = null, val userDrafts: List = emptyList(), - var isEncrypted: Boolean + var isEncrypted: Boolean, + val typingRoomMemberIds: List = emptyList() ) { val isVersioned: Boolean diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt index dbdd5b5a34..2c192ee8c7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt @@ -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? = null @@ -120,14 +122,14 @@ class CreateRoomParams { * * @param algorithm the algorithm */ - fun addCryptoAlgorithm(algorithm: String) { - if (algorithm.isNotBlank()) { - val contentMap = HashMap() - 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() - 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)) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/typing/TypingService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/typing/TypingService.kt new file mode 100644 index 0000000000..8ef550531e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/typing/TypingService.kt @@ -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() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt index 7d25a846ff..3d1b5a2d08 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt @@ -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() ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt index 4c99832b39..d857d8810c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt @@ -44,7 +44,8 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "", var aliases: RealmList = RealmList(), // this is required for querying var flatAliases: String = "", - var isEncrypted: Boolean = false + var isEncrypted: Boolean = false, + var typingUserIds: RealmList = RealmList() ) : RealmObject() { private var membershipStr: String = Membership.NONE.name diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt index 5c87f84d73..fc95dd4bb4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt @@ -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 { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt index 6896788de9..622ffbe2f0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt @@ -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 + + /** + * 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 } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index a21a3b4a8d..b24bb73d56 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -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), diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index cc786a7493..5551930bd1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -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 } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt index 9fa922b940..6d4dac64b5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt @@ -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()?.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) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt index 679f4a050b..dc44359666 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt @@ -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 { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/typing/DefaultTypingService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/typing/DefaultTypingService.kt new file mode 100644 index 0000000000..4989d8a235 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/typing/DefaultTypingService.kt @@ -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 { + 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 + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/typing/SendTypingTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/typing/SendTypingTask.kt new file mode 100644 index 0000000000..6460933621 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/typing/SendTypingTask.kt @@ -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 { + + 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(eventBus) { + apiCall = roomAPI.sendTypingState( + params.roomId, + userId, + TypingBody(params.isTyping, params.typingTimeoutMillis?.takeIf { params.isTyping }) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/typing/TypingBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/typing/TypingBody.kt new file mode 100644 index 0000000000..26099599d7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/typing/TypingBody.kt @@ -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? +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/typing/TypingEventContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/typing/TypingEventContent.kt new file mode 100644 index 0000000000..969db3f235 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/typing/TypingEventContent.kt @@ -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 = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index f814dc5046..cfac75542d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -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 = 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()?.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) { diff --git a/matrix-sdk-android/src/main/res/values/strings.xml b/matrix-sdk-android/src/main/res/values/strings.xml index c0506dd549..d5cd7a117b 100644 --- a/matrix-sdk-android/src/main/res/values/strings.xml +++ b/matrix-sdk-android/src/main/res/values/strings.xml @@ -272,4 +272,7 @@ "%1$s set the main address for this room to %2$s." "%1$s removed the main address for this room." + "%1$s has allowed guests to join the room." + "%1$s has prevented guests from joining the room." + diff --git a/vector/build.gradle b/vector/build.gradle index 19bdd1dd8c..4960c6b796 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -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 } } } diff --git a/vector/src/main/java/im/vector/riotx/core/error/fatal.kt b/vector/src/main/java/im/vector/riotx/core/error/fatal.kt index 800e1ea7ad..ad6a99928a 100644 --- a/vector/src/main/java/im/vector/riotx/core/error/fatal.kt +++ b/vector/src/main/java/im/vector/riotx/core/error/fatal.kt @@ -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) diff --git a/vector/src/main/java/im/vector/riotx/core/rx/RxConfig.kt b/vector/src/main/java/im/vector/riotx/core/rx/RxConfig.kt index d8828eb1b8..bd87251a58 100644 --- a/vector/src/main/java/im/vector/riotx/core/rx/RxConfig.kt +++ b/vector/src/main/java/im/vector/riotx/core/rx/RxConfig.kt @@ -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 } } diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt index 84a33173b8..bc50b12e6e 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt @@ -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() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsController.kt index 3b77835917..2761be88f1 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsController.kt @@ -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 { _ -> diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsItem.kt index 6d18a85b75..8b36b307a9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsItem.kt @@ -37,6 +37,7 @@ abstract class BreadcrumbsItem : VectorEpoxyModel() { @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() { 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() { val unreadCounterBadgeView by bind(R.id.breadcrumbsUnreadCounterBadgeView) val unreadIndentIndicator by bind(R.id.breadcrumbsUnreadIndicator) val draftIndentIndicator by bind(R.id.breadcrumbsDraftBadge) + val typingIndicator by bind(R.id.breadcrumbsTypingView) val avatarImageView by bind(R.id.breadcrumbsImageView) val rootView by bind(R.id.breadcrumbsRoot) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt index 768f188d7e..358a5f3f57 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt @@ -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) : RoomDetailAction() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index eaea0017c6..3d24d65086 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -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) { 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) { 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, 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(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(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") } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 3d353e3475..8de3bad570 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -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(initialState), Timeline.Listener { @@ -92,16 +89,16 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private val visibleEventsObservable = BehaviorRelay.create() 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>() @@ -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() - ?: return + val tombstoneContent = action.event.getClearContent().toModel() ?: 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()?.relatesTo?.inReplyTo?.eventId - ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId + ?: state.sendMode.timelineEvent.root.content.toModel()?.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() } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt index 165ef7b625..43a454d32e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt @@ -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 = Uninitialized, val asyncRoomSummary: Async = Uninitialized, + val typingRoomMembers: List? = null, + val typingMessage: String? = null, val sendMode: SendMode = SendMode.REGULAR(""), val tombstoneEvent: Event? = null, val tombstoneEventHandling: Async = Uninitialized, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt index 94d7812512..d9bed98b1f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt @@ -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}" diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt deleted file mode 100644 index 3080dcb2f4..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt +++ /dev/null @@ -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() ?: return null - stringProvider.getString(R.string.notice_end_to_end, senderName, content.algorithm) - } - else -> null - } - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index d3503300e1..aaf3135dab 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -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_()) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index ae03a8c2f4..2aeeb34022 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -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() ?: 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 diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index a09f61b0a4..9f1eabe0ee 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -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, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt index 23a0fd60a2..15e7a63b7b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt @@ -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() { @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() { 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() { val unreadCounterBadgeView by bind(R.id.roomUnreadCounterBadgeView) val unreadIndentIndicator by bind(R.id.roomUnreadIndicator) val lastEventView by bind(R.id.roomLastEventView) + val typingView by bind(R.id.roomTypingView) val draftView by bind(R.id.roomDraftBadge) val lastEventTimeView by bind(R.id.roomLastEventTimeView) val avatarImageView by bind(R.id.roomAvatarImageView) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt index f2ff79162f..d42c509cb7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt @@ -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) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/typing/TypingHelper.kt b/vector/src/main/java/im/vector/riotx/features/home/room/typing/TypingHelper.kt new file mode 100644 index 0000000000..e3dc3dc7ee --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/typing/TypingHelper.kt @@ -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 + ): List { + return typingUserIds + .filter { it != session.myUserId } + } + + /** + * Convert a list of userId to a list of maximum 3 UserItems + */ + fun toTypingRoomMembers( + typingUserIds: List, + membershipService: MembershipService? + ): List { + 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): 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()) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index 48422056b4..8784ec662d 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -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 } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt index 72f8cf01dd..c734558c0e 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt @@ -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)) } /** diff --git a/vector/src/main/res/drawable/bg_breadcrumbs_typing.xml b/vector/src/main/res/drawable/bg_breadcrumbs_typing.xml new file mode 100644 index 0000000000..47a095c8ba --- /dev/null +++ b/vector/src/main/res/drawable/bg_breadcrumbs_typing.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_breadcrumbs.xml b/vector/src/main/res/layout/item_breadcrumbs.xml index a7a312f16b..6596f71b40 100644 --- a/vector/src/main/res/layout/item_breadcrumbs.xml +++ b/vector/src/main/res/layout/item_breadcrumbs.xml @@ -53,6 +53,23 @@ tools:text="24" tools:visibility="visible" /> + + diff --git a/vector/src/main/res/layout/item_room.xml b/vector/src/main/res/layout/item_room.xml index 741bd47069..3c9e40fae5 100644 --- a/vector/src/main/res/layout/item_room.xml +++ b/vector/src/main/res/layout/item_room.xml @@ -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" /> + + Debug screen + + : diff --git a/vector/src/main/res/xml/vector_settings_preferences.xml b/vector/src/main/res/xml/vector_settings_preferences.xml index 7698372053..ead6a30fa6 100644 --- a/vector/src/main/res/xml/vector_settings_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_preferences.xml @@ -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" />