Merge pull request #35 from Bilb/fix-groups-issues-5

fix: more x4 fixes for group
pull/3281/head
Audric Ackermann 4 months ago committed by GitHub
commit 811d334e5a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -41,7 +41,11 @@ const Container = styled.div<{ noExtraPadding: boolean }>`
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
background-color: var(--background-secondary-color); background-color: var(--background-secondary-color);
// add padding only if we have a child.
&:has(*:not(:empty)) {
padding: ${props => (props.noExtraPadding ? '' : 'var(--margins-lg)')}; padding: ${props => (props.noExtraPadding ? '' : 'var(--margins-lg)')};
}
`; `;
const TextInner = styled.div` const TextInner = styled.div`

@ -12,6 +12,7 @@ import { renderEmojiQuickResultRow, searchEmojiForQuery } from './EmojiQuickResu
import { renderUserMentionRow, styleForCompositionBoxSuggestions } from './UserMentions'; import { renderUserMentionRow, styleForCompositionBoxSuggestions } from './UserMentions';
import { HTMLDirection, useHTMLDirection } from '../../../util/i18n/rtlSupport'; import { HTMLDirection, useHTMLDirection } from '../../../util/i18n/rtlSupport';
import { ConvoHub } from '../../../session/conversations'; import { ConvoHub } from '../../../session/conversations';
import { Constants } from '../../../session';
const sendMessageStyle = (dir?: HTMLDirection) => { const sendMessageStyle = (dir?: HTMLDirection) => {
return { return {
@ -87,7 +88,10 @@ export const CompositionTextArea = (props: Props) => {
throw new Error('selectedConversationKey is needed'); throw new Error('selectedConversationKey is needed');
} }
const newDraft = event.target.value ?? ''; const newDraft = (event.target.value ?? '').slice(
0,
Constants.CONVERSATION.MAX_MESSAGE_CHAR_COUNT
);
setDraft(newDraft); setDraft(newDraft);
updateDraftForConversation({ conversationKey: selectedConversationKey, draft: newDraft }); updateDraftForConversation({ conversationKey: selectedConversationKey, draft: newDraft });
}; };
@ -121,6 +125,7 @@ export const CompositionTextArea = (props: Props) => {
spellCheck={true} spellCheck={true}
dir={htmlDirection} dir={htmlDirection}
inputRef={textAreaRef} inputRef={textAreaRef}
maxLength={Constants.CONVERSATION.MAX_MESSAGE_CHAR_COUNT}
disabled={!typingEnabled} disabled={!typingEnabled}
rows={1} rows={1}
data-testid="message-input-text-area" data-testid="message-input-text-area"

@ -811,11 +811,20 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
networkTimestamp networkTimestamp
); );
const attachmentsWithVoiceMessage = attachments
? attachments.map(attachment => {
if (attachment.isVoiceMessage) {
return { ...attachment, flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE };
}
return attachment;
})
: undefined;
const messageModel = await this.addSingleOutgoingMessage({ const messageModel = await this.addSingleOutgoingMessage({
body, body,
quote: isEmpty(quote) ? undefined : quote, quote: isEmpty(quote) ? undefined : quote,
preview, preview,
attachments, attachments: attachmentsWithVoiceMessage,
sent_at: networkTimestamp, // overridden later, but we need one to have the sorting done in the UI even when the sending is pending sent_at: networkTimestamp, // overridden later, but we need one to have the sorting done in the UI even when the sending is pending
expirationType: DisappearingMessages.changeToDisappearingMessageType( expirationType: DisappearingMessages.changeToDisappearingMessageType(
this, this,

@ -727,11 +727,14 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
thumbnail, thumbnail,
fileName, fileName,
caption, caption,
isVoiceMessage: isVoiceMessageFromDb,
} = attachment; } = attachment;
const isVoiceMessageBool = const isVoiceMessageBool =
!!isVoiceMessageFromDb ||
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
Boolean(flags && flags & SignalService.AttachmentPointer.Flags.VOICE_MESSAGE) || false; !!(flags && flags & SignalService.AttachmentPointer.Flags.VOICE_MESSAGE) ||
false;
return { return {
id, id,

@ -22,6 +22,7 @@ import {
last, last,
map, map,
omit, omit,
some,
uniq, uniq,
} from 'lodash'; } from 'lodash';
@ -1107,7 +1108,7 @@ async function getAllMessagesWithAttachmentsInConversationSentBefore(
.all({ conversationId, beforeMs: deleteAttachBeforeSeconds * 1000 }); .all({ conversationId, beforeMs: deleteAttachBeforeSeconds * 1000 });
const messages = map(rows, row => jsonToObject(row.json)); const messages = map(rows, row => jsonToObject(row.json));
const messagesWithAttachments = messages.filter(m => { const messagesWithAttachments = messages.filter(m => {
return getExternalFilesForMessage(m, false).some(a => !isEmpty(a) && isString(a)); // when we remove an attachment, we set the path to '' so it should be excluded here return hasUserVisibleAttachments(m);
}); });
return messagesWithAttachments; return messagesWithAttachments;
} }
@ -2092,7 +2093,7 @@ function getMessagesWithFileAttachments(conversationId: string, limit: number) {
return map(rows, row => jsonToObject(row.json)); return map(rows, row => jsonToObject(row.json));
} }
function getExternalFilesForMessage(message: any, includePreview = true) { function getExternalFilesForMessage(message: any) {
const { attachments, quote, preview } = message; const { attachments, quote, preview } = message;
const files: Array<string> = []; const files: Array<string> = [];
@ -2110,7 +2111,6 @@ function getExternalFilesForMessage(message: any, includePreview = true) {
files.push(screenshot.path); files.push(screenshot.path);
} }
}); });
if (quote && quote.attachments && quote.attachments.length) { if (quote && quote.attachments && quote.attachments.length) {
forEach(quote.attachments, attachment => { forEach(quote.attachments, attachment => {
const { thumbnail } = attachment; const { thumbnail } = attachment;
@ -2121,7 +2121,7 @@ function getExternalFilesForMessage(message: any, includePreview = true) {
}); });
} }
if (includePreview && preview && preview.length) { if (preview && preview.length) {
forEach(preview, item => { forEach(preview, item => {
const { image } = item; const { image } = item;
@ -2134,6 +2134,30 @@ function getExternalFilesForMessage(message: any, includePreview = true) {
return files; return files;
} }
/**
* This looks like `getExternalFilesForMessage`, but it does not include some type of attachments not visible from the right panel.
* It should only be used when we look for messages to mark as deleted when an admin
* triggers a "delete messages with attachments since".
* Note: quoted attachments are referencing the original message, so we don't need to include them here.
* Note: previews are not considered user visible (because not visible from the right panel),
* so we don't need to include them here
* Note: voice messages are not considered user visible (because not visible from the right panel),
*/
function hasUserVisibleAttachments(message: any) {
const { attachments } = message;
return some(attachments, attachment => {
const { path: file, flags, thumbnail, screenshot } = attachment;
return (
// eslint-disable-next-line no-bitwise
(file && !(flags & SignalService.AttachmentPointer.Flags.VOICE_MESSAGE)) ||
thumbnail?.path ||
screenshot?.path
);
});
}
function getExternalFilesForConversation( function getExternalFilesForConversation(
conversationAvatar: conversationAvatar:
| string | string

@ -725,7 +725,8 @@ async function handleSingleGroupUpdate({
); );
} }
if (!ConvoHub.use().get(groupPk)) { const convoExisting = ConvoHub.use().get(groupPk);
if (!convoExisting) {
const created = await ConvoHub.use().getOrCreateAndWait(groupPk, ConversationTypeEnum.GROUPV2); const created = await ConvoHub.use().getOrCreateAndWait(groupPk, ConversationTypeEnum.GROUPV2);
const joinedAt = const joinedAt =
groupInWrapper.joinedAtSeconds * 1000 || CONVERSATION.LAST_JOINED_FALLBACK_TIMESTAMP; groupInWrapper.joinedAtSeconds * 1000 || CONVERSATION.LAST_JOINED_FALLBACK_TIMESTAMP;
@ -743,6 +744,12 @@ async function handleSingleGroupUpdate({
}); });
await created.commit(); await created.commit();
getSwarmPollingInstance().addGroupId(PubKey.cast(groupPk)); getSwarmPollingInstance().addGroupId(PubKey.cast(groupPk));
} else {
// Note: the priority is the only **field** we want to sync from our user group wrapper for 03-groups
const changes = await convoExisting.setPriorityFromWrapper(groupInWrapper.priority, false);
if (changes) {
await convoExisting.commit();
}
} }
} }

@ -1,5 +1,5 @@
import { UserGroupsGet } from 'libsession_util_nodejs'; import { UserGroupsGet } from 'libsession_util_nodejs';
import { compact, isEmpty } from 'lodash'; import { compact, isEmpty, uniqBy } from 'lodash';
import { SignalService } from '../../../../protobuf'; import { SignalService } from '../../../../protobuf';
import { MetaGroupWrapperActions } from '../../../../webworker/workers/browser/libsession_worker_interface'; import { MetaGroupWrapperActions } from '../../../../webworker/workers/browser/libsession_worker_interface';
import { GroupUpdateInfoChangeMessage } from '../../../messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateInfoChangeMessage'; import { GroupUpdateInfoChangeMessage } from '../../../messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateInfoChangeMessage';
@ -40,6 +40,13 @@ async function makeGroupMessageSubRequest(
throw new Error('makeGroupMessageSubRequest: not all messages are for the same destination'); throw new Error('makeGroupMessageSubRequest: not all messages are for the same destination');
} }
const allTimestamps = uniqBy(compactedMessages, m => m.createAtNetworkTimestamp);
if (allTimestamps.length !== compactedMessages.length) {
throw new Error(
'tried to send batch request with messages having the same timestamp, this is not supported on all platforms.'
);
}
const messagesToEncrypt: Array<StoreGroupExtraData> = compactedMessages.map(updateMessage => { const messagesToEncrypt: Array<StoreGroupExtraData> = compactedMessages.map(updateMessage => {
const wrapped = MessageWrapper.wrapContentIntoEnvelope( const wrapped = MessageWrapper.wrapContentIntoEnvelope(
SignalService.Envelope.Type.CLOSED_GROUP_MESSAGE, SignalService.Envelope.Type.CLOSED_GROUP_MESSAGE,

@ -13,6 +13,7 @@ import {
last, last,
omit, omit,
sample, sample,
sampleSize,
toNumber, toNumber,
uniqBy, uniqBy,
} from 'lodash'; } from 'lodash';
@ -53,12 +54,19 @@ import {
RetrieveMessageItem, RetrieveMessageItem,
RetrieveMessageItemWithNamespace, RetrieveMessageItemWithNamespace,
RetrieveMessagesResultsBatched, RetrieveMessagesResultsBatched,
RetrieveRequestResult, type RetrieveMessagesResultsMergedBatched,
} from './types'; } from './types';
import { ConversationTypeEnum } from '../../../models/types'; import { ConversationTypeEnum } from '../../../models/types';
import { Snode } from '../../../data/types'; import { Snode } from '../../../data/types';
const minMsgCountShouldRetry = 95; const minMsgCountShouldRetry = 95;
/**
* We retrieve from multiple snodes at the same time, and merge their reported messages because it's easy
* for a snode to be out of sync.
* Sometimes, being out of sync means that we won't be able to retrieve a message at all (revoked_subaccount).
* We need a proper fix server side, but in the meantime, that's all we can do.
*/
const RETRIEVE_SNODES_COUNT = 2;
function extractWebSocketContent( function extractWebSocketContent(
message: string, message: string,
@ -105,6 +113,33 @@ function entryToKey(entry: GroupPollingEntry) {
return entry.pubkey.key; return entry.pubkey.key;
} }
function mergeMultipleRetrieveResults(
results: RetrieveMessagesResultsBatched
): RetrieveMessagesResultsMergedBatched {
const mapped: Map<SnodeNamespaces, Map<string, RetrieveMessageItem>> = new Map();
for (let resultIndex = 0; resultIndex < results.length; resultIndex++) {
const result = results[resultIndex];
if (!mapped.has(result.namespace)) {
mapped.set(result.namespace, new Map());
}
if (result.messages.messages) {
for (let msgIndex = 0; msgIndex < result.messages.messages.length; msgIndex++) {
const msg = result.messages.messages[msgIndex];
if (!mapped.get(result.namespace)!.has(msg.hash)) {
mapped.get(result.namespace)!.set(msg.hash, msg);
}
}
}
}
// Convert the merged map back to an array
return Array.from(mapped.entries()).map(([namespace, messagesMap]) => ({
code: results.find(m => m.namespace === namespace)?.code || 200,
namespace,
messages: { messages: Array.from(messagesMap.values()) },
}));
}
export class SwarmPolling { export class SwarmPolling {
private groupPolling: Array<GroupPollingEntry>; private groupPolling: Array<GroupPollingEntry>;
@ -414,27 +449,40 @@ export class SwarmPolling {
public async pollOnceForKey([pubkey, type]: PollForUs | PollForLegacy | PollForGroup) { public async pollOnceForKey([pubkey, type]: PollForUs | PollForLegacy | PollForGroup) {
const namespaces = this.getNamespacesToPollFrom(type); const namespaces = this.getNamespacesToPollFrom(type);
const swarmSnodes = await SnodePool.getSwarmFor(pubkey); const swarmSnodes = await SnodePool.getSwarmFor(pubkey);
let resultsFromAllNamespaces: RetrieveMessagesResultsBatched | null; let resultsFromAllNamespaces: RetrieveMessagesResultsMergedBatched | null;
let toPollFrom: Snode | undefined; let toPollFrom: Array<Snode> = [];
try { try {
toPollFrom = sample(swarmSnodes); toPollFrom = sampleSize(swarmSnodes, RETRIEVE_SNODES_COUNT);
if (!toPollFrom) { if (toPollFrom.length !== RETRIEVE_SNODES_COUNT) {
throw new Error( throw new Error(
`SwarmPolling: pollOnceForKey: no snode in swarm for ${ed25519Str(pubkey)}` `SwarmPolling: pollOnceForKey: not snodes in swarm for ${ed25519Str(pubkey)}. Expected to have at least ${RETRIEVE_SNODES_COUNT}.`
); );
} }
const resultsFromAllSnodesSettled = await Promise.allSettled(
toPollFrom.map(async snode => {
// Note: always print something so we know if the polling is hanging // Note: always print something so we know if the polling is hanging
window.log.info( window.log.info(
`SwarmPolling: about to pollNodeForKey of ${ed25519Str(pubkey)} from snode: ${ed25519Str(toPollFrom.pubkey_ed25519)} namespaces: ${namespaces} ` `SwarmPolling: about to pollNodeForKey of ${ed25519Str(pubkey)} from snode: ${ed25519Str(snode.pubkey_ed25519)} namespaces: ${namespaces} `
); );
resultsFromAllNamespaces = await this.pollNodeForKey(toPollFrom, pubkey, namespaces, type); const thisSnodeResults = await this.pollNodeForKey(snode, pubkey, namespaces, type);
// Note: always print something so we know if the polling is hanging // Note: always print something so we know if the polling is hanging
window.log.info( window.log.info(
`SwarmPolling: pollNodeForKey of ${ed25519Str(pubkey)} from snode: ${ed25519Str(toPollFrom.pubkey_ed25519)} namespaces: ${namespaces} returned: ${resultsFromAllNamespaces?.length}` `SwarmPolling: pollNodeForKey of ${ed25519Str(pubkey)} from snode: ${ed25519Str(snode.pubkey_ed25519)} namespaces: ${namespaces} returned: ${thisSnodeResults?.length}`
);
return thisSnodeResults;
})
);
window.log.info(
`SwarmPolling: pollNodeForKey of ${ed25519Str(pubkey)} namespaces: ${namespaces} returned ${resultsFromAllSnodesSettled.filter(m => m.status === 'fulfilled').length}/${RETRIEVE_SNODES_COUNT} fulfilled promises`
);
resultsFromAllNamespaces = mergeMultipleRetrieveResults(
compact(
resultsFromAllSnodesSettled.filter(m => m.status === 'fulfilled').flatMap(m => m.value)
)
); );
} catch (e) { } catch (e) {
window.log.warn( window.log.warn(
@ -490,7 +538,7 @@ export class SwarmPolling {
const newMessages = await this.handleSeenMessages(uniqOtherMsgs); const newMessages = await this.handleSeenMessages(uniqOtherMsgs);
window.log.info( window.log.info(
`SwarmPolling: handleSeenMessages: ${newMessages.length} out of ${uniqOtherMsgs.length} are not seen yet about pk:${ed25519Str(pubkey)} snode: ${toPollFrom ? ed25519Str(toPollFrom.pubkey_ed25519) : 'undefined'}` `SwarmPolling: handleSeenMessages: ${newMessages.length} out of ${uniqOtherMsgs.length} are not seen yet about pk:${ed25519Str(pubkey)} snode: ${JSON.stringify(toPollFrom.map(m => ed25519Str(m.pubkey_ed25519)))}`
); );
if (type === ConversationTypeEnum.GROUPV2) { if (type === ConversationTypeEnum.GROUPV2) {
if (!PubKey.is03Pubkey(pubkey)) { if (!PubKey.is03Pubkey(pubkey)) {
@ -975,7 +1023,7 @@ const retrieveItemSchema = z.object({
}); });
function retrieveItemWithNamespace( function retrieveItemWithNamespace(
results: Array<RetrieveRequestResult> results: RetrieveMessagesResultsMergedBatched
): Array<RetrieveMessageItemWithNamespace> { ): Array<RetrieveMessageItemWithNamespace> {
return flatten( return flatten(
compact( compact(
@ -996,7 +1044,7 @@ function retrieveItemWithNamespace(
function filterMessagesPerTypeOfConvo<T extends ConversationTypeEnum>( function filterMessagesPerTypeOfConvo<T extends ConversationTypeEnum>(
type: T, type: T,
retrieveResults: RetrieveMessagesResultsBatched retrieveResults: RetrieveMessagesResultsMergedBatched
): { ): {
confMessages: Array<RetrieveMessageItemWithNamespace> | null; confMessages: Array<RetrieveMessageItemWithNamespace> | null;
revokedMessages: Array<RetrieveMessageItemWithNamespace> | null; revokedMessages: Array<RetrieveMessageItemWithNamespace> | null;

@ -34,12 +34,21 @@ export type RetrieveMessagesResultsContent = {
t: number; t: number;
}; };
export type RetrieveRequestResult = { type RetrieveMessagesResultsContentMerged = Pick<RetrieveMessagesResultsContent, 'messages'>;
type RetrieveRequestResult<
T extends RetrieveMessagesResultsContent | RetrieveMessagesResultsContentMerged,
> = {
code: number; code: number;
messages: RetrieveMessagesResultsContent; messages: T;
namespace: SnodeNamespaces; namespace: SnodeNamespaces;
}; };
export type RetrieveMessagesResultsBatched = Array<RetrieveRequestResult>; export type RetrieveMessagesResultsBatched = Array<
RetrieveRequestResult<RetrieveMessagesResultsContent>
>;
export type RetrieveMessagesResultsMergedBatched = Array<
RetrieveRequestResult<RetrieveMessagesResultsContentMerged>
>;
export type WithRevokeSubRequest = { export type WithRevokeSubRequest = {
revokeSubRequest?: SubaccountRevokeSubRequest; revokeSubRequest?: SubaccountRevokeSubRequest;

@ -77,6 +77,11 @@ export const CONVERSATION = {
MAX_GLOBAL_UNREAD_COUNT: 99, // the global one does not look good with 4 digits (999+) so we have a smaller one for it MAX_GLOBAL_UNREAD_COUNT: 99, // the global one does not look good with 4 digits (999+) so we have a smaller one for it
/** NOTE some existing groups might not have joinedAtSeconds and we need a fallback value that is not falsy in order to poll and show up in the conversations list */ /** NOTE some existing groups might not have joinedAtSeconds and we need a fallback value that is not falsy in order to poll and show up in the conversations list */
LAST_JOINED_FALLBACK_TIMESTAMP: 1, LAST_JOINED_FALLBACK_TIMESTAMP: 1,
/**
* the maximum chars that can be typed/pasted in the composition box.
* Same as android.
*/
MAX_MESSAGE_CHAR_COUNT: 2000,
} as const; } as const;
/** /**

@ -695,7 +695,7 @@ async function leaveClosedGroup(groupPk: PubkeyType | GroupPubkeyType, fromSyncM
const createAtNetworkTimestamp = NetworkTime.now(); const createAtNetworkTimestamp = NetworkTime.now();
// Send the update to the 03 group // Send the update to the 03 group
const ourLeavingMessage = new GroupUpdateMemberLeftMessage({ const ourLeavingMessage = new GroupUpdateMemberLeftMessage({
createAtNetworkTimestamp, createAtNetworkTimestamp: createAtNetworkTimestamp + 1, // we just need it to be different than the one of ourLeavingNotificationMessage
groupPk, groupPk,
expirationType: null, // we keep that one **not** expiring expirationType: null, // we keep that one **not** expiring
expireTimer: null, expireTimer: null,

@ -4,7 +4,7 @@ import { MessageEncrypter } from '../crypto/MessageEncrypter';
import { PubKey } from '../types'; import { PubKey } from '../types';
function encryptionBasedOnConversation(destination: PubKey) { function encryptionBasedOnConversation(destination: PubKey) {
if (ConvoHub.use().get(destination.key)?.isClosedGroup()) { if (PubKey.is03Pubkey(destination.key) || ConvoHub.use().get(destination.key)?.isClosedGroup()) {
return SignalService.Envelope.Type.CLOSED_GROUP_MESSAGE; return SignalService.Envelope.Type.CLOSED_GROUP_MESSAGE;
} }
return SignalService.Envelope.Type.SESSION_MESSAGE; return SignalService.Envelope.Type.SESSION_MESSAGE;

@ -41,8 +41,9 @@ export async function toRawMessage(
): Promise<OutgoingRawMessage> { ): Promise<OutgoingRawMessage> {
const ttl = message.ttl(); const ttl = message.ttl();
const plainTextBuffer = message.plainTextBuffer(); const plainTextBuffer = message.plainTextBuffer();
const is03group = PubKey.is03Pubkey(destinationPubKey.key);
const encryption = getEncryptionTypeFromMessageType(message, isGroup); const encryption = getEncryptionTypeFromMessageType(message, isGroup || is03group);
const rawMessage: OutgoingRawMessage = { const rawMessage: OutgoingRawMessage = {
identifier: message.identifier, identifier: message.identifier,

@ -17,7 +17,7 @@ import {
} from './conversations'; } from './conversations';
import { getLibMembersPubkeys, useLibGroupName } from './groups'; import { getLibMembersPubkeys, useLibGroupName } from './groups';
import { getCanWrite, getModerators, getSubscriberCount } from './sogsRoomInfo'; import { getCanWrite, getModerators, getSubscriberCount } from './sogsRoomInfo';
import { getLibGroupDestroyed, useLibGroupDestroyed } from './userGroups'; import { getLibGroupDestroyed, getLibGroupKicked, useLibGroupDestroyed } from './userGroups';
const getIsSelectedPrivate = (state: StateType): boolean => { const getIsSelectedPrivate = (state: StateType): boolean => {
return Boolean(getSelectedConversation(state)?.isPrivate) || false; return Boolean(getSelectedConversation(state)?.isPrivate) || false;
@ -59,6 +59,7 @@ export const getSelectedConversationIsPublic = (state: StateType): boolean => {
export function getSelectedCanWrite(state: StateType) { export function getSelectedCanWrite(state: StateType) {
const selectedConvoPubkey = getSelectedConversationKey(state); const selectedConvoPubkey = getSelectedConversationKey(state);
const isSelectedGroupDestroyed = getLibGroupDestroyed(state, selectedConvoPubkey); const isSelectedGroupDestroyed = getLibGroupDestroyed(state, selectedConvoPubkey);
const isSelectedGroupKicked = getLibGroupKicked(state, selectedConvoPubkey);
if (!selectedConvoPubkey) { if (!selectedConvoPubkey) {
return false; return false;
} }
@ -76,6 +77,7 @@ export function getSelectedCanWrite(state: StateType) {
return !( return !(
isBlocked || isBlocked ||
isKickedFromGroup || isKickedFromGroup ||
isSelectedGroupKicked ||
isSelectedGroupDestroyed || isSelectedGroupDestroyed ||
readOnlySogs || readOnlySogs ||
isBlindedAndDisabledMsgRequests isBlindedAndDisabledMsgRequests

@ -26,7 +26,7 @@ export function useLibGroupInviteGroupName(convoId?: string) {
return useSelector((state: StateType) => getGroupById(state, convoId)?.name); return useSelector((state: StateType) => getGroupById(state, convoId)?.name);
} }
function getLibGroupKicked(state: StateType, convoId?: string) { export function getLibGroupKicked(state: StateType, convoId?: string) {
return getGroupById(state, convoId)?.kicked; return getGroupById(state, convoId)?.kicked;
} }

Loading…
Cancel
Save