From 45f6bfc0f0e171fd37722a0365741ce607100007 Mon Sep 17 00:00:00 2001 From: Christoph Klassen Date: Fri, 4 Nov 2022 14:03:55 +0100 Subject: [PATCH 1/3] Added lab feature to pin/unpin messages --- .../src/main/res/values/strings.xml | 8 ++ .../sdk/api/session/events/model/Event.kt | 9 ++ .../sdk/api/session/events/model/EventType.kt | 2 +- .../PinnedEventsStateContent.kt | 28 ++++++ .../api/session/room/state/StateService.kt | 15 +++ .../sdk/api/session/room/timeline/Timeline.kt | 2 +- .../session/room/timeline/TimelineSettings.kt | 9 ++ .../sdk/internal/session/room/RoomAPI.kt | 16 ++- .../room/peeking/ResolveRoomStateTask.kt | 2 +- .../PinnedEventsStateResponse.kt | 28 ++++++ .../session/room/state/DefaultStateService.kt | 20 ++++ .../session/room/timeline/DefaultTimeline.kt | 54 +++++++++- .../room/timeline/DefaultTimelineService.kt | 1 + .../src/main/res/values/config-settings.xml | 1 + vector/src/main/AndroidManifest.xml | 1 + .../home/room/detail/RoomDetailAction.kt | 2 + .../home/room/detail/RoomDetailViewState.kt | 4 + .../home/room/detail/TimelineFragment.kt | 65 +++++++++++- .../home/room/detail/TimelineViewModel.kt | 60 ++++++++++- .../room/detail/arguments/TimelineArgs.kt | 2 + .../timeline/action/EventSharedAction.kt | 9 ++ .../timeline/action/MessageActionState.kt | 9 +- .../action/MessageActionsBottomSheet.kt | 5 +- .../action/MessageActionsViewModel.kt | 20 +++- .../action/TimelineEventFragmentArgs.kt | 3 +- .../timeline/factory/TimelineItemFactory.kt | 1 + .../timeline/format/NoticeEventFormatter.kt | 18 ++++ .../helper/TimelineDisplayableEvents.kt | 1 + .../detail/timeline/merged/MergedTimelines.kt | 2 +- .../pinnedmessages/PinnedMessagesActivity.kt | 99 +++++++++++++++++++ .../arguments/PinnedMessagesTimelineArgs.kt | 30 ++++++ .../features/navigation/DefaultNavigator.kt | 11 +++ .../app/features/navigation/Navigator.kt | 3 + .../features/settings/VectorPreferences.kt | 6 ++ .../res/drawable/ic_open_pinned_messages.xml | 9 ++ .../src/main/res/drawable/ic_pin_message.xml | 9 ++ .../main/res/drawable/ic_unpin_message.xml | 9 ++ .../res/layout/activity_pinned_messages.xml | 24 +++++ vector/src/main/res/menu/menu_timeline.xml | 10 ++ .../src/main/res/xml/vector_settings_labs.xml | 5 + 40 files changed, 590 insertions(+), 22 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/pinnedmessages/PinnedEventsStateContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/pinnedmessages/PinnedEventsStateResponse.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/pinnedmessages/PinnedMessagesActivity.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/pinnedmessages/arguments/PinnedMessagesTimelineArgs.kt create mode 100644 vector/src/main/res/drawable/ic_open_pinned_messages.xml create mode 100644 vector/src/main/res/drawable/ic_pin_message.xml create mode 100644 vector/src/main/res/drawable/ic_unpin_message.xml create mode 100644 vector/src/main/res/layout/activity_pinned_messages.xml diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 0ab1d85f0f..a964cb7d6a 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -373,6 +373,7 @@ Are you sure you want to sign out? Voice Call Video Call + Open Pinned Messages View Threads Mark all as read Quick reply @@ -801,6 +802,12 @@ Threads Beta Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. %sDo you want to enable threads anyway? + + Pin + Unpin + Pinned Messages + %1$s pinned a message. + %1$s unpinned a message. Search @@ -3032,6 +3039,7 @@ Auto Report Decryption Errors. Your system will automatically send logs when an unable to decrypt error occurs + Enable Pinned Messages Enable Thread Messages Note: app will be restarted Show latest user info diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index 40ce6ecb5c..d5c0ad46c5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent +import org.matrix.android.sdk.api.session.room.model.pinnedmessages.PinnedEventsStateContent import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent import org.matrix.android.sdk.api.session.room.model.relation.isReply import org.matrix.android.sdk.api.session.room.model.relation.shouldRenderInThread @@ -447,3 +448,11 @@ fun Event.supportsNotification() = fun Event.isContentReportable() = this.getClearType() in EventType.MESSAGE + EventType.STATE_ROOM_BEACON_INFO.values + +fun Event.getIdsOfPinnedEvents(): MutableList? { + return getClearContent()?.toModel()?.eventIds +} + +fun Event.getPreviousIdsOfPinnedEvents(): MutableList? { + return resolvedPrevContent()?.toModel()?.eventIds +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt index 013b452ced..6cde754d77 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt @@ -45,6 +45,7 @@ object EventType { const val STATE_ROOM_NAME = "m.room.name" const val STATE_ROOM_TOPIC = "m.room.topic" const val STATE_ROOM_AVATAR = "m.room.avatar" + const val STATE_ROOM_PINNED_EVENT = "m.room.pinned_events" const val STATE_ROOM_MEMBER = "m.room.member" const val STATE_ROOM_THIRD_PARTY_INVITE = "m.room.third_party_invite" const val STATE_ROOM_CREATE = "m.room.create" @@ -67,7 +68,6 @@ object EventType { const val STATE_ROOM_CANONICAL_ALIAS = "m.room.canonical_alias" const val STATE_ROOM_HISTORY_VISIBILITY = "m.room.history_visibility" const val STATE_ROOM_RELATED_GROUPS = "m.room.related_groups" - const val STATE_ROOM_PINNED_EVENT = "m.room.pinned_events" const val STATE_ROOM_ENCRYPTION = "m.room.encryption" const val STATE_ROOM_SERVER_ACL = "m.room.server_acl" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/pinnedmessages/PinnedEventsStateContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/pinnedmessages/PinnedEventsStateContent.kt new file mode 100644 index 0000000000..0475ee0fc4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/pinnedmessages/PinnedEventsStateContent.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.pinnedmessages + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing a pinned event content. + */ +@JsonClass(generateAdapter = true) +data class PinnedEventsStateContent( + @Json(name = "pinned") val eventIds: MutableList +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt index 6ca63c2c49..2b008ab732 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt @@ -66,6 +66,11 @@ interface StateService { */ suspend fun deleteAvatar() + /** + * Pin a message of the room. + */ + suspend fun pinMessage(eventIds: MutableList) + /** * Send a state event to the room. * @param eventType The type of event to send. @@ -103,6 +108,16 @@ interface StateService { */ fun getStateEventsLive(eventTypes: Set, stateKey: QueryStateEventValue): LiveData> + /** + * Get state event containing the IDs of pinned events of the room + */ + fun getPinnedEventsState(): Event? + + /** + * Tells if an event is a pinned message + */ + fun isPinned(eventId: String): Boolean? + suspend fun setJoinRulePublic() suspend fun setJoinRuleInviteOnly() suspend fun setJoinRuleRestricted(allowList: List) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt index 9ac33c0545..2e9b87b797 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt @@ -43,7 +43,7 @@ interface Timeline { /** * This must be called before any other method after creating the timeline. It ensures the underlying database is open */ - fun start(rootThreadEventId: String? = null) + fun start(rootThreadEventId: String? = null, rootPinnedMessageEventId: String? = null) /** * This must be called when you don't need the timeline. It ensures the underlying database get closed. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt index fd6732d0d1..3d9f4e3dc7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt @@ -32,6 +32,10 @@ data class TimelineSettings( * The root thread eventId if this is a thread timeline, or null if this is NOT a thread timeline. */ val rootThreadEventId: String? = null, + /** + * The root pinned message eventId if this is a pinned messages timeline, or null if this is NOT a pinned messages timeline. + */ + val rootPinnedMessageEventId: String? = null, /** * If true Sender Info shown in room will get the latest data information (avatar + displayName). */ @@ -42,4 +46,9 @@ data class TimelineSettings( * Returns true if this is a thread timeline or false otherwise. */ fun isThreadTimeline() = rootThreadEventId != null + + /** + * Returns true if this is a pinned messages timeline or false otherwise. + */ + fun isPinnedMessagesTimeline() = rootPinnedMessageEventId != null } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt index 31bed90b62..bf0f482c13 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.session.room.membership.RoomMembersRespon import org.matrix.android.sdk.internal.session.room.membership.admin.UserIdAndReason import org.matrix.android.sdk.internal.session.room.membership.joining.InviteBody import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody +import org.matrix.android.sdk.internal.session.room.pinnedmessages.PinnedEventsStateResponse import org.matrix.android.sdk.internal.session.room.read.ReadBody import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse import org.matrix.android.sdk.internal.session.room.reporting.ReportContentBody @@ -233,11 +234,22 @@ internal interface RoomAPI { ): SendResponse /** - * Get state events of a room + * Get all state events of a room * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-rooms-roomid-state */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state") - suspend fun getRoomState(@Path("roomId") roomId: String): List + suspend fun getAllRoomStates(@Path("roomId") roomId: String): List + + /** + * Get specific state event of a room + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-rooms-roomid-state-eventtype-statekey + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state/{eventType}/{state_key}") + suspend fun getRoomState( + @Path("roomId") roomId: String, + @Path("eventType") eventType: String, + @Path("state_key") stateKey: String + ): PinnedEventsStateResponse /** * Paginate relations for event based in normal topological order. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/ResolveRoomStateTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/ResolveRoomStateTask.kt index 64cbef23ec..24de3e1443 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/ResolveRoomStateTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/ResolveRoomStateTask.kt @@ -36,7 +36,7 @@ internal class DefaultResolveRoomStateTask @Inject constructor( override suspend fun execute(params: ResolveRoomStateTask.Params): List { return executeRequest(globalErrorReceiver) { - roomAPI.getRoomState(params.roomId) + roomAPI.getAllRoomStates(params.roomId) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/pinnedmessages/PinnedEventsStateResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/pinnedmessages/PinnedEventsStateResponse.kt new file mode 100644 index 0000000000..c964f1c769 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/pinnedmessages/PinnedEventsStateResponse.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.pinnedmessages + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class PinnedEventsStateResponse( + /** + * A unique identifier for the event. + */ + @Json(name = "pinned") val pinned: List +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt index ad47b82428..51cf975574 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt @@ -22,8 +22,10 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import org.matrix.android.sdk.api.query.QueryStateEventValue +import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.getIdsOfPinnedEvents import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.room.model.GuestAccess import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent @@ -31,6 +33,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent +import org.matrix.android.sdk.api.session.room.model.pinnedmessages.PinnedEventsStateContent import org.matrix.android.sdk.api.session.room.state.StateService import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.MimeTypes @@ -170,6 +173,23 @@ internal class DefaultStateService @AssistedInject constructor( ) } + override suspend fun pinMessage(eventIds: MutableList) { + sendStateEvent( + eventType = EventType.STATE_ROOM_PINNED_EVENT, + body = PinnedEventsStateContent(eventIds).toContent(), + stateKey = "" + ) + } + + override fun getPinnedEventsState(): Event? { + return getStateEvent(EventType.STATE_ROOM_PINNED_EVENT, QueryStringValue.Equals("")) + } + + override fun isPinned(eventId: String): Boolean? { + val idsOfPinnedEvents: MutableList = getPinnedEventsState()?.getIdsOfPinnedEvents() ?: return null + return idsOfPinnedEvents.contains(eventId) + } + override suspend fun setJoinRulePublic() { updateJoinRule(RoomJoinRules.PUBLIC, null) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 0854cc5cf4..458d2b8af0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -34,6 +34,9 @@ import kotlinx.coroutines.withContext import okhttp3.internal.closeQuietly import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.getIdsOfPinnedEvents import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent @@ -63,7 +66,8 @@ internal class DefaultTimeline( private val settings: TimelineSettings, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val clock: Clock, - stateEventDataSource: StateEventDataSource, + private val stateEventDataSource: StateEventDataSource, + private val timelineEventDataSource: TimelineEventDataSource, paginationTask: PaginationTask, getEventTask: GetContextOfEventTask, fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, @@ -95,6 +99,9 @@ internal class DefaultTimeline( private var isFromThreadTimeline = false private var rootThreadEventId: String? = null + private var isFromPinnedMessagesTimeline = false + private var rootPinnedMessageEventId: String? = null + private val strategyDependencies = LoadTimelineStrategy.Dependencies( timelineSettings = settings, realm = backgroundRealm, @@ -125,7 +132,11 @@ internal class DefaultTimeline( override fun addListener(listener: Timeline.Listener): Boolean { listeners.add(listener) timelineScope.launch { - val snapshot = strategy.buildSnapshot() + val snapshot = if (isFromPinnedMessagesTimeline) { + getPinnedEvents() + } else { + strategy.buildSnapshot() + } withContext(coroutineDispatchers.main) { tryOrNull { listener.onTimelineUpdated(snapshot) } } @@ -141,7 +152,7 @@ internal class DefaultTimeline( listeners.clear() } - override fun start(rootThreadEventId: String?) { + override fun start(rootThreadEventId: String?, rootPinnedMessageEventId: String?) { timelineScope.launch { loadRoomMembersIfNeeded() } @@ -150,6 +161,8 @@ internal class DefaultTimeline( if (isStarted.compareAndSet(false, true)) { isFromThreadTimeline = rootThreadEventId != null this@DefaultTimeline.rootThreadEventId = rootThreadEventId + isFromPinnedMessagesTimeline = rootPinnedMessageEventId != null + this@DefaultTimeline.rootPinnedMessageEventId = rootPinnedMessageEventId // / val realm = Realm.getInstance(realmConfiguration) ensureReadReceiptAreLoaded(realm) @@ -254,7 +267,12 @@ internal class DefaultTimeline( } } Timber.v("$baseLogMessage: result $loadMoreResult") - val hasMoreToLoad = loadMoreResult != LoadMoreResult.REACHED_END + val hasMoreToLoad = if (isFromPinnedMessagesTimeline) { + !areAllPinnedMessagesLoaded() + } else { + loadMoreResult != LoadMoreResult.REACHED_END + } + updateState(direction) { it.copy(loading = false, hasMoreToLoad = hasMoreToLoad) } @@ -334,7 +352,11 @@ internal class DefaultTimeline( } private suspend fun postSnapshot() { - val snapshot = strategy.buildSnapshot() + val snapshot = if (isFromPinnedMessagesTimeline) { + getPinnedEvents() + } else { + strategy.buildSnapshot() + } Timber.v("Post snapshot of ${snapshot.size} events") withContext(coroutineDispatchers.main) { listeners.forEach { @@ -349,6 +371,28 @@ internal class DefaultTimeline( } } + private fun getIdsOfPinnedEvents(): MutableList { + return stateEventDataSource + .getStateEvent(roomId, EventType.STATE_ROOM_PINNED_EVENT, QueryStringValue.Equals("")) + ?.getIdsOfPinnedEvents() ?: mutableListOf("") + } + + private fun getPinnedEvents(): List { + val idsOfPinnedEvents = getIdsOfPinnedEvents() + val pinnedEvents = ArrayList() + for (id in idsOfPinnedEvents) { + val timelineEvent = timelineEventDataSource.getTimelineEvent(roomId, id) + if (timelineEvent != null) { + pinnedEvents.add(timelineEvent) + } + } + return pinnedEvents.reversed() + } + + private fun areAllPinnedMessagesLoaded(): Boolean { + return getIdsOfPinnedEvents().size == getPinnedEvents().size + } + private fun onNewTimelineEvents(eventIds: List) { timelineScope.launch(coroutineDispatchers.main) { listeners.forEach { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index b1a3d51b36..6564231843 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -82,6 +82,7 @@ internal class DefaultTimelineService @AssistedInject constructor( lightweightSettingsStorage = lightweightSettingsStorage, clock = clock, stateEventDataSource = stateEventDataSource, + timelineEventDataSource = timelineEventDataSource, ) } diff --git a/vector-config/src/main/res/values/config-settings.xml b/vector-config/src/main/res/values/config-settings.xml index ad9c16c214..caa60af797 100755 --- a/vector-config/src/main/res/values/config-settings.xml +++ b/vector-config/src/main/res/values/config-settings.xml @@ -39,6 +39,7 @@ true true + false false true false diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 9c8186b2d4..9d62904fc4 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -149,6 +149,7 @@ + , val compressBeforeSending: Boolean) : RoomDetailAction() data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailAction() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index 897594ffad..b60154192e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -71,6 +71,7 @@ data class RoomDetailViewState( val isAllowedToManageWidgets: Boolean = false, val isAllowedToStartWebRTCCall: Boolean = true, val isAllowedToSetupEncryption: Boolean = true, + val rootPinnedMessageEventId: String?, val hasFailedSending: Boolean = false, val jitsiState: JitsiState = JitsiState(), val switchToParentSpace: Boolean = false, @@ -92,6 +93,7 @@ data class RoomDetailViewState( rootThreadEventId = args.threadTimelineArgs?.rootThreadEventId, showKeyboardWhenPresented = args.threadTimelineArgs?.showKeyboard.orFalse(), sharedData = args.sharedData, + rootPinnedMessageEventId = args.pinnedMessagesTimelineArgs?.rootPinnedMessageEventId, ) fun isCallOptionAvailable(): Boolean { @@ -113,5 +115,7 @@ data class RoomDetailViewState( fun isThreadTimeline() = rootThreadEventId != null + fun isPinnedMessagesTimeline() = rootPinnedMessageEventId != null + fun isLocalRoom() = RoomLocalEcho.isLocalEchoId(roomId) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 6ab20275c2..90b9929762 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -159,6 +159,7 @@ import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet import im.vector.app.features.home.room.detail.views.RoomDetailLazyLoadedViews import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet +import im.vector.app.features.home.room.pinnedmessages.arguments.PinnedMessagesTimelineArgs import im.vector.app.features.home.room.threads.ThreadsManager import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.html.EventHtmlRenderer @@ -378,6 +379,11 @@ class TimelineFragment : ) } + if (isPinnedMessagesTimeline()) { + views.composerContainer.isVisible = false + views.voiceMessageRecorderContainer.isVisible = false + } + timelineViewModel.observeViewEvents { when (it) { is RoomDetailViewEvents.Failure -> displayErrorMessage(it) @@ -877,6 +883,10 @@ class TimelineFragment : callActionsHandler.onVideoCallClicked() true } + R.id.open_pinned_messages -> { + navigateToPinnedMessages() + true + } R.id.menu_timeline_thread_list -> { navigateToThreadList() true @@ -1106,7 +1116,7 @@ class TimelineFragment : } private fun updateJumpToReadMarkerViewVisibility() { - if (isThreadTimeLine()) return + if (isThreadTimeLine() || isPinnedMessagesTimeline()) return viewLifecycleOwner.lifecycleScope.launchWhenResumed { val state = timelineViewModel.awaitState() val showJumpToUnreadBanner = when (state.unreadState) { @@ -1235,6 +1245,17 @@ class TimelineFragment : } views.includeThreadToolbar.roomToolbarThreadTitleTextView.text = resources.getText(R.string.thread_timeline_title) } + isPinnedMessagesTimeline() -> { + views.includeRoomToolbar.roomToolbarContentView.isVisible = false + views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = true + timelineArgs.pinnedMessagesTimelineArgs?.let { + val matrixItem = MatrixItem.RoomItem(it.roomId, it.displayName, it.avatarUrl) + avatarRenderer.render(matrixItem, views.includeThreadToolbar.roomToolbarThreadImageView) + views.includeThreadToolbar.roomToolbarThreadShieldImageView.render(it.roomEncryptionTrustLevel) + views.includeThreadToolbar.roomToolbarThreadSubtitleTextView.text = it.displayName + } + views.includeThreadToolbar.roomToolbarThreadTitleTextView.text = resources.getText(R.string.pinned_messages_timeline_title) + } else -> { views.includeRoomToolbar.roomToolbarContentView.isVisible = true views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = false @@ -1543,7 +1564,7 @@ class TimelineFragment : this.view?.hideKeyboard() MessageActionsBottomSheet - .newInstance(roomId, informationData, isThreadTimeLine()) + .newInstance(roomId, informationData, isThreadTimeLine(), isPinnedMessagesTimeline()) .show(requireActivity().supportFragmentManager, "MESSAGE_CONTEXTUAL_ACTIONS") return true @@ -1795,6 +1816,15 @@ class TimelineFragment : requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit) } } + is EventSharedAction.PinMessage -> { + timelineViewModel.handle(RoomDetailAction.PinMessage(action.eventId)) + } + is EventSharedAction.UnpinMessage -> { + timelineViewModel.handle(RoomDetailAction.UnpinMessage(action.eventId)) + } + is EventSharedAction.ViewPinnedMessageInRoom -> { + handleViewInRoomAction(action.eventId) + } is EventSharedAction.ReplyInThread -> { if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) { onReplyInThreadClicked(action) @@ -1974,6 +2004,32 @@ class TimelineFragment : } } + /** + * Navigate to pinned messages for the current room using the PinnedMessagesActivity. + */ + private fun navigateToPinnedMessages() = withState(timelineViewModel) { state -> + val pinnedEventId = timelineViewModel.getIdOfLastPinnedEvent() + context?.let { + val pinnedMessagesTimelineArgs = PinnedMessagesTimelineArgs( + roomId = timelineArgs.roomId, + displayName = state.asyncRoomSummary()?.displayName, + roomEncryptionTrustLevel = state.asyncRoomSummary()?.roomEncryptionTrustLevel, + avatarUrl = state.asyncRoomSummary()?.avatarUrl, + rootPinnedMessageEventId = pinnedEventId + ) + navigator.openPinnedMessages(it, pinnedMessagesTimelineArgs) + } + } + + private fun handleViewInRoomAction(eventId: String) { + val newRoom = timelineArgs.copy(threadTimelineArgs = null, pinnedMessagesTimelineArgs = null, eventId = eventId) + context?.let { con -> + val intent = RoomDetailActivity.newIntent(con, newRoom, false) + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK + con.startActivity(intent) + } + } + // VectorInviteView.Callback override fun onAcceptInvite() { timelineViewModel.handle(RoomDetailAction.AcceptInvite) @@ -2027,6 +2083,11 @@ class TimelineFragment : */ private fun isThreadTimeLine(): Boolean = withState(timelineViewModel) { it.isThreadTimeline() } + /** + * Returns true if the current room is a Pinned Messages room, false otherwise. + */ + private fun isPinnedMessagesTimeline(): Boolean = withState(timelineViewModel) { it.isPinnedMessagesTimeline() } + /** * Returns true if the current room is a local room, false otherwise. */ diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 02782783b8..8044845301 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -89,6 +89,7 @@ import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.getIdsOfPinnedEvents import org.matrix.android.sdk.api.session.events.model.LocalEcho import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode @@ -203,10 +204,12 @@ class TimelineViewModel @AssistedInject constructor( } private fun initSafe(room: Room, timeline: Timeline) { - timeline.start(initialState.rootThreadEventId) + timeline.start(initialState.rootThreadEventId, initialState.rootPinnedMessageEventId) timeline.addListener(this) observeMembershipChanges() - observeSummaryState() + if (!initialState.isPinnedMessagesTimeline()) { + observeSummaryState() + } getUnreadState() observeSyncState() observeDataStore() @@ -448,6 +451,8 @@ class TimelineViewModel @AssistedInject constructor( override fun handle(action: RoomDetailAction) { when (action) { + is RoomDetailAction.PinMessage -> handlePinMessage(action) + is RoomDetailAction.UnpinMessage -> handleUnpinMessage(action) is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action) is RoomDetailAction.SendMedia -> handleSendMedia(action) is RoomDetailAction.SendSticker -> handleSendSticker(action) @@ -757,6 +762,14 @@ class TimelineViewModel @AssistedInject constructor( return room?.membershipService()?.getRoomMember(userId) } + fun getIdOfLastPinnedEvent(): String? { + return room + ?.stateService() + ?.getPinnedEventsState() + ?.getIdsOfPinnedEvents() + ?.last() + } + private fun handleComposerFocusChange(action: RoomDetailAction.ComposerFocusChange) { if (room == null) return // Ensure outbound session keys @@ -827,6 +840,7 @@ class TimelineViewModel @AssistedInject constructor( else -> false } } + initialState.isPinnedMessagesTimeline() -> false else -> { when (itemId) { R.id.timeline_setting -> true @@ -837,6 +851,7 @@ class TimelineViewModel @AssistedInject constructor( // Show Join conference button only if there is an active conf id not joined. Otherwise fallback to default video disabled. ^ R.id.join_conference -> !state.isCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined R.id.search -> state.isSearchAvailable() + R.id.open_pinned_messages -> vectorPreferences.arePinnedMessagesEnabled() && areTherePinnedMessages() R.id.menu_timeline_thread_list -> vectorPreferences.areThreadMessagesEnabled() R.id.dev_tools -> vectorPreferences.developerMode() else -> false @@ -1023,6 +1038,47 @@ class TimelineViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.NavigateToEvent(targetEventId)) } + private fun handlePinMessage(action: RoomDetailAction.PinMessage) { + if (room == null) return + val idsOfPinnedMessages = getIdsOfPinnedEvents() + if (idsOfPinnedMessages == null) return + idsOfPinnedMessages.add(action.eventId) + sendPinnedStateEvent(idsOfPinnedMessages, action) + } + + private fun handleUnpinMessage(action: RoomDetailAction.UnpinMessage) { + if (room == null) return + val idsOfPinnedMessages = getIdsOfPinnedEvents() + if (idsOfPinnedMessages == null) return + idsOfPinnedMessages.remove(action.eventId) + sendPinnedStateEvent(idsOfPinnedMessages, action) + } + + private fun getIdsOfPinnedEvents(): MutableList? { + return room + ?.stateService() + ?.getPinnedEventsState() + ?.getIdsOfPinnedEvents() + } + + private fun areTherePinnedMessages(): Boolean { + val idsOfPinnedMessages = getIdsOfPinnedEvents() ?: return false + return idsOfPinnedMessages.isNotEmpty() + } + + private fun sendPinnedStateEvent(eventIds: MutableList, action: RoomDetailAction) { + viewModelScope.launch(Dispatchers.IO) { + try { + room + ?.stateService() + ?.pinMessage(eventIds) + _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) + } catch (failure: Throwable) { + _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure)) + } + } + } + private fun handleResendEvent(action: RoomDetailAction.ResendMessage) { if (room == null) return val targetEventId = action.eventId diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/arguments/TimelineArgs.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/arguments/TimelineArgs.kt index a21567acb1..d71f8ae7a9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/arguments/TimelineArgs.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/arguments/TimelineArgs.kt @@ -17,6 +17,7 @@ package im.vector.app.features.home.room.detail.arguments import android.os.Parcelable +import im.vector.app.features.home.room.pinnedmessages.arguments.PinnedMessagesTimelineArgs import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.share.SharedData import kotlinx.parcelize.Parcelize @@ -28,6 +29,7 @@ data class TimelineArgs( val sharedData: SharedData? = null, val openShareSpaceForId: String? = null, val threadTimelineArgs: ThreadTimelineArgs? = null, + val pinnedMessagesTimelineArgs: PinnedMessagesTimelineArgs? = null, val switchToParentSpace: Boolean = false, val isInviteAlreadyAccepted: Boolean = false ) : Parcelable diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt index 7bf9f536f2..1e7b7c99ef 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt @@ -53,6 +53,15 @@ sealed class EventSharedAction( data class ReplyInThread(val eventId: String, val startsThread: Boolean) : EventSharedAction(R.string.reply_in_thread, R.drawable.ic_reply_in_thread) + data class PinMessage(val eventId: String) : + EventSharedAction(R.string.pinning_message, R.drawable.ic_pin_message) + + data class UnpinMessage(val eventId: String) : + EventSharedAction(R.string.unpinning_message, R.drawable.ic_unpin_message) + + data class ViewPinnedMessageInRoom(val eventId: String) : + EventSharedAction(R.string.view_in_room, R.drawable.ic_threads_view_in_room_24) + object ViewInRoom : EventSharedAction(R.string.view_in_room, R.drawable.ic_threads_view_in_room_24) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionState.kt index f547734651..fa03877219 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionState.kt @@ -35,7 +35,8 @@ data class ToggleState( data class ActionPermissions( val canSendMessage: Boolean = false, val canReact: Boolean = false, - val canRedact: Boolean = false + val canRedact: Boolean = false, + val canPinMessage: Boolean = false ) data class MessageActionState( @@ -50,14 +51,16 @@ data class MessageActionState( val actions: List = emptyList(), val expendedReportContentMenu: Boolean = false, val actionPermissions: ActionPermissions = ActionPermissions(), - val isFromThreadTimeline: Boolean = false + val isFromThreadTimeline: Boolean = false, + val isFromPinnedMessagesTimeline: Boolean = false ) : MavericksState { constructor(args: TimelineEventFragmentArgs) : this( roomId = args.roomId, eventId = args.eventId, informationData = args.informationData, - isFromThreadTimeline = args.isFromThreadTimeline + isFromThreadTimeline = args.isFromThreadTimeline, + isFromPinnedMessagesTimeline = args.isFromPinnedMessagesTimeline ) fun senderName(): String = informationData.memberName?.toString() ?: "" diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt index 53d9e2aa99..c0740f0905 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt @@ -93,14 +93,15 @@ class MessageActionsBottomSheet : } companion object { - fun newInstance(roomId: String, informationData: MessageInformationData, isFromThreadTimeline: Boolean): MessageActionsBottomSheet { + fun newInstance(roomId: String, informationData: MessageInformationData, isFromThreadTimeline: Boolean, isFromPinnedMessagesTimeline: Boolean): MessageActionsBottomSheet { return MessageActionsBottomSheet().apply { setArguments( TimelineEventFragmentArgs( informationData.eventId, roomId, informationData, - isFromThreadTimeline + isFromThreadTimeline, + isFromPinnedMessagesTimeline ) ) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index a6d7e8386f..341fa83987 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -131,7 +131,8 @@ class MessageActionsViewModel @AssistedInject constructor( val canReact = powerLevelsHelper.isUserAllowedToSend(session.myUserId, false, EventType.REACTION) val canRedact = powerLevelsHelper.isUserAbleToRedact(session.myUserId) val canSendMessage = powerLevelsHelper.isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE) - val permissions = ActionPermissions(canSendMessage = canSendMessage, canRedact = canRedact, canReact = canReact) + val canPinMessage = powerLevelsHelper.isUserAllowedToSend(session.myUserId, false, EventType.STATE_ROOM_PINNED_EVENT) + val permissions = ActionPermissions(canSendMessage = canSendMessage, canRedact = canRedact, canReact = canReact, canPinMessage = canPinMessage) setState { copy(actionPermissions = permissions) } @@ -333,6 +334,13 @@ class MessageActionsViewModel @AssistedInject constructor( ) { val eventId = timelineEvent.eventId if (!timelineEvent.root.isRedacted()) { + if (initialState.isFromPinnedMessagesTimeline) { + if (actionPermissions.canPinMessage && vectorPreferences.arePinnedMessagesEnabled()) { + add(EventSharedAction.UnpinMessage(eventId)) + add(EventSharedAction.ViewPinnedMessageInRoom(eventId)) + } + return + } if (canReply(timelineEvent, messageContent, actionPermissions)) { add(EventSharedAction.Reply(eventId)) } @@ -362,6 +370,16 @@ class MessageActionsViewModel @AssistedInject constructor( add(EventSharedAction.AddReaction(eventId)) } + if (actionPermissions.canPinMessage && vectorPreferences.arePinnedMessagesEnabled()) { + val id: String = timelineEvent.root.eventId ?: return + val isPinned: Boolean = room?.stateService()?.isPinned(id) ?: return + if (isPinned) { + add(EventSharedAction.UnpinMessage(eventId)) + } else { + add(EventSharedAction.PinMessage(eventId)) + } + } + if (canViewReactions(timelineEvent)) { add(EventSharedAction.ViewReactions(informationData)) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/TimelineEventFragmentArgs.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/TimelineEventFragmentArgs.kt index 2bd3c54d52..17d55ac8b9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/TimelineEventFragmentArgs.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/TimelineEventFragmentArgs.kt @@ -25,5 +25,6 @@ data class TimelineEventFragmentArgs( val eventId: String, val roomId: String, val informationData: MessageInformationData, - val isFromThreadTimeline: Boolean = false + val isFromThreadTimeline: Boolean = false, + val isFromPinnedMessagesTimeline: Boolean = false ) : Parcelable diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index ae3ea143a7..2207ffc46a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -71,6 +71,7 @@ class TimelineItemFactory @Inject constructor( EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_AVATAR, EventType.STATE_ROOM_MEMBER, + EventType.STATE_ROOM_PINNED_EVENT, EventType.STATE_ROOM_THIRD_PARTY_INVITE, EventType.STATE_ROOM_CANONICAL_ALIAS, EventType.STATE_ROOM_JOIN_RULES, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 3f702ed72d..e2956d9d98 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -28,6 +28,8 @@ import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent +import org.matrix.android.sdk.api.session.events.model.getIdsOfPinnedEvents +import org.matrix.android.sdk.api.session.events.model.getPreviousIdsOfPinnedEvents import org.matrix.android.sdk.api.session.events.model.isThread import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.GuestAccess @@ -86,6 +88,7 @@ class NoticeEventFormatter @Inject constructor( EventType.STATE_ROOM_WIDGET_LEGACY -> formatWidgetEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm) EventType.STATE_ROOM_POWER_LEVELS -> formatRoomPowerLevels(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) + EventType.STATE_ROOM_PINNED_EVENT -> formatPinnedEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.CALL_INVITE, EventType.CALL_CANDIDATES, EventType.CALL_HANGUP, @@ -118,6 +121,19 @@ class NoticeEventFormatter @Inject constructor( } } + private fun formatPinnedEvent(event: Event, disambiguatedDisplayName: String): CharSequence? { + val idsOfPinnedEvents: MutableList = event.getIdsOfPinnedEvents() ?: return null + val previousIdsOfPinnedEvents: MutableList? = event.getPreviousIdsOfPinnedEvents() + // A message was pinned + val pinnedMessageString = if (event.resolvedPrevContent() == null || previousIdsOfPinnedEvents != null && previousIdsOfPinnedEvents.size < idsOfPinnedEvents.size) { + sp.getString(R.string.user_pinned_message, disambiguatedDisplayName) + // A message was unpinned + } else { + sp.getString(R.string.user_unpinned_message, disambiguatedDisplayName) + } + return pinnedMessageString + } + private fun formatRoomPowerLevels(event: Event, disambiguatedDisplayName: String): CharSequence? { val powerLevelsContent: PowerLevelsContent = event.content.toModel() ?: return null val previousPowerLevelsContent: PowerLevelsContent = event.resolvedPrevContent().toModel() ?: return null @@ -178,6 +194,7 @@ class NoticeEventFormatter @Inject constructor( } fun format(event: Event, senderName: String?, isDm: Boolean): CharSequence? { + Timber.v("°°°°°°°°°°°°°°°°°°°format(event: Event, senderName: String?, isDm: Boolean)") return when (val type = event.getClearType()) { EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(event, senderName, isDm) EventType.STATE_ROOM_NAME -> formatRoomNameEvent(event, senderName) @@ -872,6 +889,7 @@ class NoticeEventFormatter @Inject constructor( } fun formatRedactedEvent(event: Event): String { + Timber.v("°°°°°°°formatRedactedEvent°°°°°°") return (event .unsignedData ?.redactedEvent diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index 51e961f247..4fb0a3943d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -38,6 +38,7 @@ object TimelineDisplayableEvents { EventType.STATE_ROOM_HISTORY_VISIBILITY, EventType.STATE_ROOM_SERVER_ACL, EventType.STATE_ROOM_POWER_LEVELS, + EventType.STATE_ROOM_PINNED_EVENT, EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/merged/MergedTimelines.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/merged/MergedTimelines.kt index 58ad08f026..55d82e9e4f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/merged/MergedTimelines.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/merged/MergedTimelines.kt @@ -113,7 +113,7 @@ class MergedTimelines( secondaryTimeline.removeAllListeners() } - override fun start(rootThreadEventId: String?) { + override fun start(rootThreadEventId: String?, rootPinnedMessageEventId: String?) { mainTimeline.start() secondaryTimeline.start() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/pinnedmessages/PinnedMessagesActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/pinnedmessages/PinnedMessagesActivity.kt new file mode 100644 index 0000000000..3c5c305a20 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/pinnedmessages/PinnedMessagesActivity.kt @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.pinnedmessages + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.extensions.replaceFragment +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.databinding.ActivityPinnedMessagesBinding +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.TimelineFragment +import im.vector.app.features.home.room.detail.arguments.TimelineArgs +import im.vector.app.features.home.room.pinnedmessages.arguments.PinnedMessagesTimelineArgs +import im.vector.lib.core.utils.compat.getParcelableCompat +import javax.inject.Inject + +@AndroidEntryPoint +class PinnedMessagesActivity : VectorBaseActivity() { + + @Inject lateinit var avatarRenderer: AvatarRenderer + + override fun getBinding() = ActivityPinnedMessagesBinding.inflate(layoutInflater) + + override fun getCoordinatorLayout() = views.coordinatorLayout + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + initFragment() + } + + private fun initFragment() { + if (isFirstCreation()) { + when (val fragment = fragmentToNavigate()) { + is DisplayFragment.PinnedMessagesTimeLine -> { + initPinnedMessagesTimelineFragment(fragment.pinnedMessagesTimelineArgs) + } + is DisplayFragment.ErrorFragment -> { + finish() + } + } + } + } + + private fun initPinnedMessagesTimelineFragment(pinnedMessagesTimelineArgs: PinnedMessagesTimelineArgs) = + replaceFragment( + views.pinnedMessagesActivityFragmentContainer, + TimelineFragment::class.java, + TimelineArgs( + roomId = pinnedMessagesTimelineArgs.roomId, + pinnedMessagesTimelineArgs = pinnedMessagesTimelineArgs + ) + ) + + /** + * Determine in witch fragment we should navigate. + */ + private fun fragmentToNavigate(): DisplayFragment { + getPinnedMessagesTimelineArgs()?.let { + return DisplayFragment.PinnedMessagesTimeLine(it) + } + return DisplayFragment.ErrorFragment + } + + private fun getPinnedMessagesTimelineArgs(): PinnedMessagesTimelineArgs? = intent?.extras?.getParcelableCompat(PINNED_MESSAGES_TIMELINE_ARGS) + + companion object { + const val PINNED_MESSAGES_TIMELINE_ARGS = "PINNED_MESSAGES_TIMELINE_ARGS" + + fun newIntent( + context: Context, + pinnedMessagesTimelineArgs: PinnedMessagesTimelineArgs?, + ): Intent { + return Intent(context, PinnedMessagesActivity::class.java).apply { + putExtra(PINNED_MESSAGES_TIMELINE_ARGS, pinnedMessagesTimelineArgs) + } + } + } + + sealed class DisplayFragment { + data class PinnedMessagesTimeLine(val pinnedMessagesTimelineArgs: PinnedMessagesTimelineArgs) : DisplayFragment() + object ErrorFragment : DisplayFragment() + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/pinnedmessages/arguments/PinnedMessagesTimelineArgs.kt b/vector/src/main/java/im/vector/app/features/home/room/pinnedmessages/arguments/PinnedMessagesTimelineArgs.kt new file mode 100644 index 0000000000..daf6bb9240 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/pinnedmessages/arguments/PinnedMessagesTimelineArgs.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.pinnedmessages.arguments + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel + +@Parcelize +data class PinnedMessagesTimelineArgs( + val roomId: String, + val displayName: String?, + val avatarUrl: String?, + val roomEncryptionTrustLevel: RoomEncryptionTrustLevel?, + val rootPinnedMessageEventId: String? +) : Parcelable diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 3970af385e..58a28386fd 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -58,6 +58,8 @@ import im.vector.app.features.home.room.detail.arguments.TimelineArgs import im.vector.app.features.home.room.detail.search.SearchActivity import im.vector.app.features.home.room.detail.search.SearchArgs import im.vector.app.features.home.room.filtered.FilteredRoomsActivity +import im.vector.app.features.home.room.pinnedmessages.arguments.PinnedMessagesTimelineArgs +import im.vector.app.features.home.room.pinnedmessages.PinnedMessagesActivity import im.vector.app.features.home.room.threads.ThreadsActivity import im.vector.app.features.home.room.threads.arguments.ThreadListArgs import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs @@ -599,6 +601,15 @@ class DefaultNavigator @Inject constructor( ) } + override fun openPinnedMessages(context: Context, pinnedMessagesTimelineArgs: PinnedMessagesTimelineArgs) { + context.startActivity( + PinnedMessagesActivity.newIntent( + context = context, + pinnedMessagesTimelineArgs = pinnedMessagesTimelineArgs + ) + ) + } + override fun openScreenSharingPermissionDialog( screenCaptureIntent: Intent, activityResultLauncher: ActivityResultLauncher diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index 1d67f883a3..6cba3a298f 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -27,6 +27,7 @@ import androidx.fragment.app.FragmentActivity import im.vector.app.features.analytics.plan.ViewRoom import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.displayname.getBestName +import im.vector.app.features.home.room.pinnedmessages.arguments.PinnedMessagesTimelineArgs import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.location.LocationData import im.vector.app.features.location.LocationSharingMode @@ -198,6 +199,8 @@ interface Navigator { fun openThreadList(context: Context, threadTimelineArgs: ThreadTimelineArgs) + fun openPinnedMessages(context: Context, pinnedMessagesTimelineArgs: PinnedMessagesTimelineArgs) + fun openScreenSharingPermissionDialog( screenCaptureIntent: Intent, activityResultLauncher: ActivityResultLauncher diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index d46b819cce..0278ff91c2 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -234,6 +234,8 @@ class VectorPreferences @Inject constructor( private const val SETTINGS_LABS_ENABLE_ELEMENT_CALL_PERMISSION_SHORTCUTS = "SETTINGS_LABS_ENABLE_ELEMENT_CALL_PERMISSION_SHORTCUTS" + private const val SETTINGS_LABS_ENABLE_PINNED_MESSAGES = "SETTINGS_LABS_ENABLE_PINNED_MESSAGES" + // This key will be used to identify clients with the old thread support enabled io.element.thread const val SETTINGS_LABS_ENABLE_THREAD_MESSAGES_OLD_CLIENTS = "SETTINGS_LABS_ENABLE_THREAD_MESSAGES" @@ -1112,6 +1114,10 @@ class VectorPreferences @Inject constructor( return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_ELEMENT_CALL_PERMISSION_SHORTCUTS, false) } + fun arePinnedMessagesEnabled(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_PINNED_MESSAGES, getDefault(R.bool.settings_labs_pinned_messages_default)) + } + /** * Indicates whether or not thread messages are enabled. */ diff --git a/vector/src/main/res/drawable/ic_open_pinned_messages.xml b/vector/src/main/res/drawable/ic_open_pinned_messages.xml new file mode 100644 index 0000000000..389db91616 --- /dev/null +++ b/vector/src/main/res/drawable/ic_open_pinned_messages.xml @@ -0,0 +1,9 @@ + + + diff --git a/vector/src/main/res/drawable/ic_pin_message.xml b/vector/src/main/res/drawable/ic_pin_message.xml new file mode 100644 index 0000000000..9fc7b8cecc --- /dev/null +++ b/vector/src/main/res/drawable/ic_pin_message.xml @@ -0,0 +1,9 @@ + + + diff --git a/vector/src/main/res/drawable/ic_unpin_message.xml b/vector/src/main/res/drawable/ic_unpin_message.xml new file mode 100644 index 0000000000..0cad148ca7 --- /dev/null +++ b/vector/src/main/res/drawable/ic_unpin_message.xml @@ -0,0 +1,9 @@ + + + diff --git a/vector/src/main/res/layout/activity_pinned_messages.xml b/vector/src/main/res/layout/activity_pinned_messages.xml new file mode 100644 index 0000000000..e7b0ef00c9 --- /dev/null +++ b/vector/src/main/res/layout/activity_pinned_messages.xml @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/menu/menu_timeline.xml b/vector/src/main/res/menu/menu_timeline.xml index 5c35540932..06f6e51693 100644 --- a/vector/src/main/res/menu/menu_timeline.xml +++ b/vector/src/main/res/menu/menu_timeline.xml @@ -40,6 +40,16 @@ app:showAsAction="always" tools:visible="true" /> + + + + + Date: Sat, 10 Dec 2022 20:03:12 +0100 Subject: [PATCH 2/3] added changelog --- changelog.d/7762.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7762.feature diff --git a/changelog.d/7762.feature b/changelog.d/7762.feature new file mode 100644 index 0000000000..48d7fb0d2f --- /dev/null +++ b/changelog.d/7762.feature @@ -0,0 +1 @@ +Added lab feature to pin/unpin messages \ No newline at end of file From dd65c67f1f375c988c143a969f8772590e1be96b Mon Sep 17 00:00:00 2001 From: Christoph Klassen Date: Fri, 16 Dec 2022 09:31:14 +0100 Subject: [PATCH 3/3] edits after first review on Github --- changelog.d/7762.feature | 2 +- gradle.properties | 2 +- .../src/main/res/values/strings.xml | 16 +- .../sdk/api/session/events/model/Event.kt | 4 +- .../PinnedEventsStateContent.kt | 2 +- .../api/session/room/state/StateService.kt | 19 +- .../sdk/api/session/room/timeline/Timeline.kt | 2 +- .../session/room/timeline/TimelineSettings.kt | 6 +- .../sdk/internal/session/room/RoomAPI.kt | 3 +- .../PinnedEventsStateResponse.kt | 28 --- .../session/room/state/DefaultStateService.kt | 29 ++-- .../session/room/timeline/DefaultTimeline.kt | 37 ++-- .../src/main/res/values/config-settings.xml | 2 +- vector/src/main/AndroidManifest.xml | 2 +- .../home/room/detail/RoomDetailAction.kt | 4 +- .../home/room/detail/RoomDetailViewState.kt | 6 +- .../home/room/detail/TimelineFragment.kt | 63 ++++--- .../home/room/detail/TimelineViewModel.kt | 77 ++++---- .../room/detail/arguments/TimelineArgs.kt | 4 +- .../timeline/action/EventSharedAction.kt | 10 +- .../timeline/action/MessageActionState.kt | 6 +- .../action/MessageActionsBottomSheet.kt | 4 +- .../action/MessageActionsViewModel.kt | 164 +++++++++--------- .../action/TimelineEventFragmentArgs.kt | 2 +- .../timeline/format/NoticeEventFormatter.kt | 26 +-- .../detail/timeline/merged/MergedTimelines.kt | 2 +- .../room/pinnedevents/PinnedEventsActivity.kt | 82 +++++++++ .../arguments/PinnedEventsTimelineArgs.kt} | 9 +- .../pinnedmessages/PinnedMessagesActivity.kt | 99 ----------- .../features/navigation/DefaultNavigator.kt | 10 +- .../app/features/navigation/Navigator.kt | 4 +- .../features/settings/VectorPreferences.kt | 6 +- ...messages.xml => ic_open_pinned_events.xml} | 0 vector/src/main/res/drawable/ic_pin_event.xml | 4 + .../src/main/res/drawable/ic_pin_message.xml | 9 - .../src/main/res/drawable/ic_unpin_event.xml | 4 + .../main/res/drawable/ic_unpin_message.xml | 9 - ...essages.xml => activity_pinned_events.xml} | 2 +- vector/src/main/res/menu/menu_timeline.xml | 6 +- .../src/main/res/xml/vector_settings_labs.xml | 6 +- 40 files changed, 356 insertions(+), 416 deletions(-) delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/pinnedmessages/PinnedEventsStateResponse.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/pinnedevents/PinnedEventsActivity.kt rename vector/src/main/java/im/vector/app/features/home/room/{pinnedmessages/arguments/PinnedMessagesTimelineArgs.kt => pinnedevents/arguments/PinnedEventsTimelineArgs.kt} (70%) delete mode 100644 vector/src/main/java/im/vector/app/features/home/room/pinnedmessages/PinnedMessagesActivity.kt rename vector/src/main/res/drawable/{ic_open_pinned_messages.xml => ic_open_pinned_events.xml} (100%) create mode 100644 vector/src/main/res/drawable/ic_pin_event.xml delete mode 100644 vector/src/main/res/drawable/ic_pin_message.xml create mode 100644 vector/src/main/res/drawable/ic_unpin_event.xml delete mode 100644 vector/src/main/res/drawable/ic_unpin_message.xml rename vector/src/main/res/layout/{activity_pinned_messages.xml => activity_pinned_events.xml} (88%) diff --git a/changelog.d/7762.feature b/changelog.d/7762.feature index 48d7fb0d2f..485acf9415 100644 --- a/changelog.d/7762.feature +++ b/changelog.d/7762.feature @@ -1 +1 @@ -Added lab feature to pin/unpin messages \ No newline at end of file +Added lab feature to pin/unpin messages diff --git a/gradle.properties b/gradle.properties index 2c999af35d..4e3c90491f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ # The setting is particularly useful for tweaking memory settings. # Build Time Optimizations -org.gradle.jvmargs=-Xmx4g -Xms512M -XX:MaxPermSize=2048m -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC +org.gradle.jvmargs=-Xmx4g -Xms512M -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC org.gradle.configureondemand=true org.gradle.parallel=true org.gradle.vfs.watch=true diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index a964cb7d6a..59980c0e3e 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -77,6 +77,10 @@ • Servers matching %s are allowed. • Servers matching IP literals are allowed. • Servers matching IP literals are banned. + %1$s pinned a message. + %1$s unpinned a message. + You pinned a message. + You unpinned a message. %s changed the server ACLs for this room. You changed the server ACLs for this room. @@ -373,7 +377,7 @@ Are you sure you want to sign out? Voice Call Video Call - Open Pinned Messages + Open Pinned Messages View Threads Mark all as read Quick reply @@ -803,11 +807,9 @@ Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. %sDo you want to enable threads anyway? - Pin - Unpin - Pinned Messages - %1$s pinned a message. - %1$s unpinned a message. + Pin + Unpin + Pinned Messages Search @@ -3039,7 +3041,7 @@ Auto Report Decryption Errors. Your system will automatically send logs when an unable to decrypt error occurs - Enable Pinned Messages + Enable Pinned Messages Enable Thread Messages Note: app will be restarted Show latest user info diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index d5c0ad46c5..6fe608fd6d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -449,10 +449,10 @@ fun Event.supportsNotification() = fun Event.isContentReportable() = this.getClearType() in EventType.MESSAGE + EventType.STATE_ROOM_BEACON_INFO.values -fun Event.getIdsOfPinnedEvents(): MutableList? { +fun Event.getIdsOfPinnedEvents(): List? { return getClearContent()?.toModel()?.eventIds } -fun Event.getPreviousIdsOfPinnedEvents(): MutableList? { +fun Event.getPreviousIdsOfPinnedEvents(): List? { return resolvedPrevContent()?.toModel()?.eventIds } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/pinnedmessages/PinnedEventsStateContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/pinnedmessages/PinnedEventsStateContent.kt index 0475ee0fc4..646cf62cda 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/pinnedmessages/PinnedEventsStateContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/pinnedmessages/PinnedEventsStateContent.kt @@ -24,5 +24,5 @@ import com.squareup.moshi.JsonClass */ @JsonClass(generateAdapter = true) data class PinnedEventsStateContent( - @Json(name = "pinned") val eventIds: MutableList + @Json(name = "pinned") val eventIds: List ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt index 2b008ab732..851dea8b9f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt @@ -67,9 +67,14 @@ interface StateService { suspend fun deleteAvatar() /** - * Pin a message of the room. + * Pin an event of the room. */ - suspend fun pinMessage(eventIds: MutableList) + suspend fun pinEvent(eventId: String) + + /** + * Unpin an event of the room. + */ + suspend fun unpinEvent(eventId: String) /** * Send a state event to the room. @@ -108,16 +113,6 @@ interface StateService { */ fun getStateEventsLive(eventTypes: Set, stateKey: QueryStateEventValue): LiveData> - /** - * Get state event containing the IDs of pinned events of the room - */ - fun getPinnedEventsState(): Event? - - /** - * Tells if an event is a pinned message - */ - fun isPinned(eventId: String): Boolean? - suspend fun setJoinRulePublic() suspend fun setJoinRuleInviteOnly() suspend fun setJoinRuleRestricted(allowList: List) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt index 2e9b87b797..f49bae1b9b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt @@ -43,7 +43,7 @@ interface Timeline { /** * This must be called before any other method after creating the timeline. It ensures the underlying database is open */ - fun start(rootThreadEventId: String? = null, rootPinnedMessageEventId: String? = null) + fun start(rootThreadEventId: String? = null, isFromPinnedEventsTimeline: Boolean = false) /** * This must be called when you don't need the timeline. It ensures the underlying database get closed. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt index 3d9f4e3dc7..64c6a8f068 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt @@ -33,9 +33,9 @@ data class TimelineSettings( */ val rootThreadEventId: String? = null, /** - * The root pinned message eventId if this is a pinned messages timeline, or null if this is NOT a pinned messages timeline. + * True if the timeline is a pinned messages timeline. */ - val rootPinnedMessageEventId: String? = null, + val isFromPinnedEventsTimeline: Boolean = false, /** * If true Sender Info shown in room will get the latest data information (avatar + displayName). */ @@ -50,5 +50,5 @@ data class TimelineSettings( /** * Returns true if this is a pinned messages timeline or false otherwise. */ - fun isPinnedMessagesTimeline() = rootPinnedMessageEventId != null + fun isPinnedEventsTimeline() = isFromPinnedEventsTimeline } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt index bf0f482c13..e80c860eb0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -33,7 +33,6 @@ import org.matrix.android.sdk.internal.session.room.membership.RoomMembersRespon import org.matrix.android.sdk.internal.session.room.membership.admin.UserIdAndReason import org.matrix.android.sdk.internal.session.room.membership.joining.InviteBody import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody -import org.matrix.android.sdk.internal.session.room.pinnedmessages.PinnedEventsStateResponse import org.matrix.android.sdk.internal.session.room.read.ReadBody import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse import org.matrix.android.sdk.internal.session.room.reporting.ReportContentBody @@ -249,7 +248,7 @@ internal interface RoomAPI { @Path("roomId") roomId: String, @Path("eventType") eventType: String, @Path("state_key") stateKey: String - ): PinnedEventsStateResponse + ): Content /** * Paginate relations for event based in normal topological order. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/pinnedmessages/PinnedEventsStateResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/pinnedmessages/PinnedEventsStateResponse.kt deleted file mode 100644 index c964f1c769..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/pinnedmessages/PinnedEventsStateResponse.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2022 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.session.room.pinnedmessages - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -internal data class PinnedEventsStateResponse( - /** - * A unique identifier for the event. - */ - @Json(name = "pinned") val pinned: List -) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt index 51cf975574..7a12bf8896 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt @@ -173,7 +173,25 @@ internal class DefaultStateService @AssistedInject constructor( ) } - override suspend fun pinMessage(eventIds: MutableList) { + override suspend fun pinEvent(eventId: String) { + val pinnedEvents = getStateEvent(EventType.STATE_ROOM_PINNED_EVENT, QueryStringValue.Equals("")) + ?.getIdsOfPinnedEvents() + ?.toMutableList() + pinnedEvents?.add(eventId) + val newListOfPinnedEvents = pinnedEvents?.toList() ?: return + setPinnedEvents(newListOfPinnedEvents) + } + + override suspend fun unpinEvent(eventId: String) { + val pinnedEvents = getStateEvent(EventType.STATE_ROOM_PINNED_EVENT, QueryStringValue.Equals("")) + ?.getIdsOfPinnedEvents() + ?.toMutableList() + pinnedEvents?.remove(eventId) + val newListOfPinnedEvents = pinnedEvents?.toList() ?: return + setPinnedEvents(newListOfPinnedEvents) + } + + private suspend fun setPinnedEvents(eventIds: List) { sendStateEvent( eventType = EventType.STATE_ROOM_PINNED_EVENT, body = PinnedEventsStateContent(eventIds).toContent(), @@ -181,15 +199,6 @@ internal class DefaultStateService @AssistedInject constructor( ) } - override fun getPinnedEventsState(): Event? { - return getStateEvent(EventType.STATE_ROOM_PINNED_EVENT, QueryStringValue.Equals("")) - } - - override fun isPinned(eventId: String): Boolean? { - val idsOfPinnedEvents: MutableList = getPinnedEventsState()?.getIdsOfPinnedEvents() ?: return null - return idsOfPinnedEvents.contains(eventId) - } - override suspend fun setJoinRulePublic() { updateJoinRule(RoomJoinRules.PUBLIC, null) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 458d2b8af0..469c387ccc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -99,8 +99,7 @@ internal class DefaultTimeline( private var isFromThreadTimeline = false private var rootThreadEventId: String? = null - private var isFromPinnedMessagesTimeline = false - private var rootPinnedMessageEventId: String? = null + private var isFromPinnedEventsTimeline = false private val strategyDependencies = LoadTimelineStrategy.Dependencies( timelineSettings = settings, @@ -132,7 +131,7 @@ internal class DefaultTimeline( override fun addListener(listener: Timeline.Listener): Boolean { listeners.add(listener) timelineScope.launch { - val snapshot = if (isFromPinnedMessagesTimeline) { + val snapshot = if (isFromPinnedEventsTimeline) { getPinnedEvents() } else { strategy.buildSnapshot() @@ -152,7 +151,7 @@ internal class DefaultTimeline( listeners.clear() } - override fun start(rootThreadEventId: String?, rootPinnedMessageEventId: String?) { + override fun start(rootThreadEventId: String?, isFromPinnedEventsTimeline: Boolean) { timelineScope.launch { loadRoomMembersIfNeeded() } @@ -161,8 +160,7 @@ internal class DefaultTimeline( if (isStarted.compareAndSet(false, true)) { isFromThreadTimeline = rootThreadEventId != null this@DefaultTimeline.rootThreadEventId = rootThreadEventId - isFromPinnedMessagesTimeline = rootPinnedMessageEventId != null - this@DefaultTimeline.rootPinnedMessageEventId = rootPinnedMessageEventId + this@DefaultTimeline.isFromPinnedEventsTimeline = isFromPinnedEventsTimeline // / val realm = Realm.getInstance(realmConfiguration) ensureReadReceiptAreLoaded(realm) @@ -267,8 +265,8 @@ internal class DefaultTimeline( } } Timber.v("$baseLogMessage: result $loadMoreResult") - val hasMoreToLoad = if (isFromPinnedMessagesTimeline) { - !areAllPinnedMessagesLoaded() + val hasMoreToLoad = if (isFromPinnedEventsTimeline) { + !areAllPinnedEventsLoaded() } else { loadMoreResult != LoadMoreResult.REACHED_END } @@ -352,7 +350,7 @@ internal class DefaultTimeline( } private suspend fun postSnapshot() { - val snapshot = if (isFromPinnedMessagesTimeline) { + val snapshot = if (isFromPinnedEventsTimeline) { getPinnedEvents() } else { strategy.buildSnapshot() @@ -371,25 +369,22 @@ internal class DefaultTimeline( } } - private fun getIdsOfPinnedEvents(): MutableList { + private fun getIdsOfPinnedEvents(): List { return stateEventDataSource .getStateEvent(roomId, EventType.STATE_ROOM_PINNED_EVENT, QueryStringValue.Equals("")) - ?.getIdsOfPinnedEvents() ?: mutableListOf("") + ?.getIdsOfPinnedEvents() + .orEmpty() } private fun getPinnedEvents(): List { - val idsOfPinnedEvents = getIdsOfPinnedEvents() - val pinnedEvents = ArrayList() - for (id in idsOfPinnedEvents) { - val timelineEvent = timelineEventDataSource.getTimelineEvent(roomId, id) - if (timelineEvent != null) { - pinnedEvents.add(timelineEvent) - } - } - return pinnedEvents.reversed() + return getIdsOfPinnedEvents() + .mapNotNull { id -> + timelineEventDataSource.getTimelineEvent(roomId, id) + } + .reversed() } - private fun areAllPinnedMessagesLoaded(): Boolean { + private fun areAllPinnedEventsLoaded(): Boolean { return getIdsOfPinnedEvents().size == getPinnedEvents().size } diff --git a/vector-config/src/main/res/values/config-settings.xml b/vector-config/src/main/res/values/config-settings.xml index caa60af797..f32b1fd95b 100755 --- a/vector-config/src/main/res/values/config-settings.xml +++ b/vector-config/src/main/res/values/config-settings.xml @@ -39,7 +39,7 @@ true true - false + false false true false diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 9d62904fc4..60c072c8ad 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -149,7 +149,7 @@ - + , val compressBeforeSending: Boolean) : RoomDetailAction() data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailAction() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index b60154192e..1e4aafc255 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -71,7 +71,6 @@ data class RoomDetailViewState( val isAllowedToManageWidgets: Boolean = false, val isAllowedToStartWebRTCCall: Boolean = true, val isAllowedToSetupEncryption: Boolean = true, - val rootPinnedMessageEventId: String?, val hasFailedSending: Boolean = false, val jitsiState: JitsiState = JitsiState(), val switchToParentSpace: Boolean = false, @@ -81,6 +80,7 @@ data class RoomDetailViewState( val isSharingLiveLocation: Boolean = false, val showKeyboardWhenPresented: Boolean = false, val sharedData: SharedData? = null, + val isFromPinnedEventsTimeline: Boolean = false, ) : MavericksState { constructor(args: TimelineArgs) : this( @@ -93,7 +93,7 @@ data class RoomDetailViewState( rootThreadEventId = args.threadTimelineArgs?.rootThreadEventId, showKeyboardWhenPresented = args.threadTimelineArgs?.showKeyboard.orFalse(), sharedData = args.sharedData, - rootPinnedMessageEventId = args.pinnedMessagesTimelineArgs?.rootPinnedMessageEventId, + isFromPinnedEventsTimeline = args.pinnedEventsTimelineArgs != null, ) fun isCallOptionAvailable(): Boolean { @@ -115,7 +115,7 @@ data class RoomDetailViewState( fun isThreadTimeline() = rootThreadEventId != null - fun isPinnedMessagesTimeline() = rootPinnedMessageEventId != null + fun isPinnedEventsTimeline() = isFromPinnedEventsTimeline fun isLocalRoom() = RoomLocalEcho.isLocalEchoId(roomId) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 90b9929762..14f7a2ca93 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -159,7 +159,7 @@ import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet import im.vector.app.features.home.room.detail.views.RoomDetailLazyLoadedViews import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet -import im.vector.app.features.home.room.pinnedmessages.arguments.PinnedMessagesTimelineArgs +import im.vector.app.features.home.room.pinnedmessages.arguments.PinnedEventsTimelineArgs import im.vector.app.features.home.room.threads.ThreadsManager import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.html.EventHtmlRenderer @@ -379,9 +379,8 @@ class TimelineFragment : ) } - if (isPinnedMessagesTimeline()) { - views.composerContainer.isVisible = false - views.voiceMessageRecorderContainer.isVisible = false + if (isPinnedEventsTimeline()) { + views.hideComposerViews() } timelineViewModel.observeViewEvents { @@ -883,8 +882,8 @@ class TimelineFragment : callActionsHandler.onVideoCallClicked() true } - R.id.open_pinned_messages -> { - navigateToPinnedMessages() + R.id.open_pinned_events -> { + navigateToPinnedEvents() true } R.id.menu_timeline_thread_list -> { @@ -1116,7 +1115,7 @@ class TimelineFragment : } private fun updateJumpToReadMarkerViewVisibility() { - if (isThreadTimeLine() || isPinnedMessagesTimeline()) return + if (isThreadTimeLine() || isPinnedEventsTimeline()) return viewLifecycleOwner.lifecycleScope.launchWhenResumed { val state = timelineViewModel.awaitState() val showJumpToUnreadBanner = when (state.unreadState) { @@ -1197,6 +1196,9 @@ class TimelineFragment : vectorBaseActivity.finish() } updateLiveLocationIndicator(mainState.isSharingLiveLocation) + if (isPinnedEventsTimeline()) { + views.hideComposerViews() + } } private fun handleRoomSummaryFailure(asyncRoomSummary: Fail) { @@ -1245,16 +1247,18 @@ class TimelineFragment : } views.includeThreadToolbar.roomToolbarThreadTitleTextView.text = resources.getText(R.string.thread_timeline_title) } - isPinnedMessagesTimeline() -> { + isPinnedEventsTimeline() -> { + withState(timelineViewModel) { state -> + timelineArgs.let { + val matrixItem = MatrixItem.RoomItem(it.roomId, state.asyncRoomSummary()?.displayName, state.asyncRoomSummary()?.avatarUrl) + avatarRenderer.render(matrixItem, views.includeThreadToolbar.roomToolbarThreadImageView) + views.includeThreadToolbar.roomToolbarThreadShieldImageView.render(state.asyncRoomSummary()?.roomEncryptionTrustLevel) + views.includeThreadToolbar.roomToolbarThreadSubtitleTextView.text = state.asyncRoomSummary()?.displayName + } + } views.includeRoomToolbar.roomToolbarContentView.isVisible = false views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = true - timelineArgs.pinnedMessagesTimelineArgs?.let { - val matrixItem = MatrixItem.RoomItem(it.roomId, it.displayName, it.avatarUrl) - avatarRenderer.render(matrixItem, views.includeThreadToolbar.roomToolbarThreadImageView) - views.includeThreadToolbar.roomToolbarThreadShieldImageView.render(it.roomEncryptionTrustLevel) - views.includeThreadToolbar.roomToolbarThreadSubtitleTextView.text = it.displayName - } - views.includeThreadToolbar.roomToolbarThreadTitleTextView.text = resources.getText(R.string.pinned_messages_timeline_title) + views.includeThreadToolbar.roomToolbarThreadTitleTextView.text = resources.getText(R.string.pinned_events_timeline_title) } else -> { views.includeRoomToolbar.roomToolbarContentView.isVisible = true @@ -1564,7 +1568,7 @@ class TimelineFragment : this.view?.hideKeyboard() MessageActionsBottomSheet - .newInstance(roomId, informationData, isThreadTimeLine(), isPinnedMessagesTimeline()) + .newInstance(roomId, informationData, isThreadTimeLine(), isPinnedEventsTimeline()) .show(requireActivity().supportFragmentManager, "MESSAGE_CONTEXTUAL_ACTIONS") return true @@ -1816,13 +1820,13 @@ class TimelineFragment : requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit) } } - is EventSharedAction.PinMessage -> { - timelineViewModel.handle(RoomDetailAction.PinMessage(action.eventId)) + is EventSharedAction.PinEvent -> { + timelineViewModel.handle(RoomDetailAction.PinEvent(action.eventId)) } - is EventSharedAction.UnpinMessage -> { - timelineViewModel.handle(RoomDetailAction.UnpinMessage(action.eventId)) + is EventSharedAction.UnpinEvent -> { + timelineViewModel.handle(RoomDetailAction.UnpinEvent(action.eventId)) } - is EventSharedAction.ViewPinnedMessageInRoom -> { + is EventSharedAction.ViewPinnedEventInRoom -> { handleViewInRoomAction(action.eventId) } is EventSharedAction.ReplyInThread -> { @@ -2005,24 +2009,19 @@ class TimelineFragment : } /** - * Navigate to pinned messages for the current room using the PinnedMessagesActivity. + * Navigate to pinned events for the current room using the PinnedEventsActivity. */ - private fun navigateToPinnedMessages() = withState(timelineViewModel) { state -> - val pinnedEventId = timelineViewModel.getIdOfLastPinnedEvent() + private fun navigateToPinnedEvents() { context?.let { - val pinnedMessagesTimelineArgs = PinnedMessagesTimelineArgs( + val pinnedEventsTimelineArgs = PinnedEventsTimelineArgs( roomId = timelineArgs.roomId, - displayName = state.asyncRoomSummary()?.displayName, - roomEncryptionTrustLevel = state.asyncRoomSummary()?.roomEncryptionTrustLevel, - avatarUrl = state.asyncRoomSummary()?.avatarUrl, - rootPinnedMessageEventId = pinnedEventId ) - navigator.openPinnedMessages(it, pinnedMessagesTimelineArgs) + navigator.openPinnedEvents(it, pinnedEventsTimelineArgs) } } private fun handleViewInRoomAction(eventId: String) { - val newRoom = timelineArgs.copy(threadTimelineArgs = null, pinnedMessagesTimelineArgs = null, eventId = eventId) + val newRoom = timelineArgs.copy(threadTimelineArgs = null, pinnedEventsTimelineArgs = null, eventId = eventId) context?.let { con -> val intent = RoomDetailActivity.newIntent(con, newRoom, false) intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK @@ -2086,7 +2085,7 @@ class TimelineFragment : /** * Returns true if the current room is a Pinned Messages room, false otherwise. */ - private fun isPinnedMessagesTimeline(): Boolean = withState(timelineViewModel) { it.isPinnedMessagesTimeline() } + private fun isPinnedEventsTimeline(): Boolean = withState(timelineViewModel) { it.isPinnedEventsTimeline() } /** * Returns true if the current room is a local room, false otherwise. diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 8044845301..3dc3839051 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -204,10 +204,10 @@ class TimelineViewModel @AssistedInject constructor( } private fun initSafe(room: Room, timeline: Timeline) { - timeline.start(initialState.rootThreadEventId, initialState.rootPinnedMessageEventId) + timeline.start(initialState.rootThreadEventId, initialState.isFromPinnedEventsTimeline) timeline.addListener(this) observeMembershipChanges() - if (!initialState.isPinnedMessagesTimeline()) { + if (!initialState.isPinnedEventsTimeline()) { observeSummaryState() } getUnreadState() @@ -451,8 +451,8 @@ class TimelineViewModel @AssistedInject constructor( override fun handle(action: RoomDetailAction) { when (action) { - is RoomDetailAction.PinMessage -> handlePinMessage(action) - is RoomDetailAction.UnpinMessage -> handleUnpinMessage(action) + is RoomDetailAction.PinEvent -> handlePinEvent(action) + is RoomDetailAction.UnpinEvent -> handleUnpinEvent(action) is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action) is RoomDetailAction.SendMedia -> handleSendMedia(action) is RoomDetailAction.SendSticker -> handleSendSticker(action) @@ -762,14 +762,6 @@ class TimelineViewModel @AssistedInject constructor( return room?.membershipService()?.getRoomMember(userId) } - fun getIdOfLastPinnedEvent(): String? { - return room - ?.stateService() - ?.getPinnedEventsState() - ?.getIdsOfPinnedEvents() - ?.last() - } - private fun handleComposerFocusChange(action: RoomDetailAction.ComposerFocusChange) { if (room == null) return // Ensure outbound session keys @@ -840,7 +832,7 @@ class TimelineViewModel @AssistedInject constructor( else -> false } } - initialState.isPinnedMessagesTimeline() -> false + initialState.isPinnedEventsTimeline() -> false else -> { when (itemId) { R.id.timeline_setting -> true @@ -851,7 +843,7 @@ class TimelineViewModel @AssistedInject constructor( // Show Join conference button only if there is an active conf id not joined. Otherwise fallback to default video disabled. ^ R.id.join_conference -> !state.isCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined R.id.search -> state.isSearchAvailable() - R.id.open_pinned_messages -> vectorPreferences.arePinnedMessagesEnabled() && areTherePinnedMessages() + R.id.open_pinned_events -> vectorPreferences.arePinnedEventsEnabled() && areTherePinnedEvents() R.id.menu_timeline_thread_list -> vectorPreferences.areThreadMessagesEnabled() R.id.dev_tools -> vectorPreferences.developerMode() else -> false @@ -1038,40 +1030,12 @@ class TimelineViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.NavigateToEvent(targetEventId)) } - private fun handlePinMessage(action: RoomDetailAction.PinMessage) { - if (room == null) return - val idsOfPinnedMessages = getIdsOfPinnedEvents() - if (idsOfPinnedMessages == null) return - idsOfPinnedMessages.add(action.eventId) - sendPinnedStateEvent(idsOfPinnedMessages, action) - } - - private fun handleUnpinMessage(action: RoomDetailAction.UnpinMessage) { - if (room == null) return - val idsOfPinnedMessages = getIdsOfPinnedEvents() - if (idsOfPinnedMessages == null) return - idsOfPinnedMessages.remove(action.eventId) - sendPinnedStateEvent(idsOfPinnedMessages, action) - } - - private fun getIdsOfPinnedEvents(): MutableList? { - return room - ?.stateService() - ?.getPinnedEventsState() - ?.getIdsOfPinnedEvents() - } - - private fun areTherePinnedMessages(): Boolean { - val idsOfPinnedMessages = getIdsOfPinnedEvents() ?: return false - return idsOfPinnedMessages.isNotEmpty() - } - - private fun sendPinnedStateEvent(eventIds: MutableList, action: RoomDetailAction) { + private fun handlePinEvent(action: RoomDetailAction.PinEvent) { viewModelScope.launch(Dispatchers.IO) { try { room ?.stateService() - ?.pinMessage(eventIds) + ?.pinEvent(action.eventId) _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) } catch (failure: Throwable) { _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure)) @@ -1079,6 +1043,31 @@ class TimelineViewModel @AssistedInject constructor( } } + private fun handleUnpinEvent(action: RoomDetailAction.UnpinEvent) { + viewModelScope.launch(Dispatchers.IO) { + try { + room + ?.stateService() + ?.unpinEvent(action.eventId) + _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) + } catch (failure: Throwable) { + _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure)) + } + } + } + + private fun getIdsOfPinnedEvents(): List? { + return room + ?.stateService() + ?.getStateEvent(EventType.STATE_ROOM_PINNED_EVENT, QueryStringValue.Equals("")) + ?.getIdsOfPinnedEvents() + } + + private fun areTherePinnedEvents(): Boolean { + val idsOfPinnedEvents = getIdsOfPinnedEvents() ?: return false + return idsOfPinnedEvents.isNotEmpty() + } + private fun handleResendEvent(action: RoomDetailAction.ResendMessage) { if (room == null) return val targetEventId = action.eventId diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/arguments/TimelineArgs.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/arguments/TimelineArgs.kt index d71f8ae7a9..ba99331a00 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/arguments/TimelineArgs.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/arguments/TimelineArgs.kt @@ -17,7 +17,7 @@ package im.vector.app.features.home.room.detail.arguments import android.os.Parcelable -import im.vector.app.features.home.room.pinnedmessages.arguments.PinnedMessagesTimelineArgs +import im.vector.app.features.home.room.pinnedmessages.arguments.PinnedEventsTimelineArgs import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.share.SharedData import kotlinx.parcelize.Parcelize @@ -29,7 +29,7 @@ data class TimelineArgs( val sharedData: SharedData? = null, val openShareSpaceForId: String? = null, val threadTimelineArgs: ThreadTimelineArgs? = null, - val pinnedMessagesTimelineArgs: PinnedMessagesTimelineArgs? = null, + val pinnedEventsTimelineArgs: PinnedEventsTimelineArgs? = null, val switchToParentSpace: Boolean = false, val isInviteAlreadyAccepted: Boolean = false ) : Parcelable diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt index 1e7b7c99ef..6679aa1d08 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt @@ -53,13 +53,13 @@ sealed class EventSharedAction( data class ReplyInThread(val eventId: String, val startsThread: Boolean) : EventSharedAction(R.string.reply_in_thread, R.drawable.ic_reply_in_thread) - data class PinMessage(val eventId: String) : - EventSharedAction(R.string.pinning_message, R.drawable.ic_pin_message) + data class PinEvent(val eventId: String) : + EventSharedAction(R.string.pinning_event, R.drawable.ic_pin_event) - data class UnpinMessage(val eventId: String) : - EventSharedAction(R.string.unpinning_message, R.drawable.ic_unpin_message) + data class UnpinEvent(val eventId: String) : + EventSharedAction(R.string.unpinning_event, R.drawable.ic_unpin_event) - data class ViewPinnedMessageInRoom(val eventId: String) : + data class ViewPinnedEventInRoom(val eventId: String) : EventSharedAction(R.string.view_in_room, R.drawable.ic_threads_view_in_room_24) object ViewInRoom : diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionState.kt index fa03877219..9d76410129 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionState.kt @@ -36,7 +36,7 @@ data class ActionPermissions( val canSendMessage: Boolean = false, val canReact: Boolean = false, val canRedact: Boolean = false, - val canPinMessage: Boolean = false + val canPinEvent: Boolean = false ) data class MessageActionState( @@ -52,7 +52,7 @@ data class MessageActionState( val expendedReportContentMenu: Boolean = false, val actionPermissions: ActionPermissions = ActionPermissions(), val isFromThreadTimeline: Boolean = false, - val isFromPinnedMessagesTimeline: Boolean = false + val isFromPinnedEventsTimeline: Boolean = false ) : MavericksState { constructor(args: TimelineEventFragmentArgs) : this( @@ -60,7 +60,7 @@ data class MessageActionState( eventId = args.eventId, informationData = args.informationData, isFromThreadTimeline = args.isFromThreadTimeline, - isFromPinnedMessagesTimeline = args.isFromPinnedMessagesTimeline + isFromPinnedEventsTimeline = args.isFromPinnedEventsTimeline ) fun senderName(): String = informationData.memberName?.toString() ?: "" diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt index c0740f0905..9f6a117dfd 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt @@ -93,7 +93,7 @@ class MessageActionsBottomSheet : } companion object { - fun newInstance(roomId: String, informationData: MessageInformationData, isFromThreadTimeline: Boolean, isFromPinnedMessagesTimeline: Boolean): MessageActionsBottomSheet { + fun newInstance(roomId: String, informationData: MessageInformationData, isFromThreadTimeline: Boolean, isFromPinnedEventsTimeline: Boolean): MessageActionsBottomSheet { return MessageActionsBottomSheet().apply { setArguments( TimelineEventFragmentArgs( @@ -101,7 +101,7 @@ class MessageActionsBottomSheet : roomId, informationData, isFromThreadTimeline, - isFromPinnedMessagesTimeline + isFromPinnedEventsTimeline ) ) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 341fa83987..feb7e7a444 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -42,9 +42,11 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.getIdsOfPinnedEvents import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage import org.matrix.android.sdk.api.session.events.model.isContentReportable import org.matrix.android.sdk.api.session.events.model.isTextMessage @@ -131,8 +133,8 @@ class MessageActionsViewModel @AssistedInject constructor( val canReact = powerLevelsHelper.isUserAllowedToSend(session.myUserId, false, EventType.REACTION) val canRedact = powerLevelsHelper.isUserAbleToRedact(session.myUserId) val canSendMessage = powerLevelsHelper.isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE) - val canPinMessage = powerLevelsHelper.isUserAllowedToSend(session.myUserId, false, EventType.STATE_ROOM_PINNED_EVENT) - val permissions = ActionPermissions(canSendMessage = canSendMessage, canRedact = canRedact, canReact = canReact, canPinMessage = canPinMessage) + val canPinEvent = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_PINNED_EVENT) + val permissions = ActionPermissions(canSendMessage = canSendMessage, canRedact = canRedact, canReact = canReact, canPinEvent = canPinEvent) setState { copy(actionPermissions = permissions) } @@ -334,91 +336,95 @@ class MessageActionsViewModel @AssistedInject constructor( ) { val eventId = timelineEvent.eventId if (!timelineEvent.root.isRedacted()) { - if (initialState.isFromPinnedMessagesTimeline) { - if (actionPermissions.canPinMessage && vectorPreferences.arePinnedMessagesEnabled()) { - add(EventSharedAction.UnpinMessage(eventId)) - add(EventSharedAction.ViewPinnedMessageInRoom(eventId)) + if (initialState.isFromPinnedEventsTimeline && vectorPreferences.arePinnedEventsEnabled()) { + add(EventSharedAction.ViewPinnedEventInRoom(eventId)) + if (actionPermissions.canPinEvent) { + add(EventSharedAction.UnpinEvent(eventId)) } - return - } - if (canReply(timelineEvent, messageContent, actionPermissions)) { - add(EventSharedAction.Reply(eventId)) - } - - if (canReplyInThread(timelineEvent, messageContent, actionPermissions)) { - add(EventSharedAction.ReplyInThread(eventId, !timelineEvent.isRootThread())) - } - - if (canViewInRoom(timelineEvent, messageContent, actionPermissions)) { - add(EventSharedAction.ViewInRoom) - } - - if (canEndPoll(timelineEvent, actionPermissions)) { - add(EventSharedAction.EndPoll(timelineEvent.eventId)) - } - - if (canEdit(timelineEvent, session.myUserId, actionPermissions)) { - add(EventSharedAction.Edit(eventId, timelineEvent.root.getClearType())) - } - - if (canCopy(msgType)) { - // TODO copy images? html? see ClipBoard - add(EventSharedAction.Copy(messageContent!!.body)) - } - - if (timelineEvent.canReact() && actionPermissions.canReact) { - add(EventSharedAction.AddReaction(eventId)) - } - - if (actionPermissions.canPinMessage && vectorPreferences.arePinnedMessagesEnabled()) { - val id: String = timelineEvent.root.eventId ?: return - val isPinned: Boolean = room?.stateService()?.isPinned(id) ?: return - if (isPinned) { - add(EventSharedAction.UnpinMessage(eventId)) - } else { - add(EventSharedAction.PinMessage(eventId)) + } else { + if (canReply(timelineEvent, messageContent, actionPermissions)) { + add(EventSharedAction.Reply(eventId)) } - } - if (canViewReactions(timelineEvent)) { - add(EventSharedAction.ViewReactions(informationData)) - } + if (canReplyInThread(timelineEvent, messageContent, actionPermissions)) { + add(EventSharedAction.ReplyInThread(eventId, !timelineEvent.isRootThread())) + } - if (canQuote(timelineEvent, messageContent, actionPermissions)) { - add(EventSharedAction.Quote(eventId)) - } + if (canViewInRoom(timelineEvent, messageContent, actionPermissions)) { + add(EventSharedAction.ViewInRoom) + } - if (timelineEvent.hasBeenEdited()) { - add(EventSharedAction.ViewEditHistory(informationData)) - } + if (canEndPoll(timelineEvent, actionPermissions)) { + add(EventSharedAction.EndPoll(timelineEvent.eventId)) + } - if (canSave(msgType) && messageContent is MessageWithAttachmentContent) { - add(EventSharedAction.Save(timelineEvent.eventId, messageContent)) - } + if (canEdit(timelineEvent, session.myUserId, actionPermissions)) { + add(EventSharedAction.Edit(eventId, timelineEvent.root.getClearType())) + } - if (canShare(msgType)) { - add(EventSharedAction.Share(timelineEvent.eventId, messageContent!!)) - } + if (canCopy(msgType)) { + // TODO copy images? html? see ClipBoard + add(EventSharedAction.Copy(messageContent!!.body)) + } - if (canRedact(timelineEvent, actionPermissions)) { - if (timelineEvent.root.getClearType() in EventType.POLL_START.values) { - add( - EventSharedAction.Redact( - eventId, - askForReason = informationData.senderId != session.myUserId, - dialogTitleRes = R.string.delete_poll_dialog_title, - dialogDescriptionRes = R.string.delete_poll_dialog_content - ) - ) - } else { - add( - EventSharedAction.Redact( - eventId, - askForReason = informationData.senderId != session.myUserId, - dialogTitleRes = R.string.delete_event_dialog_title, - dialogDescriptionRes = R.string.delete_event_dialog_content - ) - ) + if (timelineEvent.canReact() && actionPermissions.canReact) { + add(EventSharedAction.AddReaction(eventId)) + } + + if (actionPermissions.canPinEvent && vectorPreferences.arePinnedEventsEnabled()) { + val isPinned = room + ?.stateService() + ?.getStateEvent(EventType.STATE_ROOM_PINNED_EVENT, QueryStringValue.Equals("")) + ?.getIdsOfPinnedEvents() + ?.contains(eventId) + .orFalse() + if (isPinned) { + add(EventSharedAction.UnpinEvent(eventId)) + } else { + add(EventSharedAction.PinEvent(eventId)) + } + } + + if (canViewReactions(timelineEvent)) { + add(EventSharedAction.ViewReactions(informationData)) + } + + if (canQuote(timelineEvent, messageContent, actionPermissions)) { + add(EventSharedAction.Quote(eventId)) + } + + if (timelineEvent.hasBeenEdited()) { + add(EventSharedAction.ViewEditHistory(informationData)) + } + + if (canSave(msgType) && messageContent is MessageWithAttachmentContent) { + add(EventSharedAction.Save(timelineEvent.eventId, messageContent)) + } + + if (canShare(msgType)) { + add(EventSharedAction.Share(timelineEvent.eventId, messageContent!!)) + } + + if (canRedact(timelineEvent, actionPermissions)) { + if (timelineEvent.root.getClearType() in EventType.POLL_START.values) { + add( + EventSharedAction.Redact( + eventId, + askForReason = informationData.senderId != session.myUserId, + dialogTitleRes = R.string.delete_poll_dialog_title, + dialogDescriptionRes = R.string.delete_poll_dialog_content + ) + ) + } else { + add( + EventSharedAction.Redact( + eventId, + askForReason = informationData.senderId != session.myUserId, + dialogTitleRes = R.string.delete_event_dialog_title, + dialogDescriptionRes = R.string.delete_event_dialog_content + ) + ) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/TimelineEventFragmentArgs.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/TimelineEventFragmentArgs.kt index 17d55ac8b9..e6d14bdc7f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/TimelineEventFragmentArgs.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/TimelineEventFragmentArgs.kt @@ -26,5 +26,5 @@ data class TimelineEventFragmentArgs( val roomId: String, val informationData: MessageInformationData, val isFromThreadTimeline: Boolean = false, - val isFromPinnedMessagesTimeline: Boolean = false + val isFromPinnedEventsTimeline: Boolean = false ) : Parcelable diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index e2956d9d98..efc139a7af 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -122,16 +122,24 @@ class NoticeEventFormatter @Inject constructor( } private fun formatPinnedEvent(event: Event, disambiguatedDisplayName: String): CharSequence? { - val idsOfPinnedEvents: MutableList = event.getIdsOfPinnedEvents() ?: return null - val previousIdsOfPinnedEvents: MutableList? = event.getPreviousIdsOfPinnedEvents() - // A message was pinned - val pinnedMessageString = if (event.resolvedPrevContent() == null || previousIdsOfPinnedEvents != null && previousIdsOfPinnedEvents.size < idsOfPinnedEvents.size) { - sp.getString(R.string.user_pinned_message, disambiguatedDisplayName) - // A message was unpinned + val idsOfPinnedEvents: List = event.getIdsOfPinnedEvents() ?: return null + val previousIdsOfPinnedEvents: List? = event.getPreviousIdsOfPinnedEvents() + // An event was pinned + val pinnedEventString = if (event.resolvedPrevContent() == null || previousIdsOfPinnedEvents != null && previousIdsOfPinnedEvents.size < idsOfPinnedEvents.size) { + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_user_pinned_event_by_you, disambiguatedDisplayName) + } else { + sp.getString(R.string.notice_user_pinned_event, disambiguatedDisplayName) + } + // An event was unpinned } else { - sp.getString(R.string.user_unpinned_message, disambiguatedDisplayName) + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_user_unpinned_event_by_you, disambiguatedDisplayName) + } else { + sp.getString(R.string.notice_user_unpinned_event, disambiguatedDisplayName) + } } - return pinnedMessageString + return pinnedEventString } private fun formatRoomPowerLevels(event: Event, disambiguatedDisplayName: String): CharSequence? { @@ -194,7 +202,6 @@ class NoticeEventFormatter @Inject constructor( } fun format(event: Event, senderName: String?, isDm: Boolean): CharSequence? { - Timber.v("°°°°°°°°°°°°°°°°°°°format(event: Event, senderName: String?, isDm: Boolean)") return when (val type = event.getClearType()) { EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(event, senderName, isDm) EventType.STATE_ROOM_NAME -> formatRoomNameEvent(event, senderName) @@ -889,7 +896,6 @@ class NoticeEventFormatter @Inject constructor( } fun formatRedactedEvent(event: Event): String { - Timber.v("°°°°°°°formatRedactedEvent°°°°°°") return (event .unsignedData ?.redactedEvent diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/merged/MergedTimelines.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/merged/MergedTimelines.kt index 55d82e9e4f..76ceb4b688 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/merged/MergedTimelines.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/merged/MergedTimelines.kt @@ -113,7 +113,7 @@ class MergedTimelines( secondaryTimeline.removeAllListeners() } - override fun start(rootThreadEventId: String?, rootPinnedMessageEventId: String?) { + override fun start(rootThreadEventId: String?, isFromPinnedEventsTimeline: Boolean) { mainTimeline.start() secondaryTimeline.start() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/pinnedevents/PinnedEventsActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/pinnedevents/PinnedEventsActivity.kt new file mode 100644 index 0000000000..ca673a4177 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/pinnedevents/PinnedEventsActivity.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.pinnedmessages + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.extensions.replaceFragment +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.databinding.ActivityPinnedEventsBinding +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.TimelineFragment +import im.vector.app.features.home.room.detail.arguments.TimelineArgs +import im.vector.app.features.home.room.pinnedmessages.arguments.PinnedEventsTimelineArgs +import im.vector.lib.core.utils.compat.getParcelableCompat +import javax.inject.Inject + +@AndroidEntryPoint +class PinnedEventsActivity : VectorBaseActivity() { + + @Inject lateinit var avatarRenderer: AvatarRenderer + + override fun getBinding() = ActivityPinnedEventsBinding.inflate(layoutInflater) + + override fun getCoordinatorLayout() = views.coordinatorLayout + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + initFragment() + } + + private fun initFragment() { + if (isFirstCreation()) { + val args = getPinnedEventsTimelineArgs() + if (args == null) { + finish() + } else { + initPinnedEventsTimelineFragment(args) + } + } + } + + private fun initPinnedEventsTimelineFragment(pinnedEventsTimelineArgs: PinnedEventsTimelineArgs) = + replaceFragment( + views.pinnedEventsActivityFragmentContainer, + TimelineFragment::class.java, + TimelineArgs( + roomId = pinnedEventsTimelineArgs.roomId, + pinnedEventsTimelineArgs = pinnedEventsTimelineArgs + ) + ) + + private fun getPinnedEventsTimelineArgs(): PinnedEventsTimelineArgs? = intent?.extras?.getParcelableCompat(PINNED_EVENTS_TIMELINE_ARGS) + + companion object { + const val PINNED_EVENTS_TIMELINE_ARGS = "PINNED_EVENTS_TIMELINE_ARGS" + + fun newIntent( + context: Context, + pinnedEventsTimelineArgs: PinnedEventsTimelineArgs?, + ): Intent { + return Intent(context, PinnedEventsActivity::class.java).apply { + putExtra(PINNED_EVENTS_TIMELINE_ARGS, pinnedEventsTimelineArgs) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/pinnedmessages/arguments/PinnedMessagesTimelineArgs.kt b/vector/src/main/java/im/vector/app/features/home/room/pinnedevents/arguments/PinnedEventsTimelineArgs.kt similarity index 70% rename from vector/src/main/java/im/vector/app/features/home/room/pinnedmessages/arguments/PinnedMessagesTimelineArgs.kt rename to vector/src/main/java/im/vector/app/features/home/room/pinnedevents/arguments/PinnedEventsTimelineArgs.kt index daf6bb9240..2c81c2f4d5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/pinnedmessages/arguments/PinnedMessagesTimelineArgs.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/pinnedevents/arguments/PinnedEventsTimelineArgs.kt @@ -18,13 +18,8 @@ package im.vector.app.features.home.room.pinnedmessages.arguments import android.os.Parcelable import kotlinx.parcelize.Parcelize -import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel @Parcelize -data class PinnedMessagesTimelineArgs( - val roomId: String, - val displayName: String?, - val avatarUrl: String?, - val roomEncryptionTrustLevel: RoomEncryptionTrustLevel?, - val rootPinnedMessageEventId: String? +data class PinnedEventsTimelineArgs( + val roomId: String ) : Parcelable diff --git a/vector/src/main/java/im/vector/app/features/home/room/pinnedmessages/PinnedMessagesActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/pinnedmessages/PinnedMessagesActivity.kt deleted file mode 100644 index 3c5c305a20..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/pinnedmessages/PinnedMessagesActivity.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.features.home.room.pinnedmessages - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import dagger.hilt.android.AndroidEntryPoint -import im.vector.app.core.extensions.replaceFragment -import im.vector.app.core.platform.VectorBaseActivity -import im.vector.app.databinding.ActivityPinnedMessagesBinding -import im.vector.app.features.home.AvatarRenderer -import im.vector.app.features.home.room.detail.TimelineFragment -import im.vector.app.features.home.room.detail.arguments.TimelineArgs -import im.vector.app.features.home.room.pinnedmessages.arguments.PinnedMessagesTimelineArgs -import im.vector.lib.core.utils.compat.getParcelableCompat -import javax.inject.Inject - -@AndroidEntryPoint -class PinnedMessagesActivity : VectorBaseActivity() { - - @Inject lateinit var avatarRenderer: AvatarRenderer - - override fun getBinding() = ActivityPinnedMessagesBinding.inflate(layoutInflater) - - override fun getCoordinatorLayout() = views.coordinatorLayout - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - initFragment() - } - - private fun initFragment() { - if (isFirstCreation()) { - when (val fragment = fragmentToNavigate()) { - is DisplayFragment.PinnedMessagesTimeLine -> { - initPinnedMessagesTimelineFragment(fragment.pinnedMessagesTimelineArgs) - } - is DisplayFragment.ErrorFragment -> { - finish() - } - } - } - } - - private fun initPinnedMessagesTimelineFragment(pinnedMessagesTimelineArgs: PinnedMessagesTimelineArgs) = - replaceFragment( - views.pinnedMessagesActivityFragmentContainer, - TimelineFragment::class.java, - TimelineArgs( - roomId = pinnedMessagesTimelineArgs.roomId, - pinnedMessagesTimelineArgs = pinnedMessagesTimelineArgs - ) - ) - - /** - * Determine in witch fragment we should navigate. - */ - private fun fragmentToNavigate(): DisplayFragment { - getPinnedMessagesTimelineArgs()?.let { - return DisplayFragment.PinnedMessagesTimeLine(it) - } - return DisplayFragment.ErrorFragment - } - - private fun getPinnedMessagesTimelineArgs(): PinnedMessagesTimelineArgs? = intent?.extras?.getParcelableCompat(PINNED_MESSAGES_TIMELINE_ARGS) - - companion object { - const val PINNED_MESSAGES_TIMELINE_ARGS = "PINNED_MESSAGES_TIMELINE_ARGS" - - fun newIntent( - context: Context, - pinnedMessagesTimelineArgs: PinnedMessagesTimelineArgs?, - ): Intent { - return Intent(context, PinnedMessagesActivity::class.java).apply { - putExtra(PINNED_MESSAGES_TIMELINE_ARGS, pinnedMessagesTimelineArgs) - } - } - } - - sealed class DisplayFragment { - data class PinnedMessagesTimeLine(val pinnedMessagesTimelineArgs: PinnedMessagesTimelineArgs) : DisplayFragment() - object ErrorFragment : DisplayFragment() - } -} diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 58a28386fd..7333154776 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -58,8 +58,8 @@ import im.vector.app.features.home.room.detail.arguments.TimelineArgs import im.vector.app.features.home.room.detail.search.SearchActivity import im.vector.app.features.home.room.detail.search.SearchArgs import im.vector.app.features.home.room.filtered.FilteredRoomsActivity -import im.vector.app.features.home.room.pinnedmessages.arguments.PinnedMessagesTimelineArgs -import im.vector.app.features.home.room.pinnedmessages.PinnedMessagesActivity +import im.vector.app.features.home.room.pinnedmessages.arguments.PinnedEventsTimelineArgs +import im.vector.app.features.home.room.pinnedmessages.PinnedEventsActivity import im.vector.app.features.home.room.threads.ThreadsActivity import im.vector.app.features.home.room.threads.arguments.ThreadListArgs import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs @@ -601,11 +601,11 @@ class DefaultNavigator @Inject constructor( ) } - override fun openPinnedMessages(context: Context, pinnedMessagesTimelineArgs: PinnedMessagesTimelineArgs) { + override fun openPinnedEvents(context: Context, pinnedEventsTimelineArgs: PinnedEventsTimelineArgs) { context.startActivity( - PinnedMessagesActivity.newIntent( + PinnedEventsActivity.newIntent( context = context, - pinnedMessagesTimelineArgs = pinnedMessagesTimelineArgs + pinnedEventsTimelineArgs = pinnedEventsTimelineArgs ) ) } diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index 6cba3a298f..a7f736549d 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -27,7 +27,7 @@ import androidx.fragment.app.FragmentActivity import im.vector.app.features.analytics.plan.ViewRoom import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.displayname.getBestName -import im.vector.app.features.home.room.pinnedmessages.arguments.PinnedMessagesTimelineArgs +import im.vector.app.features.home.room.pinnedmessages.arguments.PinnedEventsTimelineArgs import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.location.LocationData import im.vector.app.features.location.LocationSharingMode @@ -199,7 +199,7 @@ interface Navigator { fun openThreadList(context: Context, threadTimelineArgs: ThreadTimelineArgs) - fun openPinnedMessages(context: Context, pinnedMessagesTimelineArgs: PinnedMessagesTimelineArgs) + fun openPinnedEvents(context: Context, pinnedEventsTimelineArgs: PinnedEventsTimelineArgs) fun openScreenSharingPermissionDialog( screenCaptureIntent: Intent, diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 0278ff91c2..0f9b2c03f0 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -234,7 +234,7 @@ class VectorPreferences @Inject constructor( private const val SETTINGS_LABS_ENABLE_ELEMENT_CALL_PERMISSION_SHORTCUTS = "SETTINGS_LABS_ENABLE_ELEMENT_CALL_PERMISSION_SHORTCUTS" - private const val SETTINGS_LABS_ENABLE_PINNED_MESSAGES = "SETTINGS_LABS_ENABLE_PINNED_MESSAGES" + private const val SETTINGS_LABS_ENABLE_PINNED_EVENTS = "SETTINGS_LABS_ENABLE_PINNED_EVENTS" // This key will be used to identify clients with the old thread support enabled io.element.thread const val SETTINGS_LABS_ENABLE_THREAD_MESSAGES_OLD_CLIENTS = "SETTINGS_LABS_ENABLE_THREAD_MESSAGES" @@ -1114,8 +1114,8 @@ class VectorPreferences @Inject constructor( return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_ELEMENT_CALL_PERMISSION_SHORTCUTS, false) } - fun arePinnedMessagesEnabled(): Boolean { - return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_PINNED_MESSAGES, getDefault(R.bool.settings_labs_pinned_messages_default)) + fun arePinnedEventsEnabled(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_PINNED_EVENTS, getDefault(R.bool.settings_labs_pinned_events_default)) } /** diff --git a/vector/src/main/res/drawable/ic_open_pinned_messages.xml b/vector/src/main/res/drawable/ic_open_pinned_events.xml similarity index 100% rename from vector/src/main/res/drawable/ic_open_pinned_messages.xml rename to vector/src/main/res/drawable/ic_open_pinned_events.xml diff --git a/vector/src/main/res/drawable/ic_pin_event.xml b/vector/src/main/res/drawable/ic_pin_event.xml new file mode 100644 index 0000000000..b0341a8aa8 --- /dev/null +++ b/vector/src/main/res/drawable/ic_pin_event.xml @@ -0,0 +1,4 @@ + + + diff --git a/vector/src/main/res/drawable/ic_pin_message.xml b/vector/src/main/res/drawable/ic_pin_message.xml deleted file mode 100644 index 9fc7b8cecc..0000000000 --- a/vector/src/main/res/drawable/ic_pin_message.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/vector/src/main/res/drawable/ic_unpin_event.xml b/vector/src/main/res/drawable/ic_unpin_event.xml new file mode 100644 index 0000000000..514d21ec17 --- /dev/null +++ b/vector/src/main/res/drawable/ic_unpin_event.xml @@ -0,0 +1,4 @@ + + + diff --git a/vector/src/main/res/drawable/ic_unpin_message.xml b/vector/src/main/res/drawable/ic_unpin_message.xml deleted file mode 100644 index 0cad148ca7..0000000000 --- a/vector/src/main/res/drawable/ic_unpin_message.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/vector/src/main/res/layout/activity_pinned_messages.xml b/vector/src/main/res/layout/activity_pinned_events.xml similarity index 88% rename from vector/src/main/res/layout/activity_pinned_messages.xml rename to vector/src/main/res/layout/activity_pinned_events.xml index e7b0ef00c9..93a75fe2e3 100644 --- a/vector/src/main/res/layout/activity_pinned_messages.xml +++ b/vector/src/main/res/layout/activity_pinned_events.xml @@ -11,7 +11,7 @@ android:layout_height="match_parent"> --> + android:defaultValue="@bool/settings_labs_pinned_events_default" + android:key="SETTINGS_LABS_ENABLE_PINNED_EVENTS" + android:title="@string/labs_enable_pinned_events" />