mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-21 08:55:42 +03:00
Merge remote-tracking branch 'cintek/pin_messages' into sc
Fixes https://github.com/SchildiChat/SchildiChat-android/issues/202 @SpiritCroc edit: - change some merge-unfriendly things, e.g. - indention - strings_sc.xml - don't touch what shouldn't be touched - add icon license to third party - add FEATURES.md entry Conflicts: library/ui-strings/src/main/res/values/strings.xml matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt vector-config/src/main/res/values/config-settings.xml vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt Change-Id: I490e42d50276edce3e7b099a11dba90d040d0dc0
This commit is contained in:
commit
befb402dbe
41 changed files with 561 additions and 20 deletions
|
@ -35,6 +35,7 @@ Here you can find some extra features and changes compared to Element Android (w
|
|||
- Render media captions ([MSC2530](https://github.com/matrix-org/matrix-spec-proposals/pull/2530))
|
||||
- Escape @room in the reply fallback to avoid unintentional room pings when replying
|
||||
- Render sticker body in room/thread preview
|
||||
- Pinned messages, contributed by [cintek](https://github.com/cintek) [for Element](https://github.com/vector-im/element-android/pull/7762)
|
||||
|
||||
- Branding (name, app icon, links)
|
||||
- Show a toast instead of a snackbar after copying text, in order to not block the input area right after copying
|
||||
|
|
1
changelog.d/7762.feature
Normal file
1
changelog.d/7762.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Added lab feature to pin/unpin messages
|
|
@ -239,4 +239,14 @@
|
|||
|
||||
<string name="settings_integrations_scalar_warning">⚠️ This setting by default (unless overridden by your homeserver\'s configuration) enables access to \"scalar\", Element\'s integration manager which is unfortunately proprietary, i.e. its source code is not open and can not be checked by the public or the SchildiChat developers.</string>
|
||||
|
||||
<!-- Pinned messages -->
|
||||
<string name="notice_user_pinned_event">%1$s pinned a message.</string>
|
||||
<string name="notice_user_unpinned_event">%1$s unpinned a message.</string>
|
||||
<string name="notice_user_pinned_event_by_you">You pinned a message.</string>
|
||||
<string name="notice_user_unpinned_event_by_you">You unpinned a message.</string>
|
||||
<string name="action_open_pinned_events">Open Pinned Messages</string>
|
||||
<string name="pinning_event">Pin</string>
|
||||
<string name="unpinning_event">Unpin</string>
|
||||
<string name="pinned_events_timeline_title">Pinned Messages</string>
|
||||
<string name="labs_enable_pinned_events">Enable Pinned Messages</string>
|
||||
</resources>
|
||||
|
|
|
@ -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
|
||||
|
@ -497,3 +498,11 @@ fun Event.supportsNotification() =
|
|||
|
||||
fun Event.isContentReportable() =
|
||||
this.getClearType() in EventType.MESSAGE + EventType.STATE_ROOM_BEACON_INFO.values
|
||||
|
||||
fun Event.getIdsOfPinnedEvents(): List<String>? {
|
||||
return getClearContent()?.toModel<PinnedEventsStateContent>()?.eventIds
|
||||
}
|
||||
|
||||
fun Event.getPreviousIdsOfPinnedEvents(): List<String>? {
|
||||
return resolvedPrevContent()?.toModel<PinnedEventsStateContent>()?.eventIds
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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: List<String>
|
||||
)
|
|
@ -66,6 +66,16 @@ interface StateService {
|
|||
*/
|
||||
suspend fun deleteAvatar()
|
||||
|
||||
/**
|
||||
* Pin an event of the room.
|
||||
*/
|
||||
suspend fun pinEvent(eventId: String)
|
||||
|
||||
/**
|
||||
* Unpin an event of the room.
|
||||
*/
|
||||
suspend fun unpinEvent(eventId: String)
|
||||
|
||||
/**
|
||||
* Send a state event to the room.
|
||||
* @param eventType The type of event to send.
|
||||
|
|
|
@ -45,7 +45,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, isFromPinnedEventsTimeline: Boolean = false)
|
||||
|
||||
/**
|
||||
* This must be called when you don't need the timeline. It ensures the underlying database get closed.
|
||||
|
|
|
@ -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,
|
||||
/**
|
||||
* True if the timeline is a pinned messages timeline.
|
||||
*/
|
||||
val isFromPinnedEventsTimeline: Boolean = false,
|
||||
/**
|
||||
* 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 isPinnedEventsTimeline() = isFromPinnedEventsTimeline
|
||||
}
|
||||
|
|
|
@ -240,6 +240,17 @@ internal interface RoomAPI {
|
|||
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state")
|
||||
suspend fun getRoomState(@Path("roomId") roomId: String): List<Event>
|
||||
|
||||
/**
|
||||
* 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
|
||||
): Content
|
||||
|
||||
/**
|
||||
* Paginate relations for event based in normal topological order.
|
||||
* @param roomId the room Id
|
||||
|
|
|
@ -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,32 @@ internal class DefaultStateService @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun pinEvent(eventId: String) {
|
||||
val pinnedEvents = getStateEvent(EventType.STATE_ROOM_PINNED_EVENT, QueryStringValue.Equals(""))
|
||||
?.getIdsOfPinnedEvents()
|
||||
?.toMutableList()
|
||||
pinnedEvents?.add(eventId)
|
||||
val newListOfPinnedEvents = pinnedEvents?.toList() ?: return
|
||||
setPinnedEvents(newListOfPinnedEvents)
|
||||
}
|
||||
|
||||
override suspend fun unpinEvent(eventId: String) {
|
||||
val pinnedEvents = getStateEvent(EventType.STATE_ROOM_PINNED_EVENT, QueryStringValue.Equals(""))
|
||||
?.getIdsOfPinnedEvents()
|
||||
?.toMutableList()
|
||||
pinnedEvents?.remove(eventId)
|
||||
val newListOfPinnedEvents = pinnedEvents?.toList() ?: return
|
||||
setPinnedEvents(newListOfPinnedEvents)
|
||||
}
|
||||
|
||||
private suspend fun setPinnedEvents(eventIds: List<String>) {
|
||||
sendStateEvent(
|
||||
eventType = EventType.STATE_ROOM_PINNED_EVENT,
|
||||
body = PinnedEventsStateContent(eventIds).toContent(),
|
||||
stateKey = ""
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun setJoinRulePublic() {
|
||||
updateJoinRule(RoomJoinRules.PUBLIC, null)
|
||||
}
|
||||
|
|
|
@ -37,6 +37,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.sender.SenderInfo
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
|
@ -69,8 +72,9 @@ internal class DefaultTimeline(
|
|||
private val settings: TimelineSettings,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val clock: Clock,
|
||||
private val stateEventDataSource: StateEventDataSource,
|
||||
private val timelineEventDataSource: TimelineEventDataSource,
|
||||
localEchoEventFactory: LocalEchoEventFactory,
|
||||
stateEventDataSource: StateEventDataSource,
|
||||
paginationTask: PaginationTask,
|
||||
getEventTask: GetContextOfEventTask,
|
||||
fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
|
||||
|
@ -105,6 +109,8 @@ internal class DefaultTimeline(
|
|||
private var targetEventId = initialEventId
|
||||
private val dimber = Dimber("TimelineChunks", DbgUtil.DBG_TIMELINE_CHUNKS)
|
||||
|
||||
private var isFromPinnedEventsTimeline = false
|
||||
|
||||
private val strategyDependencies = LoadTimelineStrategy.Dependencies(
|
||||
timelineSettings = settings,
|
||||
realm = backgroundRealm,
|
||||
|
@ -136,7 +142,11 @@ internal class DefaultTimeline(
|
|||
override fun addListener(listener: Timeline.Listener): Boolean {
|
||||
listeners.add(listener)
|
||||
timelineScope.launch {
|
||||
val snapshot = strategy.buildSnapshot()
|
||||
val snapshot = if (isFromPinnedEventsTimeline) {
|
||||
getPinnedEvents()
|
||||
} else {
|
||||
strategy.buildSnapshot()
|
||||
}
|
||||
withContext(coroutineDispatchers.main) {
|
||||
tryOrNull { listener.onTimelineUpdated(snapshot) }
|
||||
}
|
||||
|
@ -152,7 +162,7 @@ internal class DefaultTimeline(
|
|||
listeners.clear()
|
||||
}
|
||||
|
||||
override fun start(rootThreadEventId: String?) {
|
||||
override fun start(rootThreadEventId: String?, isFromPinnedEventsTimeline: Boolean) {
|
||||
timelineScope.launch {
|
||||
loadRoomMembersIfNeeded()
|
||||
}
|
||||
|
@ -161,6 +171,7 @@ internal class DefaultTimeline(
|
|||
if (isStarted.compareAndSet(false, true)) {
|
||||
isFromThreadTimeline = rootThreadEventId != null
|
||||
this@DefaultTimeline.rootThreadEventId = rootThreadEventId
|
||||
this@DefaultTimeline.isFromPinnedEventsTimeline = isFromPinnedEventsTimeline
|
||||
// /
|
||||
val realm = Realm.getInstance(realmConfiguration)
|
||||
ensureReadReceiptAreLoaded(realm)
|
||||
|
@ -267,7 +278,12 @@ internal class DefaultTimeline(
|
|||
}
|
||||
}
|
||||
Timber.v("$baseLogMessage: result $loadMoreResult")
|
||||
val hasMoreToLoad = loadMoreResult != LoadMoreResult.REACHED_END
|
||||
val hasMoreToLoad = if (isFromPinnedEventsTimeline) {
|
||||
!areAllPinnedEventsLoaded()
|
||||
} else {
|
||||
loadMoreResult != LoadMoreResult.REACHED_END
|
||||
}
|
||||
|
||||
updateState(direction) {
|
||||
it.copy(loading = false, hasMoreToLoad = hasMoreToLoad, hasLoadedAtLeastOnce = true)
|
||||
}
|
||||
|
@ -378,7 +394,11 @@ internal class DefaultTimeline(
|
|||
}
|
||||
|
||||
private suspend fun postSnapshot() {
|
||||
val snapshot = strategy.buildSnapshot()
|
||||
val snapshot = if (isFromPinnedEventsTimeline) {
|
||||
getPinnedEvents()
|
||||
} else {
|
||||
strategy.buildSnapshot()
|
||||
}
|
||||
Timber.v("Post snapshot of ${snapshot.size} events")
|
||||
// Async debugging to not slow down things too much
|
||||
dimber.exec {
|
||||
|
@ -405,6 +425,25 @@ internal class DefaultTimeline(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getIdsOfPinnedEvents(): List<String> {
|
||||
return stateEventDataSource
|
||||
.getStateEvent(roomId, EventType.STATE_ROOM_PINNED_EVENT, QueryStringValue.Equals(""))
|
||||
?.getIdsOfPinnedEvents()
|
||||
.orEmpty()
|
||||
}
|
||||
|
||||
private fun getPinnedEvents(): List<TimelineEvent> {
|
||||
return getIdsOfPinnedEvents()
|
||||
.mapNotNull { id ->
|
||||
timelineEventDataSource.getTimelineEvent(roomId, id)
|
||||
}
|
||||
.reversed()
|
||||
}
|
||||
|
||||
private fun areAllPinnedEventsLoaded(): Boolean {
|
||||
return getIdsOfPinnedEvents().size == getPinnedEvents().size
|
||||
}
|
||||
|
||||
private fun onNewTimelineEvents(eventIds: List<String>) {
|
||||
timelineScope.launch(coroutineDispatchers.main) {
|
||||
listeners.forEach {
|
||||
|
|
|
@ -85,6 +85,7 @@ internal class DefaultTimelineService @AssistedInject constructor(
|
|||
lightweightSettingsStorage = lightweightSettingsStorage,
|
||||
clock = clock,
|
||||
stateEventDataSource = stateEventDataSource,
|
||||
timelineEventDataSource = timelineEventDataSource,
|
||||
localEchoEventFactory = localEchoEventFactory
|
||||
)
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@
|
|||
<!-- Level 1: Labs -->
|
||||
<bool name="settings_labs_deferred_dm_visible">true</bool>
|
||||
<bool name="settings_labs_deferred_dm_default">true</bool>
|
||||
<bool name="settings_labs_pinned_events_default">false</bool>
|
||||
<bool name="settings_labs_thread_messages_default">true</bool>
|
||||
<bool name="settings_labs_new_app_layout_default">false</bool>
|
||||
<bool name="settings_labs_new_session_manager_default">false</bool>
|
||||
|
|
|
@ -151,6 +151,7 @@
|
|||
<activity android:name=".features.roomdirectory.roompreview.RoomPreviewActivity" />
|
||||
<activity android:name=".features.home.room.filtered.FilteredRoomsActivity" />
|
||||
<activity android:name=".features.home.room.threads.ThreadsActivity" />
|
||||
<activity android:name=".features.home.room.pinnedmessages.PinnedEventsActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".features.home.room.detail.RoomDetailActivity"
|
||||
|
|
|
@ -298,6 +298,35 @@ THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
</li>
|
||||
</ul>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<b>Fluent UI System Icons</b>
|
||||
<br/>
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Microsoft Corporation
|
||||
</li>
|
||||
</ul>
|
||||
<pre>
|
||||
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.
|
||||
</pre>
|
||||
|
||||
<h3>
|
||||
Apache License
|
||||
<br/>
|
||||
|
|
|
@ -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<ContentAttachmentData>, val compressBeforeSending: Boolean) : RoomDetailAction()
|
||||
data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailAction()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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<RoomSummary>) {
|
||||
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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<String>? {
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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<EventSharedAction> = 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() ?: ""
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<String> = event.getIdsOfPinnedEvents() ?: return null
|
||||
val previousIdsOfPinnedEvents: List<String>? = 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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -113,7 +113,7 @@ class MergedTimelines(
|
|||
secondaryTimeline.removeAllListeners()
|
||||
}
|
||||
|
||||
override fun start(rootThreadEventId: String?) {
|
||||
override fun start(rootThreadEventId: String?, isFromPinnedEventsTimeline: Boolean) {
|
||||
mainTimeline.start()
|
||||
secondaryTimeline.start()
|
||||
}
|
||||
|
|
|
@ -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<ActivityPinnedEventsBinding>() {
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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<Intent>
|
||||
|
|
|
@ -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<Intent>
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
9
vector/src/main/res/drawable/ic_open_pinned_events.xml
Normal file
9
vector/src/main/res/drawable/ic_open_pinned_events.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="m21.068,7.758 l-4.826,-4.826a2.75,2.75 0,0 0,-4.404 0.715l-2.435,4.87a0.75,0.75 0,0 1,-0.426 0.374L4.81,10.33a1.25,1.25 0,0 0,-0.476 2.065L7.439,15.5 3,19.94V21h1.06l4.44,-4.44 3.104,3.105a1.25,1.25 0,0 0,2.066 -0.476l1.44,-4.166a0.75,0.75 0,0 1,0.373 -0.426l4.87,-2.435a2.75,2.75 0,0 0,0.715 -4.404Z"
|
||||
android:fillColor="#70a81d"/>
|
||||
</vector>
|
4
vector/src/main/res/drawable/ic_pin_event.xml
Normal file
4
vector/src/main/res/drawable/ic_pin_event.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<vector android:height="30dp" android:viewportHeight="24"
|
||||
android:viewportWidth="24" android:width="30dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#212121" android:pathData="m16.242,2.932 l4.826,4.826a2.75,2.75 0,0 1,-0.715 4.404l-4.87,2.435a0.75,0.75 0,0 0,-0.374 0.426l-1.44,4.166a1.25,1.25 0,0 1,-2.065 0.476L8.5,16.561 4.06,21L3,21v-1.06l4.44,-4.44 -3.105,-3.104a1.25,1.25 0,0 1,0.476 -2.066l4.166,-1.44a0.75,0.75 0,0 0,0.426 -0.373l2.435,-4.87a2.75,2.75 0,0 1,4.405 -0.715ZM20.008,8.818 L15.182,3.992a1.25,1.25 0,0 0,-2.002 0.325l-2.435,4.871a2.25,2.25 0,0 1,-1.278 1.12l-3.789,1.31 6.705,6.704 1.308,-3.789a2.25,2.25 0,0 1,1.12 -1.277l4.872,-2.436a1.25,1.25 0,0 0,0.325 -2.002Z"/>
|
||||
</vector>
|
4
vector/src/main/res/drawable/ic_unpin_event.xml
Normal file
4
vector/src/main/res/drawable/ic_unpin_event.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<vector android:height="30dp" android:viewportHeight="24"
|
||||
android:viewportWidth="24" android:width="30dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#212121" android:pathData="M3.28,2.22a0.75,0.75 0,0 0,-1.06 1.06l5.905,5.905L4.81,10.33a1.25,1.25 0,0 0,-0.476 2.065L7.439,15.5 3,19.94L3,21h1.06l4.44,-4.44 3.105,3.105a1.25,1.25 0,0 0,2.065 -0.476l1.145,-3.313 5.905,5.904a0.75,0.75 0,0 0,1.06 -1.06L3.28,2.22ZM13.635,14.696 L12.383,18.322 5.678,11.617 9.304,10.365 13.635,14.696ZM19.683,10.82 L15.896,12.714 17.014,13.832 20.354,12.162a2.75,2.75 0,0 0,0.714 -4.404l-4.825,-4.826a2.75,2.75 0,0 0,-4.405 0.715l-1.67,3.34 1.118,1.117 1.894,-3.787a1.25,1.25 0,0 1,2.002 -0.325l4.826,4.826a1.25,1.25 0,0 1,-0.325 2.002Z"/>
|
||||
</vector>
|
24
vector/src/main/res/layout/activity_pinned_events.xml
Normal file
24
vector/src/main/res/layout/activity_pinned_events.xml
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/coordinatorLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/pinnedEventsActivityFragmentContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -40,6 +40,16 @@
|
|||
app:showAsAction="always"
|
||||
tools:visible="true" />
|
||||
|
||||
<!-- We always want to show this item as an icon -->
|
||||
<item
|
||||
android:id="@+id/open_pinned_events"
|
||||
android:icon="@drawable/ic_open_pinned_events"
|
||||
android:title="@string/action_open_pinned_events"
|
||||
android:visible="true"
|
||||
app:iconTint="?colorPrimary"
|
||||
app:showAsAction="always"
|
||||
tools:visible="true" />
|
||||
|
||||
<!-- We always want to show this item as an icon -->
|
||||
<item
|
||||
android:id="@+id/menu_timeline_thread_list"
|
||||
|
|
|
@ -130,6 +130,11 @@
|
|||
android:title="@string/labs_enable_latex_maths" />
|
||||
<!--</im.vector.app.core.preference.VectorPreferenceCategory>-->
|
||||
|
||||
<im.vector.app.core.preference.VectorSwitchPreference
|
||||
android:defaultValue="@bool/settings_labs_pinned_events_default"
|
||||
android:key="SETTINGS_LABS_ENABLE_PINNED_EVENTS"
|
||||
android:title="@string/labs_enable_pinned_events" />
|
||||
|
||||
<im.vector.app.core.preference.VectorSwitchPreference
|
||||
android:defaultValue="@bool/settings_labs_thread_messages_default"
|
||||
android:key="SETTINGS_LABS_ENABLE_THREAD_MESSAGES_FINAL"
|
||||
|
|
Loading…
Reference in a new issue