@droppii-org/chat-mobile 0.2.78 → 0.2.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/module/components/messages/BubbleTail.js +31 -0
- package/lib/module/components/messages/BubbleTail.js.map +1 -0
- package/lib/module/components/messages/BubbleTailRow.js +46 -0
- package/lib/module/components/messages/BubbleTailRow.js.map +1 -0
- package/lib/module/components/messages/bubbleTail.utils.js +19 -0
- package/lib/module/components/messages/bubbleTail.utils.js.map +1 -0
- package/lib/module/components/messages/fileMessage/index.js +72 -65
- package/lib/module/components/messages/fileMessage/index.js.map +1 -1
- package/lib/module/components/messages/imageMessage/index.js +23 -17
- package/lib/module/components/messages/imageMessage/index.js.map +1 -1
- package/lib/module/components/messages/linkMessage/index.js +11 -23
- package/lib/module/components/messages/linkMessage/index.js.map +1 -1
- package/lib/module/components/messages/mergedMessage/index.js +14 -7
- package/lib/module/components/messages/mergedMessage/index.js.map +1 -1
- package/lib/module/components/messages/quotedMessage/index.js +11 -23
- package/lib/module/components/messages/quotedMessage/index.js.map +1 -1
- package/lib/module/components/messages/revokedMessage/index.js +14 -7
- package/lib/module/components/messages/revokedMessage/index.js.map +1 -1
- package/lib/module/components/messages/stickerMessage/index.js +13 -7
- package/lib/module/components/messages/stickerMessage/index.js.map +1 -1
- package/lib/module/components/messages/textMessage/index.js +11 -26
- package/lib/module/components/messages/textMessage/index.js.map +1 -1
- package/lib/module/components/messages/types.js.map +1 -1
- package/lib/module/components/messages/videoMessage/index.js +16 -8
- package/lib/module/components/messages/videoMessage/index.js.map +1 -1
- package/lib/module/core/useChatListener.js +11 -0
- package/lib/module/core/useChatListener.js.map +1 -1
- package/lib/module/hooks/groups/useDismissGroup.js +2 -1
- package/lib/module/hooks/groups/useDismissGroup.js.map +1 -1
- package/lib/module/hooks/groups/useGroupAccessGuard.js +145 -0
- package/lib/module/hooks/groups/useGroupAccessGuard.js.map +1 -0
- package/lib/module/screens/ChatInfo/ChatInfoDescriptionRow.js +4 -1
- package/lib/module/screens/ChatInfo/ChatInfoDescriptionRow.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatDetail.js +12 -10
- package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatDetailHeader.js +3 -9
- package/lib/module/screens/chat-detail/ChatDetailHeader.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatListLegend.js +71 -21
- package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
- package/lib/module/screens/chat-detail/legend/LegendChatDay.js +8 -2
- package/lib/module/screens/chat-detail/legend/LegendChatDay.js.map +1 -1
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +38 -13
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
- package/lib/module/screens/inbox/CommunityTab.js +5 -2
- package/lib/module/screens/inbox/CommunityTab.js.map +1 -1
- package/lib/module/screens/inbox/MessagesTab.js +2 -2
- package/lib/module/screens/inbox/MessagesTab.js.map +1 -1
- package/lib/module/store/conversation.js +18 -20
- package/lib/module/store/conversation.js.map +1 -1
- package/lib/module/translation/resources/i18n.js +4 -1
- package/lib/module/translation/resources/i18n.js.map +1 -1
- package/lib/module/utils/conversation.js +4 -1
- package/lib/module/utils/conversation.js.map +1 -1
- package/lib/module/utils/legendListMessage.js +86 -12
- package/lib/module/utils/legendListMessage.js.map +1 -1
- package/lib/module/utils/message.js +19 -1
- package/lib/module/utils/message.js.map +1 -1
- package/lib/module/utils/messageUtils.js +7 -1
- package/lib/module/utils/messageUtils.js.map +1 -1
- package/lib/typescript/src/components/messages/BubbleTail.d.ts +13 -0
- package/lib/typescript/src/components/messages/BubbleTail.d.ts.map +1 -0
- package/lib/typescript/src/components/messages/BubbleTailRow.d.ts +25 -0
- package/lib/typescript/src/components/messages/BubbleTailRow.d.ts.map +1 -0
- package/lib/typescript/src/components/messages/bubbleTail.utils.d.ts +11 -0
- package/lib/typescript/src/components/messages/bubbleTail.utils.d.ts.map +1 -0
- package/lib/typescript/src/components/messages/fileMessage/index.d.ts +1 -1
- package/lib/typescript/src/components/messages/fileMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/imageMessage/index.d.ts +1 -1
- package/lib/typescript/src/components/messages/imageMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/linkMessage/index.d.ts +1 -1
- package/lib/typescript/src/components/messages/linkMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/mergedMessage/index.d.ts +1 -1
- package/lib/typescript/src/components/messages/mergedMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/quotedMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/revokedMessage/index.d.ts +1 -1
- package/lib/typescript/src/components/messages/revokedMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/stickerMessage/index.d.ts +1 -1
- package/lib/typescript/src/components/messages/stickerMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/textMessage/index.d.ts +1 -1
- package/lib/typescript/src/components/messages/textMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/types.d.ts +3 -0
- package/lib/typescript/src/components/messages/types.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/videoMessage/index.d.ts +1 -1
- package/lib/typescript/src/components/messages/videoMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/core/useChatListener.d.ts.map +1 -1
- package/lib/typescript/src/hooks/groups/useDismissGroup.d.ts.map +1 -1
- package/lib/typescript/src/hooks/groups/useGroupAccessGuard.d.ts +10 -0
- package/lib/typescript/src/hooks/groups/useGroupAccessGuard.d.ts.map +1 -0
- package/lib/typescript/src/screens/ChatInfo/ChatInfoDescriptionRow.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatDetailHeader.d.ts +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatDetailHeader.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatDay.d.ts +6 -1
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatDay.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +14 -1
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/types.d.ts +0 -1
- package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
- package/lib/typescript/src/store/conversation.d.ts.map +1 -1
- package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
- package/lib/typescript/src/utils/conversation.d.ts.map +1 -1
- package/lib/typescript/src/utils/legendListMessage.d.ts +19 -4
- package/lib/typescript/src/utils/legendListMessage.d.ts.map +1 -1
- package/lib/typescript/src/utils/message.d.ts +17 -0
- package/lib/typescript/src/utils/message.d.ts.map +1 -1
- package/lib/typescript/src/utils/messageUtils.d.ts +1 -0
- package/lib/typescript/src/utils/messageUtils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/messages/BubbleTail.tsx +35 -0
- package/src/components/messages/BubbleTailRow.tsx +64 -0
- package/src/components/messages/bubbleTail.utils.ts +21 -0
- package/src/components/messages/fileMessage/index.tsx +90 -83
- package/src/components/messages/imageMessage/index.tsx +26 -23
- package/src/components/messages/linkMessage/index.tsx +6 -18
- package/src/components/messages/mergedMessage/index.tsx +27 -15
- package/src/components/messages/quotedMessage/index.tsx +7 -19
- package/src/components/messages/revokedMessage/index.tsx +23 -14
- package/src/components/messages/stickerMessage/index.tsx +16 -11
- package/src/components/messages/textMessage/index.tsx +6 -22
- package/src/components/messages/types.ts +3 -0
- package/src/components/messages/videoMessage/index.tsx +13 -10
- package/src/core/useChatListener.ts +14 -0
- package/src/hooks/groups/useDismissGroup.tsx +1 -0
- package/src/hooks/groups/useGroupAccessGuard.ts +182 -0
- package/src/screens/ChatInfo/ChatInfoDescriptionRow.tsx +5 -1
- package/src/screens/chat-detail/ChatDetail.tsx +11 -13
- package/src/screens/chat-detail/ChatDetailHeader.tsx +2 -6
- package/src/screens/chat-detail/ChatListLegend.tsx +97 -28
- package/src/screens/chat-detail/legend/LegendChatDay.tsx +25 -15
- package/src/screens/chat-detail/legend/LegendChatMessage.tsx +62 -12
- package/src/screens/chat-detail/types.ts +0 -1
- package/src/screens/inbox/CommunityTab.tsx +2 -2
- package/src/screens/inbox/MessagesTab.tsx +1 -1
- package/src/store/conversation.ts +14 -18
- package/src/translation/resources/i18n.ts +4 -0
- package/src/utils/conversation.ts +6 -3
- package/src/utils/legendListMessage.ts +123 -14
- package/src/utils/message.ts +19 -1
- package/src/utils/messageUtils.ts +9 -1
|
@@ -7,11 +7,12 @@ import {
|
|
|
7
7
|
KColors,
|
|
8
8
|
KSpacingValue,
|
|
9
9
|
} from '@droppii/libs';
|
|
10
|
-
import { formatMessageTime } from '../types';
|
|
11
10
|
import type { BaseLegendMessageProps } from '../types';
|
|
12
11
|
import { CHAT_BUBBLE_COLORS } from '../../../screens/chat-detail/constants';
|
|
13
12
|
import { parseUrlMetadata } from '../../../utils/messageUtils';
|
|
14
13
|
import { messageStyles } from '../styles';
|
|
14
|
+
import { getTailCornerStyle } from '../bubbleTail.utils';
|
|
15
|
+
import { BubbleTailRow } from '../BubbleTailRow';
|
|
15
16
|
|
|
16
17
|
const renderTextWithLinks = (
|
|
17
18
|
content: string,
|
|
@@ -61,22 +62,22 @@ export const LinkMessage = memo(
|
|
|
61
62
|
({
|
|
62
63
|
message,
|
|
63
64
|
isOutgoing,
|
|
64
|
-
createdAtTime,
|
|
65
65
|
onPressUrl,
|
|
66
66
|
isHighlighted,
|
|
67
|
+
groupPosition,
|
|
67
68
|
}: BaseLegendMessageProps & { onPressUrl?: (url: string) => void }) => {
|
|
68
69
|
const content = message?.urlTextElem?.content;
|
|
69
70
|
const urls = message?.urlTextElem?.urls ?? [];
|
|
70
71
|
const metadata = parseUrlMetadata(message.ex);
|
|
71
|
-
const timeLabel = formatMessageTime(createdAtTime);
|
|
72
72
|
|
|
73
73
|
return (
|
|
74
|
-
|
|
74
|
+
<BubbleTailRow isOutgoing={isOutgoing} groupPosition={groupPosition}>
|
|
75
75
|
<KContainer.View
|
|
76
76
|
style={[
|
|
77
77
|
styles.bubble,
|
|
78
78
|
isOutgoing ? styles.bubbleSent : styles.bubbleReceived,
|
|
79
79
|
isHighlighted && messageStyles.bubbleHighlighted,
|
|
80
|
+
getTailCornerStyle(isOutgoing, groupPosition),
|
|
80
81
|
]}
|
|
81
82
|
>
|
|
82
83
|
{!!content?.trim() && (
|
|
@@ -131,19 +132,7 @@ export const LinkMessage = memo(
|
|
|
131
132
|
</KContainer.Touchable>
|
|
132
133
|
)}
|
|
133
134
|
</KContainer.View>
|
|
134
|
-
|
|
135
|
-
<KContainer.View row alignItems style={styles.timeLabel}>
|
|
136
|
-
{!isOutgoing && timeLabel ? (
|
|
137
|
-
<KLabel.Text
|
|
138
|
-
typo="TextXsNormal"
|
|
139
|
-
color={CHAT_BUBBLE_COLORS.timestamp}
|
|
140
|
-
marginL="0.25rem"
|
|
141
|
-
>
|
|
142
|
-
{timeLabel}
|
|
143
|
-
</KLabel.Text>
|
|
144
|
-
) : null}
|
|
145
|
-
</KContainer.View>
|
|
146
|
-
</>
|
|
135
|
+
</BubbleTailRow>
|
|
147
136
|
);
|
|
148
137
|
}
|
|
149
138
|
);
|
|
@@ -184,5 +173,4 @@ const styles = StyleSheet.create({
|
|
|
184
173
|
paddingVertical: KSpacingValue['0.5rem'],
|
|
185
174
|
gap: 2,
|
|
186
175
|
},
|
|
187
|
-
timeLabel: { gap: 4, marginTop: 2 },
|
|
188
176
|
});
|
|
@@ -2,9 +2,16 @@ import { memo } from 'react';
|
|
|
2
2
|
import { KContainer } from '@droppii/libs';
|
|
3
3
|
import { MergedImageGrid } from '../../MergedImageGrid';
|
|
4
4
|
import { messageStyles } from '../styles';
|
|
5
|
+
import { getTailCornerStyle } from '../bubbleTail.utils';
|
|
6
|
+
import { BubbleTailRow } from '../BubbleTailRow';
|
|
5
7
|
import type { BaseLegendMessageProps } from '../types';
|
|
6
8
|
export const MergedMessage = memo(
|
|
7
|
-
({
|
|
9
|
+
({
|
|
10
|
+
message,
|
|
11
|
+
isOutgoing,
|
|
12
|
+
onMediaPress,
|
|
13
|
+
groupPosition,
|
|
14
|
+
}: BaseLegendMessageProps) => {
|
|
8
15
|
const mergeElem = (message as any).mergeElem;
|
|
9
16
|
|
|
10
17
|
if (!mergeElem?.multiMessage) {
|
|
@@ -17,20 +24,25 @@ export const MergedMessage = memo(
|
|
|
17
24
|
);
|
|
18
25
|
|
|
19
26
|
return (
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
<BubbleTailRow isOutgoing={isOutgoing} groupPosition={groupPosition}>
|
|
28
|
+
<KContainer.View
|
|
29
|
+
style={[
|
|
30
|
+
messageStyles.bubble,
|
|
31
|
+
isOutgoing
|
|
32
|
+
? messageStyles.bubbleSent
|
|
33
|
+
: messageStyles.bubbleReceived,
|
|
34
|
+
getTailCornerStyle(isOutgoing, groupPosition),
|
|
35
|
+
]}
|
|
36
|
+
>
|
|
37
|
+
{imageMessages.length > 0 && (
|
|
38
|
+
<MergedImageGrid
|
|
39
|
+
images={imageMessages}
|
|
40
|
+
maxDisplay={3}
|
|
41
|
+
onMediaPress={onMediaPress}
|
|
42
|
+
/>
|
|
43
|
+
)}
|
|
44
|
+
</KContainer.View>
|
|
45
|
+
</BubbleTailRow>
|
|
34
46
|
);
|
|
35
47
|
}
|
|
36
48
|
);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { memo, useCallback, useMemo } from 'react';
|
|
2
2
|
import { messageStyles } from '../styles';
|
|
3
|
-
import {
|
|
3
|
+
import { getTailCornerStyle } from '../bubbleTail.utils';
|
|
4
|
+
import { BubbleTailRow } from '../BubbleTailRow';
|
|
5
|
+
import { type BaseLegendMessageProps } from '../types';
|
|
4
6
|
import {
|
|
5
7
|
KColors,
|
|
6
8
|
KContainer,
|
|
@@ -25,7 +27,7 @@ import { ChatEvent } from '../../../types/events';
|
|
|
25
27
|
interface QuotedMessageProps extends BaseLegendMessageProps {}
|
|
26
28
|
|
|
27
29
|
const QuotedMessage = memo((props: QuotedMessageProps) => {
|
|
28
|
-
const { message, isOutgoing,
|
|
30
|
+
const { message, isOutgoing, isHighlighted, groupPosition } = props || {};
|
|
29
31
|
|
|
30
32
|
const { conversation } = useChatDetailContext();
|
|
31
33
|
|
|
@@ -35,8 +37,6 @@ const QuotedMessage = memo((props: QuotedMessageProps) => {
|
|
|
35
37
|
|
|
36
38
|
const messagePreview = getMessagePreviewParts(quotedMessage);
|
|
37
39
|
|
|
38
|
-
const timeLabel = formatMessageTime(createdAtTime);
|
|
39
|
-
|
|
40
40
|
const imageUrl = useMemo(() => {
|
|
41
41
|
switch (quotedMessage?.contentType) {
|
|
42
42
|
case MessageType.PictureMessage:
|
|
@@ -64,13 +64,14 @@ const QuotedMessage = memo((props: QuotedMessageProps) => {
|
|
|
64
64
|
}, [quotedMessage]);
|
|
65
65
|
|
|
66
66
|
return (
|
|
67
|
-
|
|
67
|
+
<BubbleTailRow isOutgoing={isOutgoing} groupPosition={groupPosition}>
|
|
68
68
|
<KContainer.View
|
|
69
69
|
style={[
|
|
70
70
|
messageStyles.bubble,
|
|
71
71
|
styles.container,
|
|
72
72
|
isOutgoing ? messageStyles.bubbleSent : messageStyles.bubbleReceived,
|
|
73
73
|
isHighlighted && messageStyles.bubbleHighlighted,
|
|
74
|
+
getTailCornerStyle(isOutgoing, groupPosition),
|
|
74
75
|
]}
|
|
75
76
|
>
|
|
76
77
|
<KContainer.Touchable
|
|
@@ -128,19 +129,7 @@ const QuotedMessage = memo((props: QuotedMessageProps) => {
|
|
|
128
129
|
{message?.quoteElem?.text || ''}
|
|
129
130
|
</KLabel.Text>
|
|
130
131
|
</KContainer.View>
|
|
131
|
-
|
|
132
|
-
<KContainer.View row alignItems style={styles.timeLabel}>
|
|
133
|
-
{!isOutgoing && timeLabel ? (
|
|
134
|
-
<KLabel.Text
|
|
135
|
-
typo="TextXsNormal"
|
|
136
|
-
color={CHAT_BUBBLE_COLORS.timestamp}
|
|
137
|
-
marginL="0.25rem"
|
|
138
|
-
>
|
|
139
|
-
{timeLabel}
|
|
140
|
-
</KLabel.Text>
|
|
141
|
-
) : null}
|
|
142
|
-
</KContainer.View>
|
|
143
|
-
</>
|
|
132
|
+
</BubbleTailRow>
|
|
144
133
|
);
|
|
145
134
|
});
|
|
146
135
|
|
|
@@ -198,5 +187,4 @@ const styles = StyleSheet.create({
|
|
|
198
187
|
justifyContent: 'center',
|
|
199
188
|
alignItems: 'center',
|
|
200
189
|
},
|
|
201
|
-
timeLabel: { gap: 4, marginTop: 2 },
|
|
202
190
|
});
|
|
@@ -3,21 +3,30 @@ import { KContainer, KLabel } from '@droppii/libs';
|
|
|
3
3
|
import { trans } from '../../../translation';
|
|
4
4
|
import { CHAT_BUBBLE_COLORS } from '../../../screens/chat-detail/constants';
|
|
5
5
|
import { messageStyles } from '../styles';
|
|
6
|
+
import { getTailCornerStyle } from '../bubbleTail.utils';
|
|
7
|
+
import { BubbleTailRow } from '../BubbleTailRow';
|
|
6
8
|
import type { BaseLegendMessageProps } from '../types';
|
|
7
9
|
|
|
8
|
-
export const RevokedMessage = memo(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
10
|
+
export const RevokedMessage = memo(
|
|
11
|
+
({ isOutgoing, groupPosition }: BaseLegendMessageProps) => {
|
|
12
|
+
return (
|
|
13
|
+
<BubbleTailRow isOutgoing={isOutgoing} groupPosition={groupPosition}>
|
|
14
|
+
<KContainer.View
|
|
15
|
+
style={[
|
|
16
|
+
messageStyles.bubble,
|
|
17
|
+
isOutgoing
|
|
18
|
+
? messageStyles.bubbleSent
|
|
19
|
+
: messageStyles.bubbleReceived,
|
|
20
|
+
getTailCornerStyle(isOutgoing, groupPosition),
|
|
21
|
+
]}
|
|
22
|
+
>
|
|
23
|
+
<KLabel.Text typo="TextMdNormal" color={CHAT_BUBBLE_COLORS.timestamp}>
|
|
24
|
+
{trans('msg_type_revoke')}
|
|
25
|
+
</KLabel.Text>
|
|
26
|
+
</KContainer.View>
|
|
27
|
+
</BubbleTailRow>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
);
|
|
22
31
|
|
|
23
32
|
RevokedMessage.displayName = 'RevokedMessage';
|
|
@@ -2,19 +2,24 @@ import { memo } from 'react';
|
|
|
2
2
|
import type { BaseLegendMessageProps } from '../types';
|
|
3
3
|
import { KContainer, KImage } from '@droppii/libs';
|
|
4
4
|
import { StyleSheet } from 'react-native';
|
|
5
|
+
import { BubbleTailRow } from '../BubbleTailRow';
|
|
5
6
|
|
|
6
7
|
export const STICKER_SIZE = 120;
|
|
7
|
-
export const StickerMessage = memo(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
8
|
+
export const StickerMessage = memo(
|
|
9
|
+
({ message, isOutgoing }: BaseLegendMessageProps) => {
|
|
10
|
+
return (
|
|
11
|
+
<BubbleTailRow isOutgoing={isOutgoing} withTail={false}>
|
|
12
|
+
<KContainer.View style={styles.message}>
|
|
13
|
+
<KImage.Base
|
|
14
|
+
uri={message?.stickerElem?.content || ''}
|
|
15
|
+
width={STICKER_SIZE}
|
|
16
|
+
height={STICKER_SIZE}
|
|
17
|
+
/>
|
|
18
|
+
</KContainer.View>
|
|
19
|
+
</BubbleTailRow>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
);
|
|
18
23
|
|
|
19
24
|
const styles = StyleSheet.create({
|
|
20
25
|
message: {
|
|
@@ -3,25 +3,24 @@ import { KContainer, KLabel } from '@droppii/libs';
|
|
|
3
3
|
import { getMessageText } from '../../../utils/legendListMessage';
|
|
4
4
|
import { CHAT_BUBBLE_COLORS } from '../../../screens/chat-detail/constants';
|
|
5
5
|
import { messageStyles } from '../styles';
|
|
6
|
-
import {
|
|
6
|
+
import { getTailCornerStyle } from '../bubbleTail.utils';
|
|
7
|
+
import { BubbleTailRow } from '../BubbleTailRow';
|
|
7
8
|
import type { BaseLegendMessageProps } from '../types';
|
|
8
|
-
import { StyleSheet } from 'react-native';
|
|
9
9
|
export const TextMessage = memo(
|
|
10
10
|
({
|
|
11
11
|
message,
|
|
12
12
|
isOutgoing,
|
|
13
|
-
createdAtTime,
|
|
14
13
|
isHighlighted,
|
|
14
|
+
groupPosition,
|
|
15
15
|
}: BaseLegendMessageProps) => {
|
|
16
16
|
const messageText = getMessageText(message);
|
|
17
|
-
const timeLabel = formatMessageTime(createdAtTime);
|
|
18
17
|
|
|
19
18
|
if (!messageText?.trim()) {
|
|
20
19
|
return null;
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
return (
|
|
24
|
-
|
|
23
|
+
<BubbleTailRow isOutgoing={isOutgoing} groupPosition={groupPosition}>
|
|
25
24
|
<KContainer.View
|
|
26
25
|
style={[
|
|
27
26
|
messageStyles.bubble,
|
|
@@ -29,31 +28,16 @@ export const TextMessage = memo(
|
|
|
29
28
|
? messageStyles.bubbleSent
|
|
30
29
|
: messageStyles.bubbleReceived,
|
|
31
30
|
isHighlighted && messageStyles.bubbleHighlighted,
|
|
31
|
+
getTailCornerStyle(isOutgoing, groupPosition),
|
|
32
32
|
]}
|
|
33
33
|
>
|
|
34
34
|
<KLabel.Text typo="TextMdNormal" color={CHAT_BUBBLE_COLORS.text}>
|
|
35
35
|
{messageText}
|
|
36
36
|
</KLabel.Text>
|
|
37
37
|
</KContainer.View>
|
|
38
|
-
|
|
39
|
-
<KContainer.View row alignItems style={styles.timeLabel}>
|
|
40
|
-
{!isOutgoing && timeLabel ? (
|
|
41
|
-
<KLabel.Text
|
|
42
|
-
typo="TextXsNormal"
|
|
43
|
-
color={CHAT_BUBBLE_COLORS.timestamp}
|
|
44
|
-
marginL={'0.25rem'}
|
|
45
|
-
>
|
|
46
|
-
{timeLabel}
|
|
47
|
-
</KLabel.Text>
|
|
48
|
-
) : null}
|
|
49
|
-
</KContainer.View>
|
|
50
|
-
</>
|
|
38
|
+
</BubbleTailRow>
|
|
51
39
|
);
|
|
52
40
|
}
|
|
53
41
|
);
|
|
54
42
|
|
|
55
43
|
TextMessage.displayName = 'TextMessage';
|
|
56
|
-
|
|
57
|
-
const styles = StyleSheet.create({
|
|
58
|
-
timeLabel: { gap: 4, marginTop: 2 },
|
|
59
|
-
});
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type { MediaItem } from '../../hooks/useMediaViewer';
|
|
2
2
|
import type { MessageItem } from '@droppii/openim-rn-client-sdk';
|
|
3
|
+
import type { GroupPosition } from '../../utils/legendListMessage';
|
|
3
4
|
|
|
4
5
|
export interface BaseLegendMessageProps {
|
|
5
6
|
message: MessageItem;
|
|
6
7
|
isOutgoing: boolean;
|
|
7
8
|
createdAtTime: number;
|
|
8
9
|
isHighlighted?: boolean;
|
|
10
|
+
/** Position within its same-sender cluster — drives the bubble tail. */
|
|
11
|
+
groupPosition?: GroupPosition;
|
|
9
12
|
onMediaPress?: (items: MediaItem[], index?: number) => void;
|
|
10
13
|
}
|
|
11
14
|
|
|
@@ -6,6 +6,7 @@ import ChatImageDimens, {
|
|
|
6
6
|
} from '../../../utils/chatImageDimens';
|
|
7
7
|
import { CHAT_BUBBLE_COLORS } from '../../../screens/chat-detail/constants';
|
|
8
8
|
import { messageStyles } from '../styles';
|
|
9
|
+
import { BubbleTailRow } from '../BubbleTailRow';
|
|
9
10
|
import type { BaseLegendMessageProps } from '../types';
|
|
10
11
|
const formatDuration = (durationMs: number): string => {
|
|
11
12
|
const totalSeconds = Math.floor(durationMs / 1000);
|
|
@@ -15,7 +16,7 @@ const formatDuration = (durationMs: number): string => {
|
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
export const VideoMessage = memo(
|
|
18
|
-
({ message, onMediaPress }: BaseLegendMessageProps) => {
|
|
19
|
+
({ message, isOutgoing, onMediaPress }: BaseLegendMessageProps) => {
|
|
19
20
|
const videoElem = message?.videoElem;
|
|
20
21
|
const videoUrl = videoElem?.videoUrl;
|
|
21
22
|
const duration = videoElem?.duration;
|
|
@@ -29,13 +30,15 @@ export const VideoMessage = memo(
|
|
|
29
30
|
|
|
30
31
|
if (!videoUrl) {
|
|
31
32
|
return (
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
<BubbleTailRow isOutgoing={isOutgoing} withTail={false}>
|
|
34
|
+
<KContainer.View
|
|
35
|
+
style={[messageStyles.bubble, messageStyles.videoPlaceholder]}
|
|
36
|
+
>
|
|
37
|
+
<KLabel.Text typo="TextSmNormal" color={CHAT_BUBBLE_COLORS.text}>
|
|
38
|
+
📹 [Video unavailable]
|
|
39
|
+
</KLabel.Text>
|
|
40
|
+
</KContainer.View>
|
|
41
|
+
</BubbleTailRow>
|
|
39
42
|
);
|
|
40
43
|
}
|
|
41
44
|
|
|
@@ -44,7 +47,7 @@ export const VideoMessage = memo(
|
|
|
44
47
|
);
|
|
45
48
|
|
|
46
49
|
return (
|
|
47
|
-
|
|
50
|
+
<BubbleTailRow isOutgoing={isOutgoing} withTail={false}>
|
|
48
51
|
<KContainer.Touchable
|
|
49
52
|
onPress={handlePress}
|
|
50
53
|
style={[
|
|
@@ -80,7 +83,7 @@ export const VideoMessage = memo(
|
|
|
80
83
|
</KContainer.View>
|
|
81
84
|
)}
|
|
82
85
|
</KContainer.Touchable>
|
|
83
|
-
|
|
86
|
+
</BubbleTailRow>
|
|
84
87
|
);
|
|
85
88
|
}
|
|
86
89
|
);
|
|
@@ -256,9 +256,23 @@ export const useChatListener = () => {
|
|
|
256
256
|
|
|
257
257
|
const markGroupJoined = useCallback(
|
|
258
258
|
(groupID: string) => {
|
|
259
|
+
// Mirror of removeGroupConversation: leaving marks the conversation
|
|
260
|
+
// `isNotInGroup`, which keeps it out of the inbox, so rejoining has to
|
|
261
|
+
// clear that flag. This runs on the WS event rather than on joinGroup's
|
|
262
|
+
// API success because isJoinGroup only reports true once the incremental
|
|
263
|
+
// sync behind that event has landed.
|
|
264
|
+
useConversationStore
|
|
265
|
+
.getState()
|
|
266
|
+
.updateConversation(getConversationID(SessionType.Group, groupID), {
|
|
267
|
+
isNotInGroup: false,
|
|
268
|
+
});
|
|
269
|
+
|
|
259
270
|
queryClient.invalidateQueries({
|
|
260
271
|
queryKey: conversationQueryKeys.joinedGroup(groupID),
|
|
261
272
|
});
|
|
273
|
+
queryClient.invalidateQueries({
|
|
274
|
+
queryKey: conversationQueryKeys.lists(),
|
|
275
|
+
});
|
|
262
276
|
|
|
263
277
|
queryClient.setQueriesData<{
|
|
264
278
|
pages: {
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { DeviceEventEmitter } from 'react-native';
|
|
3
|
+
import OpenIMSDK, {
|
|
4
|
+
GroupStatus,
|
|
5
|
+
GroupVisibility,
|
|
6
|
+
} from '@droppii/openim-rn-client-sdk';
|
|
7
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
8
|
+
import { UIUtils } from '../../utils/UIUtils';
|
|
9
|
+
import { trans } from '../../translation';
|
|
10
|
+
import { ChatEvent, type GroupRemovedPayload } from '../../types/events';
|
|
11
|
+
import { conversationQueryKeys } from '../query-keys';
|
|
12
|
+
|
|
13
|
+
type GroupAccessState =
|
|
14
|
+
/** Pre-access check still running — render nothing yet. */
|
|
15
|
+
| 'checking'
|
|
16
|
+
/** Member of the group, or not a group conversation at all. */
|
|
17
|
+
| 'allowed'
|
|
18
|
+
/** Non-member of a public group: may read, may not participate. */
|
|
19
|
+
| 'view-only'
|
|
20
|
+
/** No access: the block modal was shown and `onBlocked` fired. */
|
|
21
|
+
| 'blocked';
|
|
22
|
+
|
|
23
|
+
type EvaluationResult = { state: Exclude<GroupAccessState, 'checking'> };
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolves whether the current user may open `groupID`, per the group-access
|
|
27
|
+
* spec: private non-members and dismissed groups are blocked outright, while
|
|
28
|
+
* public non-members drop to a read-only view.
|
|
29
|
+
*/
|
|
30
|
+
const evaluateAccess = async (groupID: string): Promise<EvaluationResult> => {
|
|
31
|
+
const info = (await OpenIMSDK.getSpecifiedGroupsInfo([groupID]))?.[0];
|
|
32
|
+
if (!info) return { state: 'blocked' };
|
|
33
|
+
if (info.status === GroupStatus.Dismissed) return { state: 'blocked' };
|
|
34
|
+
|
|
35
|
+
const isMember = await OpenIMSDK.isJoinGroup(groupID);
|
|
36
|
+
if (isMember) return { state: 'allowed' };
|
|
37
|
+
|
|
38
|
+
return info.visibility === GroupVisibility.Public
|
|
39
|
+
? { state: 'view-only' }
|
|
40
|
+
: { state: 'blocked' };
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type Params = {
|
|
44
|
+
groupID?: string;
|
|
45
|
+
/** Navigate away from the chat screen. Called once, after the block modal. */
|
|
46
|
+
onBlocked: () => void;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const useGroupAccessGuard = ({ groupID, onBlocked }: Params) => {
|
|
50
|
+
const queryClient = useQueryClient();
|
|
51
|
+
|
|
52
|
+
// The conversation is dropped from the store immediately before GroupRemoved
|
|
53
|
+
// is emitted, so `groupID` can fall back to empty exactly when the guard
|
|
54
|
+
// needs it. Latch the last known id; it is replaced whenever the caller
|
|
55
|
+
// supplies a different non-empty one. (A group → 1-1 switch inside a single
|
|
56
|
+
// mount would keep the stale id, but ChatDetail remounts per conversation.)
|
|
57
|
+
const groupIdRef = useRef(groupID);
|
|
58
|
+
if (groupID) groupIdRef.current = groupID;
|
|
59
|
+
const activeGroupId = groupIdRef.current;
|
|
60
|
+
|
|
61
|
+
const [accessState, setAccessState] = useState<GroupAccessState>(() =>
|
|
62
|
+
activeGroupId ? 'checking' : 'allowed'
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// OnGroupDismissed fires twice for the group owner (once on API success,
|
|
66
|
+
// once when the WS notification lands), so blocking must be idempotent.
|
|
67
|
+
const hasBlockedRef = useRef(false);
|
|
68
|
+
const onBlockedRef = useRef(onBlocked);
|
|
69
|
+
onBlockedRef.current = onBlocked;
|
|
70
|
+
|
|
71
|
+
const block = useCallback(() => {
|
|
72
|
+
if (hasBlockedRef.current) return;
|
|
73
|
+
hasBlockedRef.current = true;
|
|
74
|
+
|
|
75
|
+
setAccessState('blocked');
|
|
76
|
+
UIUtils.alert.open({
|
|
77
|
+
title: trans('group:access_blocked_title'),
|
|
78
|
+
message: trans('group:access_blocked_desc'),
|
|
79
|
+
primaryIndex: 0,
|
|
80
|
+
buttonType: 'horizontal-button',
|
|
81
|
+
// Drop any alert raised while this one is up rather than stacking it —
|
|
82
|
+
// OnGroupDismissed fires twice for the group owner.
|
|
83
|
+
ignoreStack: true,
|
|
84
|
+
// Force an acknowledgement: dismissing by tapping outside would leave the
|
|
85
|
+
// user sitting in a conversation they cannot access.
|
|
86
|
+
touchOutsideToDismiss: false,
|
|
87
|
+
buttons: [
|
|
88
|
+
{
|
|
89
|
+
label: trans('group:access_blocked_button'),
|
|
90
|
+
kind: 'primary',
|
|
91
|
+
onPress: () => onBlockedRef.current(),
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
});
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
const applyResult = useCallback(
|
|
98
|
+
(result: EvaluationResult) => {
|
|
99
|
+
if (result.state === 'blocked') {
|
|
100
|
+
block();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
setAccessState(result.state);
|
|
104
|
+
},
|
|
105
|
+
[block]
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Pre-access check.
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (!activeGroupId) {
|
|
111
|
+
setAccessState('allowed');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let cancelled = false;
|
|
116
|
+
hasBlockedRef.current = false;
|
|
117
|
+
setAccessState('checking');
|
|
118
|
+
|
|
119
|
+
evaluateAccess(activeGroupId)
|
|
120
|
+
.then((result) => {
|
|
121
|
+
if (cancelled) return;
|
|
122
|
+
applyResult(result);
|
|
123
|
+
})
|
|
124
|
+
.catch((error) => {
|
|
125
|
+
console.error('useGroupAccessGuard: access check failed', error);
|
|
126
|
+
// A failed lookup is not proof of lost access — leave the screen open
|
|
127
|
+
// rather than ejecting the user on a transient SDK error.
|
|
128
|
+
if (!cancelled) setAccessState('allowed');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return () => {
|
|
132
|
+
cancelled = true;
|
|
133
|
+
};
|
|
134
|
+
}, [activeGroupId, applyResult]);
|
|
135
|
+
|
|
136
|
+
// Membership lost while the screen is open.
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (!activeGroupId) return;
|
|
139
|
+
|
|
140
|
+
const sub = DeviceEventEmitter.addListener(
|
|
141
|
+
ChatEvent.GroupRemoved,
|
|
142
|
+
(payload: GroupRemovedPayload) => {
|
|
143
|
+
if (payload.groupID !== activeGroupId) return;
|
|
144
|
+
|
|
145
|
+
// A dismissed group is gone for everyone — no need to re-check.
|
|
146
|
+
if (payload.reason === 'dismissed') {
|
|
147
|
+
block();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Left or kicked. `reason` cannot tell the two apart — OnJoinedGroupDeleted
|
|
152
|
+
// reports both as 'left' — so decide on visibility instead: a public group
|
|
153
|
+
// stays readable, a private one does not.
|
|
154
|
+
evaluateAccess(activeGroupId)
|
|
155
|
+
.then(applyResult)
|
|
156
|
+
.catch(() => block());
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
return () => sub.remove();
|
|
161
|
+
}, [activeGroupId, block, applyResult]);
|
|
162
|
+
|
|
163
|
+
// The composer derives membership from the joinedGroup query alone, never
|
|
164
|
+
// from this hook. Refresh that query whenever the guard's own verdict flips
|
|
165
|
+
// so the two cannot disagree.
|
|
166
|
+
const isViewOnly = accessState === 'view-only';
|
|
167
|
+
const wasViewOnlyRef = useRef(isViewOnly);
|
|
168
|
+
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
if (wasViewOnlyRef.current === isViewOnly) return;
|
|
171
|
+
wasViewOnlyRef.current = isViewOnly;
|
|
172
|
+
if (!activeGroupId) return;
|
|
173
|
+
|
|
174
|
+
queryClient.invalidateQueries({
|
|
175
|
+
queryKey: conversationQueryKeys.joinedGroup(activeGroupId),
|
|
176
|
+
});
|
|
177
|
+
}, [isViewOnly, activeGroupId, queryClient]);
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
isCheckingAccess: accessState === 'checking',
|
|
181
|
+
};
|
|
182
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { memo, useCallback, useState } from 'react';
|
|
1
|
+
import { memo, useCallback, useEffect, useState } from 'react';
|
|
2
2
|
import { StyleSheet, type TextLayoutEvent } from 'react-native';
|
|
3
3
|
import { KContainer, KLabel, KColors } from '@droppii/libs';
|
|
4
4
|
import { trans } from '../../translation';
|
|
@@ -47,6 +47,10 @@ const ChatInfoDescriptionRow = memo(
|
|
|
47
47
|
paddingH: '0.75rem' as const,
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
setDisplayedText(introduction);
|
|
52
|
+
}, [introduction]);
|
|
53
|
+
|
|
50
54
|
if (!introduction) {
|
|
51
55
|
return null;
|
|
52
56
|
}
|