+
+ -
+ Fluent UI System Icons
+
+ MIT License
+
+ Copyright (c) 2020 Microsoft Corporation
+
+
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
Apache License
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt
index e891405ec3..ebdeab62cf 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt
@@ -30,6 +30,8 @@ import org.matrix.android.sdk.api.session.widgets.model.Widget
import org.matrix.android.sdk.api.util.MatrixItem
sealed class RoomDetailAction : VectorViewModelAction {
+ data class PinEvent(val eventId: String) : RoomDetailAction()
+ data class UnpinEvent(val eventId: String) : RoomDetailAction()
data class SendSticker(val stickerContent: MessageStickerContent) : RoomDetailAction()
data class SendMedia(val attachments: List, 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 44bb0c5793..21baa4900a 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
@@ -84,6 +84,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(
@@ -98,6 +99,7 @@ data class RoomDetailViewState(
rootThreadEventId = args.threadTimelineArgs?.rootThreadEventId,
showKeyboardWhenPresented = args.threadTimelineArgs?.showKeyboard.orFalse(),
sharedData = args.sharedData,
+ isFromPinnedEventsTimeline = args.pinnedEventsTimelineArgs != null,
)
fun isCallOptionAvailable(): Boolean {
@@ -122,5 +124,7 @@ data class RoomDetailViewState(
fun isThreadTimeline() = rootThreadEventId != 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 229c08261b..0e586b4177 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
@@ -176,6 +176,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.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.home.room.typing.TypingHelper
@@ -413,6 +414,10 @@ class TimelineFragment :
)
}
+ if (isPinnedEventsTimeline()) {
+ views.hideComposerViews()
+ }
+
timelineViewModel.observeViewEvents {
when (it) {
is RoomDetailViewEvents.Failure -> displayErrorMessage(it)
@@ -1067,6 +1072,10 @@ class TimelineFragment :
requireActivity().restart()
true
}
+ R.id.open_pinned_events -> {
+ navigateToPinnedEvents()
+ true
+ }
R.id.menu_timeline_thread_list -> {
navigateToThreadList()
true
@@ -1390,7 +1399,7 @@ class TimelineFragment :
}
private fun updateJumpToReadMarkerViewVisibility() {
- if (isThreadTimeLine()) return
+ if (isThreadTimeLine() || isPinnedEventsTimeline()) return
viewLifecycleOwner.lifecycleScope.launch {
withResumed {
viewLifecycleOwner.lifecycleScope.launch {
@@ -1480,6 +1489,9 @@ class TimelineFragment :
vectorBaseActivity.finish()
}
updateLiveLocationIndicator(mainState.isSharingLiveLocation)
+ if (isPinnedEventsTimeline()) {
+ views.hideComposerViews()
+ }
}
private fun handleRoomSummaryFailure(asyncRoomSummary: Fail) {
@@ -1536,6 +1548,19 @@ class TimelineFragment :
}
views.includeThreadToolbar.roomToolbarThreadTitleTextView.text = resources.getText(R.string.thread_timeline_title)
}
+ 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
+ views.includeThreadToolbar.roomToolbarThreadTitleTextView.text = resources.getText(R.string.pinned_events_timeline_title)
+ }
else -> {
views.includeRoomToolbar.roomToolbarContentView.isVisible = true
views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = false
@@ -1863,7 +1888,7 @@ class TimelineFragment :
this.view?.hideKeyboard()
MessageActionsBottomSheet
- .newInstance(roomId, informationData, isThreadTimeLine())
+ .newInstance(roomId, informationData, isThreadTimeLine(), isPinnedEventsTimeline())
.show(requireActivity().supportFragmentManager, "MESSAGE_CONTEXTUAL_ACTIONS")
return true
@@ -2159,6 +2184,15 @@ class TimelineFragment :
requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit)
}
}
+ is EventSharedAction.PinEvent -> {
+ timelineViewModel.handle(RoomDetailAction.PinEvent(action.eventId))
+ }
+ is EventSharedAction.UnpinEvent -> {
+ timelineViewModel.handle(RoomDetailAction.UnpinEvent(action.eventId))
+ }
+ is EventSharedAction.ViewPinnedEventInRoom -> {
+ handleViewInRoomAction(action.eventId)
+ }
is EventSharedAction.ReplyInThread -> {
if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) {
onReplyInThreadClicked(action)
@@ -2339,6 +2373,27 @@ class TimelineFragment :
}
}
+ /**
+ * Navigate to pinned events for the current room using the PinnedEventsActivity.
+ */
+ private fun navigateToPinnedEvents() {
+ context?.let {
+ val pinnedEventsTimelineArgs = PinnedEventsTimelineArgs(
+ roomId = timelineArgs.roomId,
+ )
+ navigator.openPinnedEvents(it, pinnedEventsTimelineArgs)
+ }
+ }
+
+ private fun handleViewInRoomAction(eventId: String) {
+ 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
+ con.startActivity(intent)
+ }
+ }
+
// VectorInviteView.Callback
override fun onAcceptInvite() {
timelineViewModel.handle(RoomDetailAction.AcceptInvite)
@@ -2421,6 +2476,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 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 514a2e1dc9..78581cc090 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
@@ -105,6 +105,7 @@ import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState
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.RelationType
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
@@ -263,10 +264,12 @@ class TimelineViewModel @AssistedInject constructor(
}
private fun initSafe(room: Room, timeline: Timeline) {
- timeline.start(initialState.rootThreadEventId)
+ timeline.start(initialState.rootThreadEventId, initialState.isFromPinnedEventsTimeline)
timeline.addListener(this)
observeMembershipChanges()
- observeSummaryState()
+ if (!initialState.isPinnedEventsTimeline()) {
+ observeSummaryState()
+ }
getUnreadState()
observeSyncState()
observeDataStore()
@@ -535,6 +538,8 @@ class TimelineViewModel @AssistedInject constructor(
override fun handle(action: RoomDetailAction) {
when (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)
@@ -944,6 +949,7 @@ class TimelineViewModel @AssistedInject constructor(
else -> false
}
}
+ initialState.isPinnedEventsTimeline() -> false
else -> {
when (itemId) {
R.id.timeline_setting -> false // replaced by show_room_info (downstream)
@@ -954,6 +960,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_events -> vectorPreferences.arePinnedEventsEnabled() && areTherePinnedEvents()
R.id.menu_timeline_thread_list -> vectorPreferences.areThreadMessagesEnabled()
// SC extras start
R.id.show_room_info -> true // SC
@@ -1163,6 +1170,44 @@ class TimelineViewModel @AssistedInject constructor(
}
}
+ private fun handlePinEvent(action: RoomDetailAction.PinEvent) {
+ viewModelScope.launch(Dispatchers.IO) {
+ try {
+ room
+ ?.stateService()
+ ?.pinEvent(action.eventId)
+ _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action))
+ } catch (failure: Throwable) {
+ _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure))
+ }
+ }
+ }
+
+ 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 57dc49800c..dfeae679ca 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.PinnedEventsTimelineArgs
import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs
import im.vector.app.features.share.SharedData
import kotlinx.parcelize.Parcelize
@@ -30,6 +31,7 @@ data class TimelineArgs(
val openAtFirstUnread: Boolean? = null,
val openAnonymously: Boolean = false,
val threadTimelineArgs: ThreadTimelineArgs? = 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 7bf9f536f2..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,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 PinEvent(val eventId: String) :
+ EventSharedAction(R.string.pinning_event, R.drawable.ic_pin_event)
+
+ data class UnpinEvent(val eventId: String) :
+ EventSharedAction(R.string.unpinning_event, R.drawable.ic_unpin_event)
+
+ data class ViewPinnedEventInRoom(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..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
@@ -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 canPinEvent: 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 isFromPinnedEventsTimeline: Boolean = false
) : MavericksState {
constructor(args: TimelineEventFragmentArgs) : this(
roomId = args.roomId,
eventId = args.eventId,
informationData = args.informationData,
- isFromThreadTimeline = args.isFromThreadTimeline
+ isFromThreadTimeline = args.isFromThreadTimeline,
+ 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 53d9e2aa99..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,14 +93,15 @@ class MessageActionsBottomSheet :
}
companion object {
- fun newInstance(roomId: String, informationData: MessageInformationData, isFromThreadTimeline: Boolean): MessageActionsBottomSheet {
+ fun newInstance(roomId: String, informationData: MessageInformationData, isFromThreadTimeline: Boolean, isFromPinnedEventsTimeline: Boolean): MessageActionsBottomSheet {
return MessageActionsBottomSheet().apply {
setArguments(
TimelineEventFragmentArgs(
informationData.eventId,
roomId,
informationData,
- isFromThreadTimeline
+ isFromThreadTimeline,
+ 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 dc16f9eda5..9b1e9cbc0c 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,7 +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 permissions = ActionPermissions(canSendMessage = canSendMessage, canRedact = canRedact, canReact = canReact)
+ 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)
}
@@ -337,6 +340,15 @@ class MessageActionsViewModel @AssistedInject constructor(
) {
val eventId = timelineEvent.eventId
if (!timelineEvent.root.isRedacted()) {
+ if (initialState.isFromPinnedEventsTimeline && vectorPreferences.arePinnedEventsEnabled()) {
+ add(EventSharedAction.ViewPinnedEventInRoom(eventId))
+ if (actionPermissions.canPinEvent) {
+ add(EventSharedAction.UnpinEvent(eventId))
+ }
+ } else {
+
+ // wrong indention for merge-ability
+
if (canReply(timelineEvent, messageContent, actionPermissions)) {
add(EventSharedAction.Reply(eventId))
}
@@ -370,6 +382,20 @@ class MessageActionsViewModel @AssistedInject constructor(
add(EventSharedAction.ViewReactions(informationData))
}
+ 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 (canQuote(timelineEvent, messageContent, actionPermissions) && !vectorPreferences.simplifiedMode()) {
add(EventSharedAction.Quote(eventId))
}
@@ -407,7 +433,7 @@ class MessageActionsViewModel @AssistedInject constructor(
)
}
}
- }
+ }} // wrong indention on purpose - end
if (vectorPreferences.developerMode()) {
if (timelineEvent.isEncrypted() && timelineEvent.root.mCryptoError != null) {
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..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
@@ -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 isFromPinnedEventsTimeline: 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 84b71ceedf..5bc77c659c 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
@@ -76,6 +76,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 837b828bf6..8e213e4ffd 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
@@ -31,6 +31,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
@@ -90,6 +92,7 @@ class NoticeEventFormatter @Inject constructor(
EventType.STATE_ROOM_WIDGET_LEGACY -> formatWidgetEvent(event, senderName)
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName, isDm)
EventType.STATE_ROOM_POWER_LEVELS -> formatRoomPowerLevels(event, senderName)
+ EventType.STATE_ROOM_PINNED_EVENT -> formatPinnedEvent(event, senderName)
EventType.CALL_INVITE,
EventType.CALL_CANDIDATES,
EventType.CALL_HANGUP,
@@ -122,6 +125,27 @@ class NoticeEventFormatter @Inject constructor(
}
}
+ private fun formatPinnedEvent(event: Event, disambiguatedDisplayName: String): CharSequence? {
+ 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 {
+ 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 pinnedEventString
+ }
+
private fun formatRoomPowerLevels(event: Event, disambiguatedDisplayName: String): CharSequence? {
val powerLevelsContent: PowerLevelsContent = event.content.toModel() ?: return null
val previousPowerLevelsContent: PowerLevelsContent = event.resolvedPrevContent().toModel() ?: return null
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 2dcb6cc6d8..1d28285a10 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..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?) {
+ 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/pinnedevents/arguments/PinnedEventsTimelineArgs.kt b/vector/src/main/java/im/vector/app/features/home/room/pinnedevents/arguments/PinnedEventsTimelineArgs.kt
new file mode 100644
index 0000000000..2c81c2f4d5
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/pinnedevents/arguments/PinnedEventsTimelineArgs.kt
@@ -0,0 +1,25 @@
+/*
+ * 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
+
+@Parcelize
+data class PinnedEventsTimelineArgs(
+ val roomId: 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 21494f019b..cb00a69dd6 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
@@ -60,6 +60,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.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
@@ -609,6 +611,15 @@ class DefaultNavigator @Inject constructor(
)
}
+ override fun openPinnedEvents(context: Context, pinnedEventsTimelineArgs: PinnedEventsTimelineArgs) {
+ context.startActivity(
+ PinnedEventsActivity.newIntent(
+ context = context,
+ pinnedEventsTimelineArgs = pinnedEventsTimelineArgs
+ )
+ )
+ }
+
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 7c1e825b0e..c2ee411772 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.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
@@ -200,6 +201,8 @@ interface Navigator {
fun openThreadList(context: Context, threadTimelineArgs: ThreadTimelineArgs)
+ fun openPinnedEvents(context: Context, pinnedEventsTimelineArgs: PinnedEventsTimelineArgs)
+
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 d2fb3493c8..2d62eed68d 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
@@ -278,6 +278,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_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"
@@ -1436,6 +1438,10 @@ class VectorPreferences @Inject constructor(
return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_ELEMENT_CALL_PERMISSION_SHORTCUTS, false)
}
+ fun arePinnedEventsEnabled(): Boolean {
+ return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_PINNED_EVENTS, getDefault(R.bool.settings_labs_pinned_events_default))
+ }
+
/**
* Indicates whether or not thread messages are enabled.
*/
diff --git a/vector/src/main/res/drawable/ic_open_pinned_events.xml b/vector/src/main/res/drawable/ic_open_pinned_events.xml
new file mode 100644
index 0000000000..389db91616
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_open_pinned_events.xml
@@ -0,0 +1,9 @@
+
+
+
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_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/layout/activity_pinned_events.xml b/vector/src/main/res/layout/activity_pinned_events.xml
new file mode 100644
index 0000000000..93a75fe2e3
--- /dev/null
+++ b/vector/src/main/res/layout/activity_pinned_events.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 4a19e19065..fed21fc34d 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" />
+
+
+
+
+