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" />
+
+
+
+
+