Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added lab feature to pin/unpin messages #7762

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/7762.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added lab feature to pin/unpin messages
8 changes: 8 additions & 0 deletions library/ui-strings/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@
<string name="action_sign_out_confirmation_simple">Are you sure you want to sign out?</string>
<string name="action_voice_call">Voice Call</string>
<string name="action_video_call">Video Call</string>
<string name="action_open_pinned_messages">Open Pinned Messages</string>
<string name="action_view_threads">View Threads</string>
<string name="action_mark_all_as_read">Mark all as read</string>
<string name="action_quick_reply">Quick reply</string>
Expand Down Expand Up @@ -801,6 +802,12 @@
<string name="threads_labs_enable_notice_title">Threads Beta</string>
<string name="threads_labs_enable_notice_message">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?</string>

<!-- Pinning -->
<string name="pinning_message">Pin</string>
<string name="unpinning_message">Unpin</string>
<string name="pinned_messages_timeline_title">Pinned Messages</string>
<string name="user_pinned_message">%1$s pinned a message.</string>
<string name="user_unpinned_message">%1$s unpinned a message.</string>

<!-- Search -->
<string name="search_hint">Search</string>
Expand Down Expand Up @@ -3032,6 +3039,7 @@

<string name="labs_auto_report_uisi">Auto Report Decryption Errors.</string>
<string name="labs_auto_report_uisi_desc">Your system will automatically send logs when an unable to decrypt error occurs</string>
<string name="labs_enable_pinned_messages">Enable Pinned Messages</string>
<string name="labs_enable_thread_messages">Enable Thread Messages</string>
<string name="labs_enable_thread_messages_desc">Note: app will be restarted</string>
<string name="settings_show_latest_profile">Show latest user info</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<String>? {
cintek marked this conversation as resolved.
Show resolved Hide resolved
return getClearContent()?.toModel<PinnedEventsStateContent>()?.eventIds
}

fun Event.getPreviousIdsOfPinnedEvents(): MutableList<String>? {
cintek marked this conversation as resolved.
Show resolved Hide resolved
return resolvedPrevContent()?.toModel<PinnedEventsStateContent>()?.eventIds
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"

Expand Down
Original file line number Diff line number Diff line change
@@ -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<String>
cintek marked this conversation as resolved.
Show resolved Hide resolved
)
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ interface StateService {
*/
suspend fun deleteAvatar()

/**
* Pin a message of the room.
*/
suspend fun pinMessage(eventIds: MutableList<String>)
cintek marked this conversation as resolved.
Show resolved Hide resolved

/**
* Send a state event to the room.
* @param eventType The type of event to send.
Expand Down Expand Up @@ -103,6 +108,16 @@ interface StateService {
*/
fun getStateEventsLive(eventTypes: Set<String>, stateKey: QueryStateEventValue): LiveData<List<Event>>

/**
* Get state event containing the IDs of pinned events of the room
*/
fun getPinnedEventsState(): Event?
cintek marked this conversation as resolved.
Show resolved Hide resolved

/**
* Tells if an event is a pinned message
*/
fun isPinned(eventId: String): Boolean?
cintek marked this conversation as resolved.
Show resolved Hide resolved

suspend fun setJoinRulePublic()
suspend fun setJoinRuleInviteOnly()
suspend fun setJoinRuleRestricted(allowList: List<String>)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*/
Expand All @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Event>
suspend fun getAllRoomStates(@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
): PinnedEventsStateResponse
cintek marked this conversation as resolved.
Show resolved Hide resolved

/**
* Paginate relations for event based in normal topological order.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ internal class DefaultResolveRoomStateTask @Inject constructor(

override suspend fun execute(params: ResolveRoomStateTask.Params): List<Event> {
return executeRequest(globalErrorReceiver) {
roomAPI.getRoomState(params.roomId)
roomAPI.getAllRoomStates(params.roomId)
}
}
}
Original file line number Diff line number Diff line change
@@ -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(
cintek marked this conversation as resolved.
Show resolved Hide resolved
/**
* A unique identifier for the event.
*/
@Json(name = "pinned") val pinned: List<String>
)
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,18 @@ 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
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
Expand Down Expand Up @@ -170,6 +173,23 @@ internal class DefaultStateService @AssistedInject constructor(
)
}

override suspend fun pinMessage(eventIds: MutableList<String>) {
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<String> = getPinnedEventsState()?.getIdsOfPinnedEvents() ?: return null
return idsOfPinnedEvents.contains(eventId)
}

override suspend fun setJoinRulePublic() {
updateJoinRule(RoomJoinRules.PUBLIC, null)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) }
}
Expand All @@ -141,7 +152,7 @@ internal class DefaultTimeline(
listeners.clear()
}

override fun start(rootThreadEventId: String?) {
override fun start(rootThreadEventId: String?, rootPinnedMessageEventId: String?) {
timelineScope.launch {
loadRoomMembersIfNeeded()
}
Expand All @@ -150,6 +161,8 @@ internal class DefaultTimeline(
if (isStarted.compareAndSet(false, true)) {
isFromThreadTimeline = rootThreadEventId != null
[email protected] = rootThreadEventId
isFromPinnedMessagesTimeline = rootPinnedMessageEventId != null
[email protected] = rootPinnedMessageEventId
// /
val realm = Realm.getInstance(realmConfiguration)
ensureReadReceiptAreLoaded(realm)
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 {
Expand All @@ -349,6 +371,28 @@ internal class DefaultTimeline(
}
}

private fun getIdsOfPinnedEvents(): MutableList<String> {
cintek marked this conversation as resolved.
Show resolved Hide resolved
return stateEventDataSource
.getStateEvent(roomId, EventType.STATE_ROOM_PINNED_EVENT, QueryStringValue.Equals(""))
?.getIdsOfPinnedEvents() ?: mutableListOf("")
cintek marked this conversation as resolved.
Show resolved Hide resolved
}

private fun getPinnedEvents(): List<TimelineEvent> {
val idsOfPinnedEvents = getIdsOfPinnedEvents()
val pinnedEvents = ArrayList<TimelineEvent>()
for (id in idsOfPinnedEvents) {
val timelineEvent = timelineEventDataSource.getTimelineEvent(roomId, id)
if (timelineEvent != null) {
pinnedEvents.add(timelineEvent)
}
}
return pinnedEvents.reversed()
}
cintek marked this conversation as resolved.
Show resolved Hide resolved

private fun areAllPinnedMessagesLoaded(): Boolean {
return getIdsOfPinnedEvents().size == getPinnedEvents().size
}

private fun onNewTimelineEvents(eventIds: List<String>) {
timelineScope.launch(coroutineDispatchers.main) {
listeners.forEach {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ internal class DefaultTimelineService @AssistedInject constructor(
lightweightSettingsStorage = lightweightSettingsStorage,
clock = clock,
stateEventDataSource = stateEventDataSource,
timelineEventDataSource = timelineEventDataSource,
)
}

Expand Down
Loading