mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-03 12:37:31 +01:00
Merge pull request #833 from vector-im/feature/typing
Send and render typing events (#564)
This commit is contained in:
commit
b5fead18fe
@ -2,7 +2,7 @@ Changes in RiotX 0.13.0 (2020-XX-XX)
|
|||||||
===================================================
|
===================================================
|
||||||
|
|
||||||
Features ✨:
|
Features ✨:
|
||||||
-
|
- Send and render typing events (#564)
|
||||||
|
|
||||||
Improvements 🙌:
|
Improvements 🙌:
|
||||||
- Render events m.room.encryption and m.room.guest_access in the timeline
|
- Render events m.room.encryption and m.room.guest_access in the timeline
|
||||||
|
@ -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.RoomSummary
|
||||||
import im.vector.matrix.android.api.session.room.model.relation.RelationService
|
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.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.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.DraftService
|
||||||
import im.vector.matrix.android.api.session.room.send.SendService
|
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.state.StateService
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineService
|
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.Optional
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -38,6 +39,7 @@ interface Room :
|
|||||||
SendService,
|
SendService,
|
||||||
DraftService,
|
DraftService,
|
||||||
ReadService,
|
ReadService,
|
||||||
|
TypingService,
|
||||||
MembershipService,
|
MembershipService,
|
||||||
StateService,
|
StateService,
|
||||||
ReportingService,
|
ReportingService,
|
||||||
|
@ -42,7 +42,8 @@ data class RoomSummary(
|
|||||||
val versioningState: VersioningState = VersioningState.NONE,
|
val versioningState: VersioningState = VersioningState.NONE,
|
||||||
val readMarkerId: String? = null,
|
val readMarkerId: String? = null,
|
||||||
val userDrafts: List<UserDraft> = emptyList(),
|
val userDrafts: List<UserDraft> = emptyList(),
|
||||||
var isEncrypted: Boolean
|
var isEncrypted: Boolean,
|
||||||
|
val typingRoomMemberIds: List<String> = emptyList()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val isVersioned: Boolean
|
val isVersioned: Boolean
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.api.session.room.typing
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This interface defines methods to handle typing data. It's implemented at the room level.
|
||||||
|
*/
|
||||||
|
interface TypingService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To call when user is typing a message in the room
|
||||||
|
* The SDK will handle the requests scheduling to the homeserver:
|
||||||
|
* - No more than one typing request per 10s
|
||||||
|
* - If not called after 10s, the SDK will notify the homeserver that the user is not typing anymore
|
||||||
|
*/
|
||||||
|
fun userIsTyping()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To call when user stops typing in the room
|
||||||
|
* Notify immediately the homeserver that the user is not typing anymore in the room, for
|
||||||
|
* instance when user has emptied the composer, or when the user quits the timeline screen.
|
||||||
|
*/
|
||||||
|
fun userStopsTyping()
|
||||||
|
}
|
@ -22,7 +22,7 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary
|
|||||||
import im.vector.matrix.android.api.session.room.model.tag.RoomTag
|
import im.vector.matrix.android.api.session.room.model.tag.RoomTag
|
||||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||||
import java.util.UUID
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class RoomSummaryMapper @Inject constructor(
|
internal class RoomSummaryMapper @Inject constructor(
|
||||||
@ -71,7 +71,8 @@ internal class RoomSummaryMapper @Inject constructor(
|
|||||||
userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList(),
|
userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList(),
|
||||||
canonicalAlias = roomSummaryEntity.canonicalAlias,
|
canonicalAlias = roomSummaryEntity.canonicalAlias,
|
||||||
aliases = roomSummaryEntity.aliases.toList(),
|
aliases = roomSummaryEntity.aliases.toList(),
|
||||||
isEncrypted = roomSummaryEntity.isEncrypted
|
isEncrypted = roomSummaryEntity.isEncrypted,
|
||||||
|
typingRoomMemberIds = roomSummaryEntity.typingUserIds.toList()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,8 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
|
|||||||
var aliases: RealmList<String> = RealmList(),
|
var aliases: RealmList<String> = RealmList(),
|
||||||
// this is required for querying
|
// this is required for querying
|
||||||
var flatAliases: String = "",
|
var flatAliases: String = "",
|
||||||
var isEncrypted: Boolean = false
|
var isEncrypted: Boolean = false,
|
||||||
|
var typingUserIds: RealmList<String> = RealmList()
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
private var membershipStr: String = Membership.NONE.name
|
private var membershipStr: String = Membership.NONE.name
|
||||||
|
@ -32,6 +32,7 @@ import im.vector.matrix.android.api.session.room.send.DraftService
|
|||||||
import im.vector.matrix.android.api.session.room.send.SendService
|
import im.vector.matrix.android.api.session.room.send.SendService
|
||||||
import im.vector.matrix.android.api.session.room.state.StateService
|
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.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.Optional
|
||||||
import im.vector.matrix.android.api.util.toOptional
|
import im.vector.matrix.android.api.util.toOptional
|
||||||
import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
|
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 stateService: StateService,
|
||||||
private val reportingService: ReportingService,
|
private val reportingService: ReportingService,
|
||||||
private val readService: ReadService,
|
private val readService: ReadService,
|
||||||
|
private val typingService: TypingService,
|
||||||
private val cryptoService: CryptoService,
|
private val cryptoService: CryptoService,
|
||||||
private val relationService: RelationService,
|
private val relationService: RelationService,
|
||||||
private val roomMembersService: MembershipService,
|
private val roomMembersService: MembershipService,
|
||||||
@ -60,6 +62,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
|
|||||||
StateService by stateService,
|
StateService by stateService,
|
||||||
ReportingService by reportingService,
|
ReportingService by reportingService,
|
||||||
ReadService by readService,
|
ReadService by readService,
|
||||||
|
TypingService by typingService,
|
||||||
RelationService by relationService,
|
RelationService by relationService,
|
||||||
MembershipService by roomMembersService,
|
MembershipService by roomMembersService,
|
||||||
RoomPushRuleService by roomPushRuleService {
|
RoomPushRuleService by roomPushRuleService {
|
||||||
|
@ -18,13 +18,13 @@ package im.vector.matrix.android.internal.session.room
|
|||||||
|
|
||||||
import im.vector.matrix.android.api.session.events.model.Content
|
import im.vector.matrix.android.api.session.events.model.Content
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
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.CreateRoomParams
|
||||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse
|
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.PublicRoomsParams
|
||||||
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsResponse
|
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.api.session.room.model.thirdparty.ThirdPartyProtocol
|
||||||
import im.vector.matrix.android.internal.network.NetworkConstants
|
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.RoomMembersResponse
|
||||||
import im.vector.matrix.android.internal.session.room.membership.joining.InviteBody
|
import im.vector.matrix.android.internal.session.room.membership.joining.InviteBody
|
||||||
import im.vector.matrix.android.internal.session.room.relation.RelationsResponse
|
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.send.SendResponse
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.EventContextResponse
|
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.timeline.PaginationResponse
|
||||||
|
import im.vector.matrix.android.internal.session.room.typing.TypingBody
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.http.*
|
import retrofit2.http.*
|
||||||
|
|
||||||
@ -268,4 +269,12 @@ internal interface RoomAPI {
|
|||||||
*/
|
*/
|
||||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}")
|
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}")
|
||||||
fun getRoomIdByAlias(@Path("roomAlias") roomAlias: String): Call<RoomAliasDescription>
|
fun getRoomIdByAlias(@Path("roomAlias") roomAlias: String): Call<RoomAliasDescription>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inform that the user is starting to type or has stopped typing
|
||||||
|
*/
|
||||||
|
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/typing/{userId}")
|
||||||
|
fun sendTypingState(@Path("roomId") roomId: String,
|
||||||
|
@Path("userId") userId: String,
|
||||||
|
@Body body: TypingBody): Call<Unit>
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.session.room.reporting.DefaultReporting
|
|||||||
import im.vector.matrix.android.internal.session.room.send.DefaultSendService
|
import im.vector.matrix.android.internal.session.room.send.DefaultSendService
|
||||||
import im.vector.matrix.android.internal.session.room.state.DefaultStateService
|
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.timeline.DefaultTimelineService
|
||||||
|
import im.vector.matrix.android.internal.session.room.typing.DefaultTypingService
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal interface RoomFactory {
|
internal interface RoomFactory {
|
||||||
@ -46,6 +47,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
|
|||||||
private val stateServiceFactory: DefaultStateService.Factory,
|
private val stateServiceFactory: DefaultStateService.Factory,
|
||||||
private val reportingServiceFactory: DefaultReportingService.Factory,
|
private val reportingServiceFactory: DefaultReportingService.Factory,
|
||||||
private val readServiceFactory: DefaultReadService.Factory,
|
private val readServiceFactory: DefaultReadService.Factory,
|
||||||
|
private val typingServiceFactory: DefaultTypingService.Factory,
|
||||||
private val relationServiceFactory: DefaultRelationService.Factory,
|
private val relationServiceFactory: DefaultRelationService.Factory,
|
||||||
private val membershipServiceFactory: DefaultMembershipService.Factory,
|
private val membershipServiceFactory: DefaultMembershipService.Factory,
|
||||||
private val roomPushRuleServiceFactory: DefaultRoomPushRuleService.Factory) :
|
private val roomPushRuleServiceFactory: DefaultRoomPushRuleService.Factory) :
|
||||||
@ -62,6 +64,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
|
|||||||
stateServiceFactory.create(roomId),
|
stateServiceFactory.create(roomId),
|
||||||
reportingServiceFactory.create(roomId),
|
reportingServiceFactory.create(roomId),
|
||||||
readServiceFactory.create(roomId),
|
readServiceFactory.create(roomId),
|
||||||
|
typingServiceFactory.create(roomId),
|
||||||
cryptoService,
|
cryptoService,
|
||||||
relationServiceFactory.create(roomId),
|
relationServiceFactory.create(roomId),
|
||||||
membershipServiceFactory.create(roomId),
|
membershipServiceFactory.create(roomId),
|
||||||
|
@ -52,6 +52,8 @@ import im.vector.matrix.android.internal.session.room.reporting.ReportContentTas
|
|||||||
import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask
|
import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask
|
||||||
import im.vector.matrix.android.internal.session.room.state.SendStateTask
|
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.timeline.*
|
||||||
|
import im.vector.matrix.android.internal.session.room.typing.DefaultSendTypingTask
|
||||||
|
import im.vector.matrix.android.internal.session.room.typing.SendTypingTask
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@ -68,74 +70,77 @@ internal abstract class RoomModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindRoomFactory(roomFactory: DefaultRoomFactory): RoomFactory
|
abstract fun bindRoomFactory(factory: DefaultRoomFactory): RoomFactory
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindRoomService(roomService: DefaultRoomService): RoomService
|
abstract fun bindRoomService(service: DefaultRoomService): RoomService
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindRoomDirectoryService(roomDirectoryService: DefaultRoomDirectoryService): RoomDirectoryService
|
abstract fun bindRoomDirectoryService(service: DefaultRoomDirectoryService): RoomDirectoryService
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindEventRelationsAggregationTask(eventRelationsAggregationTask: DefaultEventRelationsAggregationTask): EventRelationsAggregationTask
|
abstract fun bindFileService(service: DefaultFileService): FileService
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindCreateRoomTask(createRoomTask: DefaultCreateRoomTask): CreateRoomTask
|
abstract fun bindEventRelationsAggregationTask(task: DefaultEventRelationsAggregationTask): EventRelationsAggregationTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindGetPublicRoomTask(getPublicRoomTask: DefaultGetPublicRoomTask): GetPublicRoomTask
|
abstract fun bindCreateRoomTask(task: DefaultCreateRoomTask): CreateRoomTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindGetThirdPartyProtocolsTask(getThirdPartyProtocolsTask: DefaultGetThirdPartyProtocolsTask): GetThirdPartyProtocolsTask
|
abstract fun bindGetPublicRoomTask(task: DefaultGetPublicRoomTask): GetPublicRoomTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindInviteTask(inviteTask: DefaultInviteTask): InviteTask
|
abstract fun bindGetThirdPartyProtocolsTask(task: DefaultGetThirdPartyProtocolsTask): GetThirdPartyProtocolsTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindJoinRoomTask(joinRoomTask: DefaultJoinRoomTask): JoinRoomTask
|
abstract fun bindInviteTask(task: DefaultInviteTask): InviteTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindLeaveRoomTask(leaveRoomTask: DefaultLeaveRoomTask): LeaveRoomTask
|
abstract fun bindJoinRoomTask(task: DefaultJoinRoomTask): JoinRoomTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindLoadRoomMembersTask(loadRoomMembersTask: DefaultLoadRoomMembersTask): LoadRoomMembersTask
|
abstract fun bindLeaveRoomTask(task: DefaultLeaveRoomTask): LeaveRoomTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindPruneEventTask(pruneEventTask: DefaultPruneEventTask): PruneEventTask
|
abstract fun bindLoadRoomMembersTask(task: DefaultLoadRoomMembersTask): LoadRoomMembersTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindSetReadMarkersTask(setReadMarkersTask: DefaultSetReadMarkersTask): SetReadMarkersTask
|
abstract fun bindPruneEventTask(task: DefaultPruneEventTask): PruneEventTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindMarkAllRoomsReadTask(markAllRoomsReadTask: DefaultMarkAllRoomsReadTask): MarkAllRoomsReadTask
|
abstract fun bindSetReadMarkersTask(task: DefaultSetReadMarkersTask): SetReadMarkersTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindFindReactionEventForUndoTask(findReactionEventForUndoTask: DefaultFindReactionEventForUndoTask): FindReactionEventForUndoTask
|
abstract fun bindMarkAllRoomsReadTask(task: DefaultMarkAllRoomsReadTask): MarkAllRoomsReadTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindUpdateQuickReactionTask(updateQuickReactionTask: DefaultUpdateQuickReactionTask): UpdateQuickReactionTask
|
abstract fun bindFindReactionEventForUndoTask(task: DefaultFindReactionEventForUndoTask): FindReactionEventForUndoTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindSendStateTask(sendStateTask: DefaultSendStateTask): SendStateTask
|
abstract fun bindUpdateQuickReactionTask(task: DefaultUpdateQuickReactionTask): UpdateQuickReactionTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindReportContentTask(reportContentTask: DefaultReportContentTask): ReportContentTask
|
abstract fun bindSendStateTask(task: DefaultSendStateTask): SendStateTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindGetContextOfEventTask(getContextOfEventTask: DefaultGetContextOfEventTask): GetContextOfEventTask
|
abstract fun bindReportContentTask(task: DefaultReportContentTask): ReportContentTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindClearUnlinkedEventsTask(clearUnlinkedEventsTask: DefaultClearUnlinkedEventsTask): ClearUnlinkedEventsTask
|
abstract fun bindGetContextOfEventTask(task: DefaultGetContextOfEventTask): GetContextOfEventTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindPaginationTask(paginationTask: DefaultPaginationTask): PaginationTask
|
abstract fun bindClearUnlinkedEventsTask(task: DefaultClearUnlinkedEventsTask): ClearUnlinkedEventsTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindFileService(fileService: DefaultFileService): FileService
|
abstract fun bindPaginationTask(task: DefaultPaginationTask): PaginationTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindFetchEditHistoryTask(fetchEditHistoryTask: DefaultFetchEditHistoryTask): FetchEditHistoryTask
|
abstract fun bindFetchEditHistoryTask(task: DefaultFetchEditHistoryTask): FetchEditHistoryTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindGetRoomIdByAliasTask(getRoomIdByAliasTask: DefaultGetRoomIdByAliasTask): GetRoomIdByAliasTask
|
abstract fun bindGetRoomIdByAliasTask(task: DefaultGetRoomIdByAliasTask): GetRoomIdByAliasTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindSendTypingTask(task: DefaultSendTypingTask): SendTypingTask
|
||||||
}
|
}
|
||||||
|
@ -24,14 +24,15 @@ import im.vector.matrix.android.api.session.room.model.RoomAliasesContent
|
|||||||
import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent
|
import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
|
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.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.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.RoomSummaryEntity
|
||||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||||
import im.vector.matrix.android.internal.database.query.*
|
import im.vector.matrix.android.internal.database.query.*
|
||||||
import im.vector.matrix.android.internal.di.UserId
|
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.RoomDisplayNameResolver
|
||||||
import im.vector.matrix.android.internal.session.room.membership.RoomMembers
|
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.RoomSyncSummary
|
||||||
import im.vector.matrix.android.internal.session.sync.model.RoomSyncUnreadNotifications
|
import im.vector.matrix.android.internal.session.sync.model.RoomSyncUnreadNotifications
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
@ -65,7 +66,8 @@ internal class RoomSummaryUpdater @Inject constructor(
|
|||||||
membership: Membership? = null,
|
membership: Membership? = null,
|
||||||
roomSummary: RoomSyncSummary? = null,
|
roomSummary: RoomSyncSummary? = null,
|
||||||
unreadNotifications: RoomSyncUnreadNotifications? = null,
|
unreadNotifications: RoomSyncUnreadNotifications? = null,
|
||||||
updateMembers: Boolean = false) {
|
updateMembers: Boolean = false,
|
||||||
|
ephemeralResult: RoomSyncHandler.EphemeralResult? = null) {
|
||||||
val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
|
val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
|
||||||
if (roomSummary != null) {
|
if (roomSummary != null) {
|
||||||
if (roomSummary.heroes.isNotEmpty()) {
|
if (roomSummary.heroes.isNotEmpty()) {
|
||||||
@ -109,6 +111,8 @@ internal class RoomSummaryUpdater @Inject constructor(
|
|||||||
roomSummaryEntity.aliases.addAll(roomAliases)
|
roomSummaryEntity.aliases.addAll(roomAliases)
|
||||||
roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|")
|
roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|")
|
||||||
roomSummaryEntity.isEncrypted = encryptionEvent != null
|
roomSummaryEntity.isEncrypted = encryptionEvent != null
|
||||||
|
roomSummaryEntity.typingUserIds.clear()
|
||||||
|
roomSummaryEntity.typingUserIds.addAll(ephemeralResult?.typingUserIds.orEmpty())
|
||||||
|
|
||||||
if (updateMembers) {
|
if (updateMembers) {
|
||||||
val otherRoomMembers = RoomMembers(realm, roomId)
|
val otherRoomMembers = RoomMembers(realm, roomId)
|
||||||
|
@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.session.room.typing
|
||||||
|
|
||||||
|
import android.os.SystemClock
|
||||||
|
import com.squareup.inject.assisted.Assisted
|
||||||
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.session.room.typing.TypingService
|
||||||
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
|
import im.vector.matrix.android.internal.task.configureWith
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rules:
|
||||||
|
* - user is typing: notify the homeserver (true), at least once every 10s
|
||||||
|
* - user stop typing: after 10s delay: notify the homeserver (false)
|
||||||
|
* - user empty the text composer or quit the timeline screen: notify the homeserver (false)
|
||||||
|
*/
|
||||||
|
internal class DefaultTypingService @AssistedInject constructor(
|
||||||
|
@Assisted private val roomId: String,
|
||||||
|
private val taskExecutor: TaskExecutor,
|
||||||
|
private val sendTypingTask: SendTypingTask
|
||||||
|
) : TypingService {
|
||||||
|
|
||||||
|
@AssistedInject.Factory
|
||||||
|
interface Factory {
|
||||||
|
fun create(roomId: String): TypingService
|
||||||
|
}
|
||||||
|
|
||||||
|
private var currentTask: Cancelable? = null
|
||||||
|
private var currentAutoStopTask: Cancelable? = null
|
||||||
|
|
||||||
|
// What the homeserver knows
|
||||||
|
private var userIsTyping = false
|
||||||
|
// Last time the user is typing event has been sent
|
||||||
|
private var lastRequestTimestamp: Long = 0
|
||||||
|
|
||||||
|
override fun userIsTyping() {
|
||||||
|
scheduleAutoStop()
|
||||||
|
|
||||||
|
val now = SystemClock.elapsedRealtime()
|
||||||
|
|
||||||
|
if (userIsTyping && now < lastRequestTimestamp + MIN_DELAY_BETWEEN_TWO_USER_IS_TYPING_REQUESTS_MILLIS) {
|
||||||
|
Timber.d("Typing: Skip start request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.d("Typing: Send start request")
|
||||||
|
userIsTyping = true
|
||||||
|
lastRequestTimestamp = now
|
||||||
|
|
||||||
|
currentTask?.cancel()
|
||||||
|
|
||||||
|
val params = SendTypingTask.Params(roomId, true)
|
||||||
|
currentTask = sendTypingTask
|
||||||
|
.configureWith(params)
|
||||||
|
.executeBy(taskExecutor)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun userStopsTyping() {
|
||||||
|
if (!userIsTyping) {
|
||||||
|
Timber.d("Typing: Skip stop request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.d("Typing: Send stop request")
|
||||||
|
userIsTyping = false
|
||||||
|
lastRequestTimestamp = 0
|
||||||
|
|
||||||
|
currentAutoStopTask?.cancel()
|
||||||
|
currentTask?.cancel()
|
||||||
|
|
||||||
|
val params = SendTypingTask.Params(roomId, false)
|
||||||
|
currentTask = sendTypingTask
|
||||||
|
.configureWith(params)
|
||||||
|
.executeBy(taskExecutor)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scheduleAutoStop() {
|
||||||
|
Timber.d("Typing: Schedule auto stop")
|
||||||
|
currentAutoStopTask?.cancel()
|
||||||
|
|
||||||
|
val params = SendTypingTask.Params(
|
||||||
|
roomId,
|
||||||
|
false,
|
||||||
|
delay = MIN_DELAY_TO_SEND_STOP_TYPING_REQUEST_WHEN_NO_USER_ACTIVITY_MILLIS)
|
||||||
|
currentAutoStopTask = sendTypingTask
|
||||||
|
.configureWith(params) {
|
||||||
|
callback = object : MatrixCallback<Unit> {
|
||||||
|
override fun onSuccess(data: Unit) {
|
||||||
|
userIsTyping = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.executeBy(taskExecutor)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MIN_DELAY_BETWEEN_TWO_USER_IS_TYPING_REQUESTS_MILLIS = 10_000L
|
||||||
|
private const val MIN_DELAY_TO_SEND_STOP_TYPING_REQUEST_WHEN_NO_USER_ACTIVITY_MILLIS = 10_000L
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.session.room.typing
|
||||||
|
|
||||||
|
import im.vector.matrix.android.internal.di.UserId
|
||||||
|
import im.vector.matrix.android.internal.network.executeRequest
|
||||||
|
import im.vector.matrix.android.internal.session.room.RoomAPI
|
||||||
|
import im.vector.matrix.android.internal.task.Task
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal interface SendTypingTask : Task<SendTypingTask.Params, Unit> {
|
||||||
|
|
||||||
|
data class Params(
|
||||||
|
val roomId: String,
|
||||||
|
val isTyping: Boolean,
|
||||||
|
val typingTimeoutMillis: Int? = 30_000,
|
||||||
|
// Optional delay before sending the request to the homeserver
|
||||||
|
val delay: Long? = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultSendTypingTask @Inject constructor(
|
||||||
|
private val roomAPI: RoomAPI,
|
||||||
|
@UserId private val userId: String,
|
||||||
|
private val eventBus: EventBus
|
||||||
|
) : SendTypingTask {
|
||||||
|
|
||||||
|
override suspend fun execute(params: SendTypingTask.Params) {
|
||||||
|
delay(params.delay ?: -1)
|
||||||
|
|
||||||
|
executeRequest<Unit>(eventBus) {
|
||||||
|
apiCall = roomAPI.sendTypingState(
|
||||||
|
params.roomId,
|
||||||
|
userId,
|
||||||
|
TypingBody(params.isTyping, params.typingTimeoutMillis?.takeIf { params.isTyping })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.session.room.typing
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class TypingBody(
|
||||||
|
// Required. Whether the user is typing or not. If false, the timeout key can be omitted.
|
||||||
|
@Json(name = "typing")
|
||||||
|
val typing: Boolean,
|
||||||
|
// The length of time in milliseconds to mark this user as typing.
|
||||||
|
@Json(name = "timeout")
|
||||||
|
val timeout: Int?
|
||||||
|
)
|
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.session.room.typing
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class TypingEventContent(
|
||||||
|
@Json(name = "user_ids")
|
||||||
|
val typingUserIds: List<String> = emptyList()
|
||||||
|
)
|
@ -37,6 +37,7 @@ import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
|||||||
import im.vector.matrix.android.internal.session.room.membership.RoomMemberEventHandler
|
import im.vector.matrix.android.internal.session.room.membership.RoomMemberEventHandler
|
||||||
import im.vector.matrix.android.internal.session.room.read.FullyReadContent
|
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.timeline.PaginationDirection
|
||||||
|
import im.vector.matrix.android.internal.session.room.typing.TypingEventContent
|
||||||
import im.vector.matrix.android.internal.session.sync.model.*
|
import im.vector.matrix.android.internal.session.sync.model.*
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.kotlin.createObject
|
import io.realm.kotlin.createObject
|
||||||
@ -97,11 +98,12 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||||||
isInitialSync: Boolean): RoomEntity {
|
isInitialSync: Boolean): RoomEntity {
|
||||||
Timber.v("Handle join sync for room $roomId")
|
Timber.v("Handle join sync for room $roomId")
|
||||||
|
|
||||||
if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) {
|
var ephemeralResult: EphemeralResult? = null
|
||||||
handleEphemeral(realm, roomId, roomSync.ephemeral, isInitialSync)
|
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)
|
handleRoomAccountDataEvents(realm, roomId, roomSync.accountData)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +116,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||||||
|
|
||||||
// State event
|
// 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()
|
val minStateIndex = roomEntity.untimelinedStateEvents.where().min(EventEntityFields.STATE_INDEX)?.toInt()
|
||||||
?: Int.MIN_VALUE
|
?: Int.MIN_VALUE
|
||||||
val untimelinedStateIndex = minStateIndex + 1
|
val untimelinedStateIndex = minStateIndex + 1
|
||||||
@ -125,7 +127,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||||||
roomMemberEventHandler.handle(realm, roomId, event)
|
roomMemberEventHandler.handle(realm, roomId, event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (roomSync.timeline != null && roomSync.timeline.events.isNotEmpty()) {
|
if (roomSync.timeline?.events?.isNotEmpty() == true) {
|
||||||
val chunkEntity = handleTimelineEvents(
|
val chunkEntity = handleTimelineEvents(
|
||||||
realm,
|
realm,
|
||||||
roomEntity,
|
roomEntity,
|
||||||
@ -141,7 +143,14 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||||||
it.type == EventType.STATE_ROOM_MEMBER
|
it.type == EventType.STATE_ROOM_MEMBER
|
||||||
} != null
|
} != 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
|
return roomEntity
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,17 +224,34 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||||||
return chunkEntity
|
return chunkEntity
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
data class EphemeralResult(
|
||||||
|
val typingUserIds: List<String> = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
private fun handleEphemeral(realm: Realm,
|
private fun handleEphemeral(realm: Realm,
|
||||||
roomId: String,
|
roomId: String,
|
||||||
ephemeral: RoomSyncEphemeral,
|
ephemeral: RoomSyncEphemeral,
|
||||||
isInitialSync: Boolean) {
|
isInitialSync: Boolean): EphemeralResult {
|
||||||
|
var result = EphemeralResult()
|
||||||
for (event in ephemeral.events) {
|
for (event in ephemeral.events) {
|
||||||
if (event.type != EventType.RECEIPT) continue
|
when (event.type) {
|
||||||
val readReceiptContent = event.content as? ReadReceiptContent ?: continue
|
EventType.RECEIPT -> {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
(event.content as? ReadReceiptContent)?.let { readReceiptContent ->
|
||||||
readReceiptHandler.handle(realm, roomId, readReceiptContent, isInitialSync)
|
readReceiptHandler.handle(realm, roomId, readReceiptContent, isInitialSync)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
EventType.TYPING -> {
|
||||||
|
event.content.toModel<TypingEventContent>()?.let { typingEventContent ->
|
||||||
|
result = result.copy(typingUserIds = typingEventContent.typingUserIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> Timber.w("Ephemeral event type '${event.type}' not yet supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) {
|
private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) {
|
||||||
for (event in accountData.events) {
|
for (event in accountData.events) {
|
||||||
|
@ -22,9 +22,11 @@ import im.vector.matrix.android.api.util.toMatrixItem
|
|||||||
import im.vector.riotx.core.epoxy.zeroItem
|
import im.vector.riotx.core.epoxy.zeroItem
|
||||||
import im.vector.riotx.core.utils.DebouncedClickListener
|
import im.vector.riotx.core.utils.DebouncedClickListener
|
||||||
import im.vector.riotx.features.home.AvatarRenderer
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
|
import im.vector.riotx.features.home.room.typing.TypingHelper
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class BreadcrumbsController @Inject constructor(
|
class BreadcrumbsController @Inject constructor(
|
||||||
|
private val typingHelper: TypingHelper,
|
||||||
private val avatarRenderer: AvatarRenderer
|
private val avatarRenderer: AvatarRenderer
|
||||||
) : EpoxyController() {
|
) : EpoxyController() {
|
||||||
|
|
||||||
@ -62,6 +64,7 @@ class BreadcrumbsController @Inject constructor(
|
|||||||
unreadNotificationCount(it.notificationCount)
|
unreadNotificationCount(it.notificationCount)
|
||||||
showHighlighted(it.highlightCount > 0)
|
showHighlighted(it.highlightCount > 0)
|
||||||
hasUnreadMessage(it.hasUnreadMessages)
|
hasUnreadMessage(it.hasUnreadMessages)
|
||||||
|
hasTypingUsers(typingHelper.excludeCurrentUser(it.typingRoomMemberIds).isNotEmpty())
|
||||||
hasDraft(it.userDrafts.isNotEmpty())
|
hasDraft(it.userDrafts.isNotEmpty())
|
||||||
itemClickListener(
|
itemClickListener(
|
||||||
DebouncedClickListener(View.OnClickListener { _ ->
|
DebouncedClickListener(View.OnClickListener { _ ->
|
||||||
|
@ -37,6 +37,7 @@ abstract class BreadcrumbsItem : VectorEpoxyModel<BreadcrumbsItem.Holder>() {
|
|||||||
@EpoxyAttribute var unreadNotificationCount: Int = 0
|
@EpoxyAttribute var unreadNotificationCount: Int = 0
|
||||||
@EpoxyAttribute var showHighlighted: Boolean = false
|
@EpoxyAttribute var showHighlighted: Boolean = false
|
||||||
@EpoxyAttribute var hasUnreadMessage: Boolean = false
|
@EpoxyAttribute var hasUnreadMessage: Boolean = false
|
||||||
|
@EpoxyAttribute var hasTypingUsers: Boolean = false
|
||||||
@EpoxyAttribute var hasDraft: Boolean = false
|
@EpoxyAttribute var hasDraft: Boolean = false
|
||||||
@EpoxyAttribute var itemClickListener: View.OnClickListener? = null
|
@EpoxyAttribute var itemClickListener: View.OnClickListener? = null
|
||||||
|
|
||||||
@ -44,6 +45,7 @@ abstract class BreadcrumbsItem : VectorEpoxyModel<BreadcrumbsItem.Holder>() {
|
|||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
holder.rootView.setOnClickListener(itemClickListener)
|
holder.rootView.setOnClickListener(itemClickListener)
|
||||||
holder.unreadIndentIndicator.isVisible = hasUnreadMessage
|
holder.unreadIndentIndicator.isVisible = hasUnreadMessage
|
||||||
|
holder.typingIndicator.isVisible = hasTypingUsers
|
||||||
avatarRenderer.render(matrixItem, holder.avatarImageView)
|
avatarRenderer.render(matrixItem, holder.avatarImageView)
|
||||||
holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted))
|
holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted))
|
||||||
holder.draftIndentIndicator.isVisible = hasDraft
|
holder.draftIndentIndicator.isVisible = hasDraft
|
||||||
@ -53,6 +55,7 @@ abstract class BreadcrumbsItem : VectorEpoxyModel<BreadcrumbsItem.Holder>() {
|
|||||||
val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.breadcrumbsUnreadCounterBadgeView)
|
val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.breadcrumbsUnreadCounterBadgeView)
|
||||||
val unreadIndentIndicator by bind<View>(R.id.breadcrumbsUnreadIndicator)
|
val unreadIndentIndicator by bind<View>(R.id.breadcrumbsUnreadIndicator)
|
||||||
val draftIndentIndicator by bind<View>(R.id.breadcrumbsDraftBadge)
|
val draftIndentIndicator by bind<View>(R.id.breadcrumbsDraftBadge)
|
||||||
|
val typingIndicator by bind<View>(R.id.breadcrumbsTypingView)
|
||||||
val avatarImageView by bind<ImageView>(R.id.breadcrumbsImageView)
|
val avatarImageView by bind<ImageView>(R.id.breadcrumbsImageView)
|
||||||
val rootView by bind<ViewGroup>(R.id.breadcrumbsRoot)
|
val rootView by bind<ViewGroup>(R.id.breadcrumbsRoot)
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
|||||||
import im.vector.riotx.core.platform.VectorViewModelAction
|
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
sealed class RoomDetailAction : VectorViewModelAction {
|
sealed class RoomDetailAction : VectorViewModelAction {
|
||||||
|
data class UserIsTyping(val isTyping: Boolean) : RoomDetailAction()
|
||||||
data class SaveDraft(val draft: String) : RoomDetailAction()
|
data class SaveDraft(val draft: String) : RoomDetailAction()
|
||||||
data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : RoomDetailAction()
|
data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : RoomDetailAction()
|
||||||
data class SendMedia(val attachments: List<ContentAttachmentData>) : RoomDetailAction()
|
data class SendMedia(val attachments: List<ContentAttachmentData>) : RoomDetailAction()
|
||||||
|
@ -20,6 +20,7 @@ import android.annotation.SuppressLint
|
|||||||
import android.app.Activity.RESULT_OK
|
import android.app.Activity.RESULT_OK
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Typeface
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@ -50,6 +51,7 @@ import com.github.piasy.biv.BigImageViewer
|
|||||||
import com.github.piasy.biv.loader.ImageLoader
|
import com.github.piasy.biv.loader.ImageLoader
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
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.permalinks.PermalinkFactory
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
@ -100,6 +102,7 @@ import im.vector.riotx.features.permalink.PermalinkHandler
|
|||||||
import im.vector.riotx.features.reactions.EmojiReactionPickerActivity
|
import im.vector.riotx.features.reactions.EmojiReactionPickerActivity
|
||||||
import im.vector.riotx.features.settings.VectorPreferences
|
import im.vector.riotx.features.settings.VectorPreferences
|
||||||
import im.vector.riotx.features.share.SharedData
|
import im.vector.riotx.features.share.SharedData
|
||||||
|
import im.vector.riotx.features.themes.ThemeUtils
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.schedulers.Schedulers
|
import io.reactivex.schedulers.Schedulers
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
@ -109,6 +112,7 @@ import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
|
|||||||
import org.commonmark.parser.Parser
|
import org.commonmark.parser.Parser
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@ -539,6 +543,9 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
|
|
||||||
private fun setupComposer() {
|
private fun setupComposer() {
|
||||||
autoCompleter.setup(composerLayout.composerEditText)
|
autoCompleter.setup(composerLayout.composerEditText)
|
||||||
|
|
||||||
|
observerUserTyping()
|
||||||
|
|
||||||
composerLayout.callback = object : TextComposerView.Callback {
|
composerLayout.callback = object : TextComposerView.Callback {
|
||||||
override fun onAddAttachment() {
|
override fun onAddAttachment() {
|
||||||
if (!::attachmentTypeSelector.isInitialized) {
|
if (!::attachmentTypeSelector.isInitialized) {
|
||||||
@ -575,6 +582,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 {
|
private fun sendUri(uri: Uri): Boolean {
|
||||||
val shareIntent = Intent(Intent.ACTION_SEND, uri)
|
val shareIntent = Intent(Intent.ACTION_SEND, uri)
|
||||||
val isHandled = attachmentsHelper.handleShareIntent(shareIntent)
|
val isHandled = attachmentsHelper.handleShareIntent(shareIntent)
|
||||||
@ -627,13 +646,29 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
} else {
|
} else {
|
||||||
roomToolbarTitleView.text = it.displayName
|
roomToolbarTitleView.text = it.displayName
|
||||||
avatarRenderer.render(it.toMatrixItem(), roomToolbarAvatarImageView)
|
avatarRenderer.render(it.toMatrixItem(), roomToolbarAvatarImageView)
|
||||||
roomToolbarSubtitleView.setTextOrHide(it.topic)
|
|
||||||
|
renderSubTitle(state.typingMessage, it.topic)
|
||||||
}
|
}
|
||||||
jumpToBottomView.count = it.notificationCount
|
jumpToBottomView.count = it.notificationCount
|
||||||
jumpToBottomView.drawBadge = it.hasUnreadMessages
|
jumpToBottomView.drawBadge = it.hasUnreadMessages
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun renderSubTitle(typingMessage: String?, topic: String) {
|
||||||
|
// TODO Temporary place to put typing data
|
||||||
|
roomToolbarSubtitleView.let {
|
||||||
|
it.setTextOrHide(typingMessage ?: topic)
|
||||||
|
|
||||||
|
if (typingMessage == null) {
|
||||||
|
it.setTextColor(ThemeUtils.getColor(requireContext(), R.attr.vctr_toolbar_secondary_text_color))
|
||||||
|
it.setTypeface(null, Typeface.NORMAL)
|
||||||
|
} else {
|
||||||
|
it.setTextColor(ContextCompat.getColor(requireContext(), R.color.riotx_accent))
|
||||||
|
it.setTypeface(null, Typeface.BOLD)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun renderTombstoneEventHandling(async: Async<String>) {
|
private fun renderTombstoneEventHandling(async: Async<String>) {
|
||||||
when (async) {
|
when (async) {
|
||||||
is Loading -> {
|
is Loading -> {
|
||||||
|
@ -20,12 +20,7 @@ import android.net.Uri
|
|||||||
import androidx.annotation.IdRes
|
import androidx.annotation.IdRes
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.airbnb.mvrx.Async
|
import com.airbnb.mvrx.*
|
||||||
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.jakewharton.rxrelay2.BehaviorRelay
|
import com.jakewharton.rxrelay2.BehaviorRelay
|
||||||
import com.jakewharton.rxrelay2.PublishRelay
|
import com.jakewharton.rxrelay2.PublishRelay
|
||||||
import com.squareup.inject.assisted.Assisted
|
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.CommandParser
|
||||||
import im.vector.riotx.features.command.ParsedCommand
|
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.detail.timeline.helper.TimelineDisplayableEvents
|
||||||
|
import im.vector.riotx.features.home.room.typing.TypingHelper
|
||||||
import im.vector.riotx.features.settings.VectorPreferences
|
import im.vector.riotx.features.settings.VectorPreferences
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.functions.BiFunction
|
import io.reactivex.functions.BiFunction
|
||||||
@ -83,6 +79,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
userPreferencesProvider: UserPreferencesProvider,
|
userPreferencesProvider: UserPreferencesProvider,
|
||||||
private val vectorPreferences: VectorPreferences,
|
private val vectorPreferences: VectorPreferences,
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
|
private val typingHelper: TypingHelper,
|
||||||
private val session: Session
|
private val session: Session
|
||||||
) : VectorViewModel<RoomDetailViewState, RoomDetailAction>(initialState), Timeline.Listener {
|
) : VectorViewModel<RoomDetailViewState, RoomDetailAction>(initialState), Timeline.Listener {
|
||||||
|
|
||||||
@ -159,6 +156,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
|
|
||||||
override fun handle(action: RoomDetailAction) {
|
override fun handle(action: RoomDetailAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
|
is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action)
|
||||||
is RoomDetailAction.SaveDraft -> handleSaveDraft(action)
|
is RoomDetailAction.SaveDraft -> handleSaveDraft(action)
|
||||||
is RoomDetailAction.SendMessage -> handleSendMessage(action)
|
is RoomDetailAction.SendMessage -> handleSendMessage(action)
|
||||||
is RoomDetailAction.SendMedia -> handleSendMedia(action)
|
is RoomDetailAction.SendMedia -> handleSendMedia(action)
|
||||||
@ -259,9 +257,18 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
.disposeOnClear()
|
.disposeOnClear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleUserIsTyping(action: RoomDetailAction.UserIsTyping) {
|
||||||
|
if (vectorPreferences.sendTypingNotifs()) {
|
||||||
|
if (action.isTyping) {
|
||||||
|
room.userIsTyping()
|
||||||
|
} else {
|
||||||
|
room.userStopsTyping()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleTombstoneEvent(action: RoomDetailAction.HandleTombstoneEvent) {
|
private fun handleTombstoneEvent(action: RoomDetailAction.HandleTombstoneEvent) {
|
||||||
val tombstoneContent = action.event.getClearContent().toModel<RoomTombstoneContent>()
|
val tombstoneContent = action.event.getClearContent().toModel<RoomTombstoneContent>() ?: return
|
||||||
?: return
|
|
||||||
|
|
||||||
val roomId = tombstoneContent.replacementRoom ?: ""
|
val roomId = tombstoneContent.replacementRoom ?: ""
|
||||||
val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN
|
val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN
|
||||||
@ -730,8 +737,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
.buffer(1, TimeUnit.SECONDS)
|
.buffer(1, TimeUnit.SECONDS)
|
||||||
.filter { it.isNotEmpty() }
|
.filter { it.isNotEmpty() }
|
||||||
.subscribeBy(onNext = { actions ->
|
.subscribeBy(onNext = { actions ->
|
||||||
val bufferedMostRecentDisplayedEvent = actions.maxBy { it.event.displayIndex }?.event
|
val bufferedMostRecentDisplayedEvent = actions.maxBy { it.event.displayIndex }?.event ?: return@subscribeBy
|
||||||
?: return@subscribeBy
|
|
||||||
val globalMostRecentDisplayedEvent = mostRecentDisplayedEvent
|
val globalMostRecentDisplayedEvent = mostRecentDisplayedEvent
|
||||||
if (trackUnreadMessages.get()) {
|
if (trackUnreadMessages.get()) {
|
||||||
if (globalMostRecentDisplayedEvent == null) {
|
if (globalMostRecentDisplayedEvent == null) {
|
||||||
@ -794,7 +800,14 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
room.rx().liveRoomSummary()
|
room.rx().liveRoomSummary()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.execute { async ->
|
.execute { async ->
|
||||||
copy(asyncRoomSummary = async)
|
val typingRoomMembers =
|
||||||
|
typingHelper.toTypingRoomMembers(async.invoke()?.typingRoomMemberIds.orEmpty(), room)
|
||||||
|
|
||||||
|
copy(
|
||||||
|
asyncRoomSummary = async,
|
||||||
|
typingRoomMembers = typingRoomMembers,
|
||||||
|
typingMessage = typingHelper.toTypingMessage(typingRoomMembers)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -881,6 +894,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
timeline.dispose()
|
timeline.dispose()
|
||||||
timeline.removeAllListeners()
|
timeline.removeAllListeners()
|
||||||
|
if (vectorPreferences.sendTypingNotifs()) {
|
||||||
|
room.userStopsTyping()
|
||||||
|
}
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary
|
|||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.api.session.sync.SyncState
|
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.session.user.model.User
|
||||||
|
import im.vector.matrix.android.api.util.MatrixItem
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Describes the current send mode:
|
* Describes the current send mode:
|
||||||
@ -43,7 +44,7 @@ sealed class SendMode(open val text: String) {
|
|||||||
sealed class UnreadState {
|
sealed class UnreadState {
|
||||||
object Unknown : UnreadState()
|
object Unknown : UnreadState()
|
||||||
object HasNoUnread : 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()
|
data class HasUnread(val firstUnreadEventId: String) : UnreadState()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,6 +53,8 @@ data class RoomDetailViewState(
|
|||||||
val eventId: String?,
|
val eventId: String?,
|
||||||
val asyncInviter: Async<User> = Uninitialized,
|
val asyncInviter: Async<User> = Uninitialized,
|
||||||
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
||||||
|
val typingRoomMembers: List<MatrixItem.UserItem>? = null,
|
||||||
|
val typingMessage: String? = null,
|
||||||
val sendMode: SendMode = SendMode.REGULAR(""),
|
val sendMode: SendMode = SendMode.REGULAR(""),
|
||||||
val tombstoneEvent: Event? = null,
|
val tombstoneEvent: Event? = null,
|
||||||
val tombstoneEventHandling: Async<String> = Uninitialized,
|
val tombstoneEventHandling: Async<String> = Uninitialized,
|
||||||
|
@ -20,6 +20,7 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
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.R
|
||||||
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
||||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||||
|
import im.vector.riotx.core.extensions.setTextOrHide
|
||||||
import im.vector.riotx.features.home.AvatarRenderer
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
|
|
||||||
@EpoxyModelClass(layout = R.layout.item_room)
|
@EpoxyModelClass(layout = R.layout.item_room)
|
||||||
@ -36,6 +38,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
|
|||||||
@EpoxyAttribute lateinit var matrixItem: MatrixItem
|
@EpoxyAttribute lateinit var matrixItem: MatrixItem
|
||||||
@EpoxyAttribute lateinit var lastFormattedEvent: CharSequence
|
@EpoxyAttribute lateinit var lastFormattedEvent: CharSequence
|
||||||
@EpoxyAttribute lateinit var lastEventTime: CharSequence
|
@EpoxyAttribute lateinit var lastEventTime: CharSequence
|
||||||
|
@EpoxyAttribute var typingString: CharSequence? = null
|
||||||
@EpoxyAttribute var unreadNotificationCount: Int = 0
|
@EpoxyAttribute var unreadNotificationCount: Int = 0
|
||||||
@EpoxyAttribute var hasUnreadMessage: Boolean = false
|
@EpoxyAttribute var hasUnreadMessage: Boolean = false
|
||||||
@EpoxyAttribute var hasDraft: Boolean = false
|
@EpoxyAttribute var hasDraft: Boolean = false
|
||||||
@ -50,6 +53,8 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
|
|||||||
holder.titleView.text = matrixItem.getBestName()
|
holder.titleView.text = matrixItem.getBestName()
|
||||||
holder.lastEventTimeView.text = lastEventTime
|
holder.lastEventTimeView.text = lastEventTime
|
||||||
holder.lastEventView.text = lastFormattedEvent
|
holder.lastEventView.text = lastFormattedEvent
|
||||||
|
holder.typingView.setTextOrHide(typingString)
|
||||||
|
holder.lastEventView.isInvisible = holder.typingView.isVisible
|
||||||
holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted))
|
holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted))
|
||||||
holder.unreadIndentIndicator.isVisible = hasUnreadMessage
|
holder.unreadIndentIndicator.isVisible = hasUnreadMessage
|
||||||
holder.draftView.isVisible = hasDraft
|
holder.draftView.isVisible = hasDraft
|
||||||
@ -61,6 +66,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
|
|||||||
val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.roomUnreadCounterBadgeView)
|
val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.roomUnreadCounterBadgeView)
|
||||||
val unreadIndentIndicator by bind<View>(R.id.roomUnreadIndicator)
|
val unreadIndentIndicator by bind<View>(R.id.roomUnreadIndicator)
|
||||||
val lastEventView by bind<TextView>(R.id.roomLastEventView)
|
val lastEventView by bind<TextView>(R.id.roomLastEventView)
|
||||||
|
val typingView by bind<TextView>(R.id.roomTypingView)
|
||||||
val draftView by bind<ImageView>(R.id.roomDraftBadge)
|
val draftView by bind<ImageView>(R.id.roomDraftBadge)
|
||||||
val lastEventTimeView by bind<TextView>(R.id.roomLastEventTimeView)
|
val lastEventTimeView by bind<TextView>(R.id.roomLastEventTimeView)
|
||||||
val avatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
|
val avatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
package im.vector.riotx.features.home.room.list
|
package im.vector.riotx.features.home.room.list
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
import im.vector.matrix.android.api.session.room.model.Membership
|
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.session.room.model.RoomSummary
|
||||||
@ -32,6 +33,7 @@ import im.vector.riotx.core.resources.StringProvider
|
|||||||
import im.vector.riotx.core.utils.DebouncedClickListener
|
import im.vector.riotx.core.utils.DebouncedClickListener
|
||||||
import im.vector.riotx.features.home.AvatarRenderer
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter
|
import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter
|
||||||
|
import im.vector.riotx.features.home.room.typing.TypingHelper
|
||||||
import me.gujun.android.span.span
|
import me.gujun.android.span.span
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -39,6 +41,8 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte
|
|||||||
private val dateFormatter: VectorDateFormatter,
|
private val dateFormatter: VectorDateFormatter,
|
||||||
private val colorProvider: ColorProvider,
|
private val colorProvider: ColorProvider,
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
|
private val typingHelper: TypingHelper,
|
||||||
|
private val session: Session,
|
||||||
private val avatarRenderer: AvatarRenderer) {
|
private val avatarRenderer: AvatarRenderer) {
|
||||||
|
|
||||||
fun create(roomSummary: RoomSummary,
|
fun create(roomSummary: RoomSummary,
|
||||||
@ -121,11 +125,22 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte
|
|||||||
dateFormatter.formatMessageDay(date)
|
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_()
|
return RoomSummaryItem_()
|
||||||
.id(roomSummary.roomId)
|
.id(roomSummary.roomId)
|
||||||
.avatarRenderer(avatarRenderer)
|
.avatarRenderer(avatarRenderer)
|
||||||
.matrixItem(roomSummary.toMatrixItem())
|
.matrixItem(roomSummary.toMatrixItem())
|
||||||
.lastEventTime(latestEventTime)
|
.lastEventTime(latestEventTime)
|
||||||
|
.typingString(typingString)
|
||||||
.lastFormattedEvent(latestFormattedEvent)
|
.lastFormattedEvent(latestFormattedEvent)
|
||||||
.showHighlighted(showHighlighted)
|
.showHighlighted(showHighlighted)
|
||||||
.unreadNotificationCount(unreadCount)
|
.unreadNotificationCount(unreadCount)
|
||||||
|
@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.home.room.typing
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
import im.vector.matrix.android.api.session.room.members.MembershipService
|
||||||
|
import im.vector.matrix.android.api.util.MatrixItem
|
||||||
|
import im.vector.matrix.android.api.util.toMatrixItem
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class TypingHelper @Inject constructor(
|
||||||
|
private val session: Session,
|
||||||
|
private val stringProvider: StringProvider
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Exclude current user from the list of typing users
|
||||||
|
*/
|
||||||
|
fun excludeCurrentUser(
|
||||||
|
typingUserIds: List<String>
|
||||||
|
): List<String> {
|
||||||
|
return typingUserIds
|
||||||
|
.filter { it != session.myUserId }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a list of userId to a list of maximum 3 UserItems
|
||||||
|
*/
|
||||||
|
fun toTypingRoomMembers(
|
||||||
|
typingUserIds: List<String>,
|
||||||
|
membershipService: MembershipService?
|
||||||
|
): List<MatrixItem.UserItem> {
|
||||||
|
return excludeCurrentUser(typingUserIds)
|
||||||
|
.take(3)
|
||||||
|
.mapNotNull { membershipService?.getRoomMember(it) }
|
||||||
|
.map { it.toMatrixItem() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a list of typing UserItems to a human readable String
|
||||||
|
*/
|
||||||
|
fun toTypingMessage(typingUserItems: List<MatrixItem.UserItem>): String? {
|
||||||
|
return when {
|
||||||
|
typingUserItems.isEmpty() ->
|
||||||
|
null
|
||||||
|
typingUserItems.size == 1 ->
|
||||||
|
stringProvider.getString(R.string.room_one_user_is_typing, typingUserItems[0].getBestName())
|
||||||
|
typingUserItems.size == 2 ->
|
||||||
|
stringProvider.getString(R.string.room_two_users_are_typing, typingUserItems[0].getBestName(), typingUserItems[1].getBestName())
|
||||||
|
else ->
|
||||||
|
stringProvider.getString(R.string.room_many_users_are_typing, typingUserItems[0].getBestName(), typingUserItems[1].getBestName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
vector/src/main/res/drawable/bg_breadcrumbs_typing.xml
Normal file
8
vector/src/main/res/drawable/bg_breadcrumbs_typing.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
|
||||||
|
<corners android:radius="40dp" />
|
||||||
|
|
||||||
|
<solid android:color="@color/riotx_accent" />
|
||||||
|
</shape>
|
@ -53,6 +53,23 @@
|
|||||||
tools:text="24"
|
tools:text="24"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/breadcrumbsTypingView"
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:background="@drawable/bg_breadcrumbs_typing"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/ellipsis"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintCircle="@+id/breadcrumbsImageView"
|
||||||
|
app:layout_constraintCircleAngle="135"
|
||||||
|
app:layout_constraintCircleRadius="28dp"
|
||||||
|
tools:ignore="MissingConstraints"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/breadcrumbsDraftBadge"
|
android:id="@+id/breadcrumbsDraftBadge"
|
||||||
android:layout_width="20dp"
|
android:layout_width="20dp"
|
||||||
@ -62,7 +79,7 @@
|
|||||||
android:src="@drawable/ic_edit"
|
android:src="@drawable/ic_edit"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintCircle="@+id/breadcrumbsImageView"
|
app:layout_constraintCircle="@+id/breadcrumbsImageView"
|
||||||
app:layout_constraintCircleAngle="135"
|
app:layout_constraintCircleAngle="225"
|
||||||
app:layout_constraintCircleRadius="28dp"
|
app:layout_constraintCircleRadius="28dp"
|
||||||
tools:ignore="MissingConstraints"
|
tools:ignore="MissingConstraints"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
@ -15,11 +15,11 @@
|
|||||||
android:layout_width="4dp"
|
android:layout_width="4dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:background="?attr/colorAccent"
|
android:background="?attr/colorAccent"
|
||||||
|
android:visibility="gone"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
android:visibility="gone"
|
tools:visibility="visible" />
|
||||||
tools:visibility="visible"/>
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/roomAvatarImageView"
|
android:id="@+id/roomAvatarImageView"
|
||||||
@ -128,6 +128,23 @@
|
|||||||
app:layout_constraintTop_toBottomOf="@+id/roomNameView"
|
app:layout_constraintTop_toBottomOf="@+id/roomNameView"
|
||||||
tools:text="@sample/matrix.json/data/message" />
|
tools:text="@sample/matrix.json/data/message" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/roomTypingView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="3dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:textColor="@color/riotx_accent"
|
||||||
|
android:textSize="15sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/roomNameView"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/roomNameView"
|
||||||
|
tools:text="Alice is typing…" />
|
||||||
|
|
||||||
<!-- Margin bottom does not work, so I use space -->
|
<!-- Margin bottom does not work, so I use space -->
|
||||||
<Space
|
<Space
|
||||||
android:id="@+id/roomLastEventBottomSpace"
|
android:id="@+id/roomLastEventBottomSpace"
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
<string name="debug_screen" translatable="false">Debug screen</string>
|
<string name="debug_screen" translatable="false">Debug screen</string>
|
||||||
|
|
||||||
|
<string name="ellipsis" translatable="false">…</string>
|
||||||
<string name="plus_sign" translatable="false">+</string>
|
<string name="plus_sign" translatable="false">+</string>
|
||||||
<string name="semicolon_sign" translatable="false">:</string>
|
<string name="semicolon_sign" translatable="false">:</string>
|
||||||
|
|
||||||
|
@ -37,8 +37,7 @@
|
|||||||
android:defaultValue="true"
|
android:defaultValue="true"
|
||||||
android:key="SETTINGS_SEND_TYPING_NOTIF_KEY"
|
android:key="SETTINGS_SEND_TYPING_NOTIF_KEY"
|
||||||
android:summary="@string/settings_send_typing_notifs_summary"
|
android:summary="@string/settings_send_typing_notifs_summary"
|
||||||
android:title="@string/settings_send_typing_notifs"
|
android:title="@string/settings_send_typing_notifs" />
|
||||||
app:isPreferenceVisible="@bool/false_not_implemented" />
|
|
||||||
|
|
||||||
<im.vector.riotx.core.preference.VectorSwitchPreference
|
<im.vector.riotx.core.preference.VectorSwitchPreference
|
||||||
android:defaultValue="false"
|
android:defaultValue="false"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user