Enforce correct permissions when pinning messages.

This commit is contained in:
Michelle Tang
2025-11-25 11:19:20 -05:00
committed by jeffrey-signal
parent 40008bddc7
commit 44ec15c0e0
12 changed files with 63 additions and 19 deletions

View File

@@ -39,9 +39,10 @@ public final class ConversationReactionDelegate {
@NonNull Recipient conversationRecipient, @NonNull Recipient conversationRecipient,
@NonNull ConversationMessage conversationMessage, @NonNull ConversationMessage conversationMessage,
boolean isNonAdminInAnnouncementGroup, boolean isNonAdminInAnnouncementGroup,
@NonNull SelectedConversationModel selectedConversationModel) @NonNull SelectedConversationModel selectedConversationModel,
boolean canEditGroupInfo)
{ {
resolveOverlay().show(activity, conversationRecipient, conversationMessage, lastSeenDownPoint, isNonAdminInAnnouncementGroup, selectedConversationModel); resolveOverlay().show(activity, conversationRecipient, conversationMessage, lastSeenDownPoint, isNonAdminInAnnouncementGroup, selectedConversationModel, canEditGroupInfo);
} }
public void hide() { public void hide() {

View File

@@ -69,6 +69,7 @@ public final class ConversationReactionOverlay extends FrameLayout {
private SelectedConversationModel selectedConversationModel; private SelectedConversationModel selectedConversationModel;
private OverlayState overlayState = OverlayState.HIDDEN; private OverlayState overlayState = OverlayState.HIDDEN;
private boolean isNonAdminInAnnouncementGroup; private boolean isNonAdminInAnnouncementGroup;
private boolean canEditGroupInfo;
private boolean downIsOurs; private boolean downIsOurs;
private int selected = -1; private int selected = -1;
@@ -144,7 +145,8 @@ public final class ConversationReactionOverlay extends FrameLayout {
@NonNull ConversationMessage conversationMessage, @NonNull ConversationMessage conversationMessage,
@NonNull PointF lastSeenDownPoint, @NonNull PointF lastSeenDownPoint,
boolean isNonAdminInAnnouncementGroup, boolean isNonAdminInAnnouncementGroup,
@NonNull SelectedConversationModel selectedConversationModel) @NonNull SelectedConversationModel selectedConversationModel,
boolean canEditGroupInfo)
{ {
if (overlayState != OverlayState.HIDDEN) { if (overlayState != OverlayState.HIDDEN) {
return; return;
@@ -154,6 +156,7 @@ public final class ConversationReactionOverlay extends FrameLayout {
this.conversationRecipient = conversationRecipient; this.conversationRecipient = conversationRecipient;
this.selectedConversationModel = selectedConversationModel; this.selectedConversationModel = selectedConversationModel;
this.isNonAdminInAnnouncementGroup = isNonAdminInAnnouncementGroup; this.isNonAdminInAnnouncementGroup = isNonAdminInAnnouncementGroup;
this.canEditGroupInfo = canEditGroupInfo;
overlayState = OverlayState.UNINITAILIZED; overlayState = OverlayState.UNINITAILIZED;
selected = -1; selected = -1;
@@ -674,7 +677,7 @@ public final class ConversationReactionOverlay extends FrameLayout {
} }
private @NonNull List<ActionItem> getMenuActionItems(@NonNull ConversationMessage conversationMessage) { private @NonNull List<ActionItem> getMenuActionItems(@NonNull ConversationMessage conversationMessage) {
MenuState menuState = MenuState.getMenuState(conversationRecipient, conversationMessage.getMultiselectCollection().toSet(), false, isNonAdminInAnnouncementGroup); MenuState menuState = MenuState.getMenuState(conversationRecipient, conversationMessage.getMultiselectCollection().toSet(), false, isNonAdminInAnnouncementGroup, canEditGroupInfo);
List<ActionItem> items = new ArrayList<>(); List<ActionItem> items = new ArrayList<>();

View File

@@ -102,7 +102,8 @@ public final class MenuState {
public static MenuState getMenuState(@NonNull Recipient conversationRecipient, public static MenuState getMenuState(@NonNull Recipient conversationRecipient,
@NonNull Set<MultiselectPart> selectedParts, @NonNull Set<MultiselectPart> selectedParts,
boolean shouldShowMessageRequest, boolean shouldShowMessageRequest,
boolean isNonAdminInAnnouncementGroup) boolean isNonAdminInAnnouncementGroup,
boolean canEditGroupInfo)
{ {
Builder builder = new Builder(); Builder builder = new Builder();
@@ -170,11 +171,11 @@ public final class MenuState {
hasPollTerminate = true; hasPollTerminate = true;
} }
if (RemoteConfig.sendPinnedMessages() && !messageRecord.isPending() && messageRecord.getPinnedUntil() == 0 && !conversationRecipient.isReleaseNotes()) { // TODO(michelle): Also check against group permissions if (RemoteConfig.sendPinnedMessages() && !messageRecord.isPending() && messageRecord.getPinnedUntil() == 0 && !conversationRecipient.isReleaseNotes() && canEditGroupInfo) {
canPinMessage = true; canPinMessage = true;
} }
if (RemoteConfig.sendPinnedMessages() && messageRecord.getPinnedUntil() != 0 && !conversationRecipient.isReleaseNotes()) { // TODO(michelle): Also check against group permissions if (RemoteConfig.sendPinnedMessages() && messageRecord.getPinnedUntil() != 0 && !conversationRecipient.isReleaseNotes() && canEditGroupInfo) {
canUnpinMessage = true; canUnpinMessage = true;
} }
} }

View File

@@ -38,6 +38,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.BottomSheetUtil import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.StickyHeaderDecoration import org.thoughtcrime.securesms.util.StickyHeaderDecoration
import org.thoughtcrime.securesms.util.fragments.findListener import org.thoughtcrime.securesms.util.fragments.findListener
import org.thoughtcrime.securesms.util.visible
import java.util.Locale import java.util.Locale
/** /**
@@ -110,12 +111,13 @@ class PinnedMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment()
initializeGiphyMp4(view.findViewById(R.id.video_container)!!, list) initializeGiphyMp4(view.findViewById(R.id.video_container)!!, list)
// TODO(michelle): Hide if not allowed to unpin / Check with design about a confirmation dialog here // TODO(michelle): Check with design about a confirmation dialog here
val unpinAll = view.findViewById<TextView>(R.id.unpin_all) val unpinAll = view.findViewById<TextView>(R.id.unpin_all)
unpinAll.setOnClickListener { unpinAll.setOnClickListener {
viewModel.unpinMessage() viewModel.unpinMessage()
dismissAllowingStateLoss() dismissAllowingStateLoss()
} }
unpinAll.visible = requireArguments().getBoolean(KEY_CAN_UNPIN)
} }
private fun initializeGiphyMp4(videoContainer: ViewGroup, list: RecyclerView): GiphyMp4ProjectionRecycler { private fun initializeGiphyMp4(videoContainer: ViewGroup, list: RecyclerView): GiphyMp4ProjectionRecycler {
@@ -271,12 +273,14 @@ class PinnedMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment()
private const val KEY_THREAD_ID = "thread_id" private const val KEY_THREAD_ID = "thread_id"
private const val KEY_CONVERSATION_RECIPIENT_ID = "conversation_recipient_id" private const val KEY_CONVERSATION_RECIPIENT_ID = "conversation_recipient_id"
private const val KEY_CAN_UNPIN = "can_unpin"
@JvmStatic @JvmStatic
fun show(fragmentManager: FragmentManager, threadId: Long, conversationRecipientId: RecipientId) { fun show(fragmentManager: FragmentManager, threadId: Long, conversationRecipientId: RecipientId, canUnpin: Boolean) {
val args = Bundle().apply { val args = Bundle().apply {
putLong(KEY_THREAD_ID, threadId) putLong(KEY_THREAD_ID, threadId)
putString(KEY_CONVERSATION_RECIPIENT_ID, conversationRecipientId.serialize()) putString(KEY_CONVERSATION_RECIPIENT_ID, conversationRecipientId.serialize())
putBoolean(KEY_CAN_UNPIN, canUnpin)
} }
val fragment = PinnedMessagesBottomSheet().apply { val fragment = PinnedMessagesBottomSheet().apply {

View File

@@ -133,7 +133,7 @@ class ConversationBannerView @JvmOverloads constructor(
hide(voiceNotePlayerStub) hide(voiceNotePlayerStub)
} }
fun showPinnedMessageStub(messages: List<ConversationMessage>) { fun showPinnedMessageStub(messages: List<ConversationMessage>, canUnpin: Boolean) {
show( show(
stub = pinnedMessageStub stub = pinnedMessageStub
) { ) {
@@ -142,6 +142,7 @@ class ConversationBannerView @JvmOverloads constructor(
SignalTheme(isDarkMode = DynamicTheme.isDarkTheme(context)) { SignalTheme(isDarkMode = DynamicTheme.isDarkTheme(context)) {
PinnedMessagesBanner( PinnedMessagesBanner(
messages = messages, messages = messages,
canUnpin = canUnpin,
onUnpinMessage = { messageId -> listener?.onUnpinMessage(messageId) }, onUnpinMessage = { messageId -> listener?.onUnpinMessage(messageId) },
onGoToMessage = { messageId -> listener?.onGoToMessage(messageId) }, onGoToMessage = { messageId -> listener?.onGoToMessage(messageId) },
onViewAllMessages = { listener?.onViewAllMessages() } onViewAllMessages = { listener?.onViewAllMessages() }

View File

@@ -1336,7 +1336,7 @@ class ConversationFragment :
private fun presentPinnedMessage(pinnedMessages: List<ConversationMessage>) { private fun presentPinnedMessage(pinnedMessages: List<ConversationMessage>) {
if (pinnedMessages.isNotEmpty()) { if (pinnedMessages.isNotEmpty()) {
binding.conversationBanner.showPinnedMessageStub(pinnedMessages) binding.conversationBanner.showPinnedMessageStub(messages = pinnedMessages, canUnpin = conversationGroupViewModel.canEditGroupInfo())
} else { } else {
binding.conversationBanner.hidePinnedMessageStub() binding.conversationBanner.hidePinnedMessageStub()
} }
@@ -2311,7 +2311,8 @@ class ConversationFragment :
recipient, recipient,
selectedParts, selectedParts,
viewModel.hasMessageRequestState, viewModel.hasMessageRequestState,
conversationGroupViewModel.isNonAdminInAnnouncementGroup() conversationGroupViewModel.isNonAdminInAnnouncementGroup(),
conversationGroupViewModel.canEditGroupInfo()
) )
val items = arrayListOf<ActionItem>() val items = arrayListOf<ActionItem>()
@@ -2458,7 +2459,7 @@ class ConversationFragment :
) { ) {
reactionDelegate.setOnActionSelectedListener(onActionSelectedListener) reactionDelegate.setOnActionSelectedListener(onActionSelectedListener)
reactionDelegate.setOnHideListener(onHideListener) reactionDelegate.setOnHideListener(onHideListener)
reactionDelegate.show(requireActivity(), viewModel.recipientSnapshot!!, conversationMessage, conversationGroupViewModel.isNonAdminInAnnouncementGroup(), selectedConversationModel) reactionDelegate.show(requireActivity(), viewModel.recipientSnapshot!!, conversationMessage, conversationGroupViewModel.isNonAdminInAnnouncementGroup(), selectedConversationModel, conversationGroupViewModel.canEditGroupInfo())
viewModel.setIsReactionDelegateShowing(true) viewModel.setIsReactionDelegateShowing(true)
composeText.clearFocus() composeText.clearFocus()
} }
@@ -4232,8 +4233,9 @@ class ConversationFragment :
override fun onViewAllMessages() { override fun onViewAllMessages() {
PinnedMessagesBottomSheet.show( PinnedMessagesBottomSheet.show(
childFragmentManager, childFragmentManager,
args.threadId, threadId = args.threadId,
viewModel.recipientSnapshot?.id!! conversationRecipientId = viewModel.recipientSnapshot?.id!!,
canUnpin = conversationGroupViewModel.canEditGroupInfo()
) )
} }
} }

View File

@@ -63,6 +63,7 @@ import kotlin.jvm.optionals.getOrDefault
@Composable @Composable
fun PinnedMessagesBanner( fun PinnedMessagesBanner(
messages: List<ConversationMessage> = emptyList(), messages: List<ConversationMessage> = emptyList(),
canUnpin: Boolean,
onUnpinMessage: (Long) -> Unit = {}, onUnpinMessage: (Long) -> Unit = {},
onGoToMessage: (Long) -> Unit = {}, onGoToMessage: (Long) -> Unit = {},
onViewAllMessages: () -> Unit = {} onViewAllMessages: () -> Unit = {}
@@ -150,7 +151,9 @@ fun PinnedMessagesBanner(
DropdownMenus.Menu(controller = menuController, offsetX = 2.dp, offsetY = 16.dp) { menuController -> DropdownMenus.Menu(controller = menuController, offsetX = 2.dp, offsetY = 16.dp) { menuController ->
Column { Column {
if (canUnpin) {
DropdownMenus.ItemWithIcon(menuController, R.drawable.symbol_pin_slash_24, R.string.PinnedMessage__unpin_message) { onUnpinMessage(message.id) } DropdownMenus.ItemWithIcon(menuController, R.drawable.symbol_pin_slash_24, R.string.PinnedMessage__unpin_message) { onUnpinMessage(message.id) }
}
DropdownMenus.ItemWithIcon(menuController, R.drawable.symbol_chat_24, R.string.PinnedMessage__go_to_message) { onGoToMessage(message.id) } DropdownMenus.ItemWithIcon(menuController, R.drawable.symbol_chat_24, R.string.PinnedMessage__go_to_message) { onGoToMessage(message.id) }
DropdownMenus.ItemWithIcon(menuController, R.drawable.symbol_list_bullet_24, R.string.PinnedMessage__view_all_messages) { onViewAllMessages() } DropdownMenus.ItemWithIcon(menuController, R.drawable.symbol_list_bullet_24, R.string.PinnedMessage__view_all_messages) { onViewAllMessages() }
} }

View File

@@ -8,5 +8,6 @@ import org.thoughtcrime.securesms.database.GroupTable
*/ */
data class ConversationGroupMemberLevel( data class ConversationGroupMemberLevel(
val groupTableMemberLevel: GroupTable.MemberLevel, val groupTableMemberLevel: GroupTable.MemberLevel,
val isAnnouncementGroup: Boolean val isAnnouncementGroup: Boolean,
val allMembersCanEditGroupInfo: Boolean
) )

View File

@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.conversation.v2.ConversationRecipientRepositor
import org.thoughtcrime.securesms.database.GroupTable import org.thoughtcrime.securesms.database.GroupTable
import org.thoughtcrime.securesms.database.model.GroupRecord import org.thoughtcrime.securesms.database.model.GroupRecord
import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.groups.GroupAccessControl
import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
import org.thoughtcrime.securesms.groups.v2.GroupBlockJoinRequestResult import org.thoughtcrime.securesms.groups.v2.GroupBlockJoinRequestResult
@@ -53,7 +54,7 @@ class ConversationGroupViewModel(
init { init {
disposables += _groupRecord.subscribe { groupRecord -> disposables += _groupRecord.subscribe { groupRecord ->
_groupActiveState.onNext(ConversationGroupActiveState(groupRecord.isActive, groupRecord.isV2Group)) _groupActiveState.onNext(ConversationGroupActiveState(groupRecord.isActive, groupRecord.isV2Group))
_memberLevel.onNext(ConversationGroupMemberLevel(groupRecord.memberLevel(Recipient.self()), groupRecord.isAnnouncementGroup)) _memberLevel.onNext(ConversationGroupMemberLevel(groupRecord.memberLevel(Recipient.self()), groupRecord.isAnnouncementGroup, groupRecord.attributesAccessControl == GroupAccessControl.ALL_MEMBERS))
} }
} }
@@ -66,6 +67,11 @@ class ConversationGroupViewModel(
return memberLevel.groupTableMemberLevel != GroupTable.MemberLevel.ADMINISTRATOR && memberLevel.isAnnouncementGroup return memberLevel.groupTableMemberLevel != GroupTable.MemberLevel.ADMINISTRATOR && memberLevel.isAnnouncementGroup
} }
fun canEditGroupInfo(): Boolean {
val memberLevel = _memberLevel.value ?: return true
return memberLevel.groupTableMemberLevel == GroupTable.MemberLevel.ADMINISTRATOR || memberLevel.allMembersCanEditGroupInfo
}
fun blockJoinRequests(recipient: Recipient): Single<GroupBlockJoinRequestResult> { fun blockJoinRequests(recipient: Recipient): Single<GroupBlockJoinRequestResult> {
return _groupRecord return _groupRecord
.firstOrError() .firstOrError()

View File

@@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.database.model.GroupRecord;
import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.groups.GroupAccessControl;
import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobLogger; import org.thoughtcrime.securesms.jobmanager.JobLogger;
@@ -348,7 +349,9 @@ public final class PushGroupSendJob extends PushSendJob {
} else { } else {
Optional<GroupRecord> groupRecord = SignalDatabase.groups().getGroup(groupRecipient.requireGroupId()); Optional<GroupRecord> groupRecord = SignalDatabase.groups().getGroup(groupRecipient.requireGroupId());
if (groupRecord.isPresent() && groupRecord.get().isAnnouncementGroup() && !groupRecord.get().isAdmin(Recipient.self())) { if (pinnedMessage != null && groupRecord.isPresent() && groupRecord.get().getAttributesAccessControl() == GroupAccessControl.ONLY_ADMINS && !groupRecord.get().isAdmin(Recipient.self())) {
throw new UndeliverableMessageException("Non-admins cannot pin messages in this group!");
} else if (pinnedMessage == null && groupRecord.isPresent() && groupRecord.get().isAnnouncementGroup() && !groupRecord.get().isAdmin(Recipient.self())) {
throw new UndeliverableMessageException("Non-admins cannot send messages in announcement groups!"); throw new UndeliverableMessageException("Non-admins cannot send messages in announcement groups!");
} }

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.jobs
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.groups.GroupAccessControl
import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
@@ -10,6 +11,7 @@ import org.thoughtcrime.securesms.jobs.protos.UnpinJobData
import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.messages.GroupSendUtil import org.thoughtcrime.securesms.messages.GroupSendUtil
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.Recipient.Companion.self
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.recipients.RecipientUtil import org.thoughtcrime.securesms.recipients.RecipientUtil
import org.thoughtcrime.securesms.util.GroupUtil import org.thoughtcrime.securesms.util.GroupUtil
@@ -97,6 +99,14 @@ class UnpinMessageJob(
return Result.failure() return Result.failure()
} }
if (conversationRecipient.isPushV2Group) {
val groupRecord = SignalDatabase.groups.getGroup(conversationRecipient.id)
if (groupRecord.isPresent && groupRecord.get().attributesAccessControl == GroupAccessControl.ONLY_ADMINS && !groupRecord.get().isAdmin(self())) {
Log.w(TAG, "Non-admins cannot send unpin messages to group.")
return Result.failure()
}
}
val targetSentTimestamp = message.dateSent val targetSentTimestamp = message.dateSent
val recipients = Recipient.resolvedList(recipientIds.filter { it != Recipient.self().id.toLong() }.map { RecipientId.from(it) }) val recipients = Recipient.resolvedList(recipientIds.filter { it != Recipient.self().id.toLong() }.map { RecipientId.from(it) })

View File

@@ -48,6 +48,7 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.PollTerminate
import org.thoughtcrime.securesms.database.model.toBodyRangeList import org.thoughtcrime.securesms.database.model.toBodyRangeList
import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.groups.BadGroupIdException import org.thoughtcrime.securesms.groups.BadGroupIdException
import org.thoughtcrime.securesms.groups.GroupAccessControl
import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob
@@ -1305,6 +1306,10 @@ object DataMessageProcessor {
warn(envelope.timestamp!!, "[handlePinMessage] Sender is not in the group! timestamp: ${pinMessage.targetSentTimestamp}") warn(envelope.timestamp!!, "[handlePinMessage] Sender is not in the group! timestamp: ${pinMessage.targetSentTimestamp}")
return null return null
} }
if (groupRecord != null && groupRecord.attributesAccessControl == GroupAccessControl.ONLY_ADMINS && !groupRecord.isAdmin(senderRecipient)) {
warn(envelope.timestamp!!, "[handlePinMessage] Sender needs to be an admin! timestamp: ${pinMessage.targetSentTimestamp}")
return null
}
if (groupRecord == null && senderRecipient.id != threadRecipient.id && Recipient.self().id != senderRecipient.id) { if (groupRecord == null && senderRecipient.id != threadRecipient.id && Recipient.self().id != senderRecipient.id) {
warn(envelope.timestamp!!, "[handlePinMessage] Sender is not a part of the 1:1 thread! timestamp: ${pinMessage.targetSentTimestamp}") warn(envelope.timestamp!!, "[handlePinMessage] Sender is not a part of the 1:1 thread! timestamp: ${pinMessage.targetSentTimestamp}")
@@ -1384,6 +1389,10 @@ object DataMessageProcessor {
warn(envelope.timestamp!!, "[handleUnpinMessage] Sender is not in the group! timestamp: ${unpinMessage.targetSentTimestamp}") warn(envelope.timestamp!!, "[handleUnpinMessage] Sender is not in the group! timestamp: ${unpinMessage.targetSentTimestamp}")
return null return null
} }
if (groupRecord != null && groupRecord.attributesAccessControl == GroupAccessControl.ONLY_ADMINS && !groupRecord.isAdmin(senderRecipient)) {
warn(envelope.timestamp!!, "[handleUnpinMessage] Sender needs to be an admin! timestamp: ${unpinMessage.targetSentTimestamp}")
return null
}
if (groupRecord == null && senderRecipient.id != threadRecipient.id && Recipient.self().id != senderRecipient.id) { if (groupRecord == null && senderRecipient.id != threadRecipient.id && Recipient.self().id != senderRecipient.id) {
warn(envelope.timestamp!!, "[handleUnpinMessage] Sender is not a part of the 1:1 thread! timestamp: ${unpinMessage.targetSentTimestamp}") warn(envelope.timestamp!!, "[handleUnpinMessage] Sender is not a part of the 1:1 thread! timestamp: ${unpinMessage.targetSentTimestamp}")