@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.
Files changed (140) hide show
  1. package/lib/module/components/messages/BubbleTail.js +31 -0
  2. package/lib/module/components/messages/BubbleTail.js.map +1 -0
  3. package/lib/module/components/messages/BubbleTailRow.js +46 -0
  4. package/lib/module/components/messages/BubbleTailRow.js.map +1 -0
  5. package/lib/module/components/messages/bubbleTail.utils.js +19 -0
  6. package/lib/module/components/messages/bubbleTail.utils.js.map +1 -0
  7. package/lib/module/components/messages/fileMessage/index.js +72 -65
  8. package/lib/module/components/messages/fileMessage/index.js.map +1 -1
  9. package/lib/module/components/messages/imageMessage/index.js +23 -17
  10. package/lib/module/components/messages/imageMessage/index.js.map +1 -1
  11. package/lib/module/components/messages/linkMessage/index.js +11 -23
  12. package/lib/module/components/messages/linkMessage/index.js.map +1 -1
  13. package/lib/module/components/messages/mergedMessage/index.js +14 -7
  14. package/lib/module/components/messages/mergedMessage/index.js.map +1 -1
  15. package/lib/module/components/messages/quotedMessage/index.js +11 -23
  16. package/lib/module/components/messages/quotedMessage/index.js.map +1 -1
  17. package/lib/module/components/messages/revokedMessage/index.js +14 -7
  18. package/lib/module/components/messages/revokedMessage/index.js.map +1 -1
  19. package/lib/module/components/messages/stickerMessage/index.js +13 -7
  20. package/lib/module/components/messages/stickerMessage/index.js.map +1 -1
  21. package/lib/module/components/messages/textMessage/index.js +11 -26
  22. package/lib/module/components/messages/textMessage/index.js.map +1 -1
  23. package/lib/module/components/messages/types.js.map +1 -1
  24. package/lib/module/components/messages/videoMessage/index.js +16 -8
  25. package/lib/module/components/messages/videoMessage/index.js.map +1 -1
  26. package/lib/module/core/useChatListener.js +11 -0
  27. package/lib/module/core/useChatListener.js.map +1 -1
  28. package/lib/module/hooks/groups/useDismissGroup.js +2 -1
  29. package/lib/module/hooks/groups/useDismissGroup.js.map +1 -1
  30. package/lib/module/hooks/groups/useGroupAccessGuard.js +145 -0
  31. package/lib/module/hooks/groups/useGroupAccessGuard.js.map +1 -0
  32. package/lib/module/screens/ChatInfo/ChatInfoDescriptionRow.js +4 -1
  33. package/lib/module/screens/ChatInfo/ChatInfoDescriptionRow.js.map +1 -1
  34. package/lib/module/screens/chat-detail/ChatDetail.js +12 -10
  35. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  36. package/lib/module/screens/chat-detail/ChatDetailHeader.js +3 -9
  37. package/lib/module/screens/chat-detail/ChatDetailHeader.js.map +1 -1
  38. package/lib/module/screens/chat-detail/ChatListLegend.js +71 -21
  39. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
  40. package/lib/module/screens/chat-detail/legend/LegendChatDay.js +8 -2
  41. package/lib/module/screens/chat-detail/legend/LegendChatDay.js.map +1 -1
  42. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +38 -13
  43. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
  44. package/lib/module/screens/inbox/CommunityTab.js +5 -2
  45. package/lib/module/screens/inbox/CommunityTab.js.map +1 -1
  46. package/lib/module/screens/inbox/MessagesTab.js +2 -2
  47. package/lib/module/screens/inbox/MessagesTab.js.map +1 -1
  48. package/lib/module/store/conversation.js +18 -20
  49. package/lib/module/store/conversation.js.map +1 -1
  50. package/lib/module/translation/resources/i18n.js +4 -1
  51. package/lib/module/translation/resources/i18n.js.map +1 -1
  52. package/lib/module/utils/conversation.js +4 -1
  53. package/lib/module/utils/conversation.js.map +1 -1
  54. package/lib/module/utils/legendListMessage.js +86 -12
  55. package/lib/module/utils/legendListMessage.js.map +1 -1
  56. package/lib/module/utils/message.js +19 -1
  57. package/lib/module/utils/message.js.map +1 -1
  58. package/lib/module/utils/messageUtils.js +7 -1
  59. package/lib/module/utils/messageUtils.js.map +1 -1
  60. package/lib/typescript/src/components/messages/BubbleTail.d.ts +13 -0
  61. package/lib/typescript/src/components/messages/BubbleTail.d.ts.map +1 -0
  62. package/lib/typescript/src/components/messages/BubbleTailRow.d.ts +25 -0
  63. package/lib/typescript/src/components/messages/BubbleTailRow.d.ts.map +1 -0
  64. package/lib/typescript/src/components/messages/bubbleTail.utils.d.ts +11 -0
  65. package/lib/typescript/src/components/messages/bubbleTail.utils.d.ts.map +1 -0
  66. package/lib/typescript/src/components/messages/fileMessage/index.d.ts +1 -1
  67. package/lib/typescript/src/components/messages/fileMessage/index.d.ts.map +1 -1
  68. package/lib/typescript/src/components/messages/imageMessage/index.d.ts +1 -1
  69. package/lib/typescript/src/components/messages/imageMessage/index.d.ts.map +1 -1
  70. package/lib/typescript/src/components/messages/linkMessage/index.d.ts +1 -1
  71. package/lib/typescript/src/components/messages/linkMessage/index.d.ts.map +1 -1
  72. package/lib/typescript/src/components/messages/mergedMessage/index.d.ts +1 -1
  73. package/lib/typescript/src/components/messages/mergedMessage/index.d.ts.map +1 -1
  74. package/lib/typescript/src/components/messages/quotedMessage/index.d.ts.map +1 -1
  75. package/lib/typescript/src/components/messages/revokedMessage/index.d.ts +1 -1
  76. package/lib/typescript/src/components/messages/revokedMessage/index.d.ts.map +1 -1
  77. package/lib/typescript/src/components/messages/stickerMessage/index.d.ts +1 -1
  78. package/lib/typescript/src/components/messages/stickerMessage/index.d.ts.map +1 -1
  79. package/lib/typescript/src/components/messages/textMessage/index.d.ts +1 -1
  80. package/lib/typescript/src/components/messages/textMessage/index.d.ts.map +1 -1
  81. package/lib/typescript/src/components/messages/types.d.ts +3 -0
  82. package/lib/typescript/src/components/messages/types.d.ts.map +1 -1
  83. package/lib/typescript/src/components/messages/videoMessage/index.d.ts +1 -1
  84. package/lib/typescript/src/components/messages/videoMessage/index.d.ts.map +1 -1
  85. package/lib/typescript/src/core/useChatListener.d.ts.map +1 -1
  86. package/lib/typescript/src/hooks/groups/useDismissGroup.d.ts.map +1 -1
  87. package/lib/typescript/src/hooks/groups/useGroupAccessGuard.d.ts +10 -0
  88. package/lib/typescript/src/hooks/groups/useGroupAccessGuard.d.ts.map +1 -0
  89. package/lib/typescript/src/screens/ChatInfo/ChatInfoDescriptionRow.d.ts.map +1 -1
  90. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  91. package/lib/typescript/src/screens/chat-detail/ChatDetailHeader.d.ts +1 -1
  92. package/lib/typescript/src/screens/chat-detail/ChatDetailHeader.d.ts.map +1 -1
  93. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
  94. package/lib/typescript/src/screens/chat-detail/legend/LegendChatDay.d.ts +6 -1
  95. package/lib/typescript/src/screens/chat-detail/legend/LegendChatDay.d.ts.map +1 -1
  96. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +14 -1
  97. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
  98. package/lib/typescript/src/screens/chat-detail/types.d.ts +0 -1
  99. package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
  100. package/lib/typescript/src/store/conversation.d.ts.map +1 -1
  101. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  102. package/lib/typescript/src/utils/conversation.d.ts.map +1 -1
  103. package/lib/typescript/src/utils/legendListMessage.d.ts +19 -4
  104. package/lib/typescript/src/utils/legendListMessage.d.ts.map +1 -1
  105. package/lib/typescript/src/utils/message.d.ts +17 -0
  106. package/lib/typescript/src/utils/message.d.ts.map +1 -1
  107. package/lib/typescript/src/utils/messageUtils.d.ts +1 -0
  108. package/lib/typescript/src/utils/messageUtils.d.ts.map +1 -1
  109. package/package.json +1 -1
  110. package/src/components/messages/BubbleTail.tsx +35 -0
  111. package/src/components/messages/BubbleTailRow.tsx +64 -0
  112. package/src/components/messages/bubbleTail.utils.ts +21 -0
  113. package/src/components/messages/fileMessage/index.tsx +90 -83
  114. package/src/components/messages/imageMessage/index.tsx +26 -23
  115. package/src/components/messages/linkMessage/index.tsx +6 -18
  116. package/src/components/messages/mergedMessage/index.tsx +27 -15
  117. package/src/components/messages/quotedMessage/index.tsx +7 -19
  118. package/src/components/messages/revokedMessage/index.tsx +23 -14
  119. package/src/components/messages/stickerMessage/index.tsx +16 -11
  120. package/src/components/messages/textMessage/index.tsx +6 -22
  121. package/src/components/messages/types.ts +3 -0
  122. package/src/components/messages/videoMessage/index.tsx +13 -10
  123. package/src/core/useChatListener.ts +14 -0
  124. package/src/hooks/groups/useDismissGroup.tsx +1 -0
  125. package/src/hooks/groups/useGroupAccessGuard.ts +182 -0
  126. package/src/screens/ChatInfo/ChatInfoDescriptionRow.tsx +5 -1
  127. package/src/screens/chat-detail/ChatDetail.tsx +11 -13
  128. package/src/screens/chat-detail/ChatDetailHeader.tsx +2 -6
  129. package/src/screens/chat-detail/ChatListLegend.tsx +97 -28
  130. package/src/screens/chat-detail/legend/LegendChatDay.tsx +25 -15
  131. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +62 -12
  132. package/src/screens/chat-detail/types.ts +0 -1
  133. package/src/screens/inbox/CommunityTab.tsx +2 -2
  134. package/src/screens/inbox/MessagesTab.tsx +1 -1
  135. package/src/store/conversation.ts +14 -18
  136. package/src/translation/resources/i18n.ts +4 -0
  137. package/src/utils/conversation.ts +6 -3
  138. package/src/utils/legendListMessage.ts +123 -14
  139. package/src/utils/message.ts +19 -1
  140. 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
- ({ message, isOutgoing, onMediaPress }: BaseLegendMessageProps) => {
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
- <KContainer.View
21
- style={[
22
- messageStyles.bubble,
23
- isOutgoing ? messageStyles.bubbleSent : messageStyles.bubbleReceived,
24
- ]}
25
- >
26
- {imageMessages.length > 0 && (
27
- <MergedImageGrid
28
- images={imageMessages}
29
- maxDisplay={3}
30
- onMediaPress={onMediaPress}
31
- />
32
- )}
33
- </KContainer.View>
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 { formatMessageTime, type BaseLegendMessageProps } from '../types';
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, createdAtTime, isHighlighted } = props || {};
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(({ isOutgoing }: BaseLegendMessageProps) => {
9
- return (
10
- <KContainer.View
11
- style={[
12
- messageStyles.bubble,
13
- isOutgoing ? messageStyles.bubbleSent : messageStyles.bubbleReceived,
14
- ]}
15
- >
16
- <KLabel.Text typo="TextMdNormal" color={CHAT_BUBBLE_COLORS.timestamp}>
17
- {trans('msg_type_revoke')}
18
- </KLabel.Text>
19
- </KContainer.View>
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(({ message }: BaseLegendMessageProps) => {
8
- return (
9
- <KContainer.View style={styles.message}>
10
- <KImage.Base
11
- uri={message?.stickerElem?.content || ''}
12
- width={STICKER_SIZE}
13
- height={STICKER_SIZE}
14
- />
15
- </KContainer.View>
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 { formatMessageTime } from '../types';
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
- <KContainer.View
33
- style={[messageStyles.bubble, messageStyles.videoPlaceholder]}
34
- >
35
- <KLabel.Text typo="TextSmNormal" color={CHAT_BUBBLE_COLORS.text}>
36
- 📹 [Video unavailable]
37
- </KLabel.Text>
38
- </KContainer.View>
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: {
@@ -14,6 +14,7 @@ export function useDismissGroup({ onSuccess }: UseDismissGroupProps) {
14
14
  header: {
15
15
  title: { text: trans('group:dismiss_confirm_title') },
16
16
  alignment: 'center',
17
+ showCloseButton: true,
17
18
  },
18
19
  body: {
19
20
  renderContent: () => {
@@ -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
  }