@streamplace/components 0.7.2 → 0.7.7

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 (91) hide show
  1. package/dist/components/chat/chat-box.js +212 -24
  2. package/dist/components/chat/chat-message.js +5 -5
  3. package/dist/components/chat/chat.js +83 -5
  4. package/dist/components/chat/emoji-suggestions.js +35 -0
  5. package/dist/components/chat/mod-view.js +59 -8
  6. package/dist/components/chat/system-message.js +19 -0
  7. package/dist/components/icons/bluesky-icon.js +9 -0
  8. package/dist/components/keep-awake.js +7 -0
  9. package/dist/components/keep-awake.native.js +16 -0
  10. package/dist/components/mobile-player/fullscreen.js +2 -1
  11. package/dist/components/mobile-player/fullscreen.native.js +3 -3
  12. package/dist/components/mobile-player/player.js +15 -30
  13. package/dist/components/mobile-player/ui/index.js +2 -0
  14. package/dist/components/mobile-player/ui/report-modal.js +90 -0
  15. package/dist/components/mobile-player/ui/streamer-loading-overlay.js +104 -0
  16. package/dist/components/mobile-player/ui/viewer-context-menu.js +20 -1
  17. package/dist/components/mobile-player/ui/viewer-loading-overlay.js +49 -0
  18. package/dist/components/mobile-player/use-webrtc.js +7 -1
  19. package/dist/components/mobile-player/video-retry.js +29 -0
  20. package/dist/components/mobile-player/video.js +84 -9
  21. package/dist/components/mobile-player/video.native.js +24 -10
  22. package/dist/components/share/sharesheet.js +91 -0
  23. package/dist/components/ui/dialog.js +1 -1
  24. package/dist/components/ui/dropdown.js +6 -6
  25. package/dist/components/ui/index.js +2 -0
  26. package/dist/components/ui/primitives/modal.js +0 -1
  27. package/dist/components/ui/resizeable.js +20 -11
  28. package/dist/components/ui/slider.js +5 -0
  29. package/dist/hooks/index.js +1 -0
  30. package/dist/hooks/usePointerDevice.js +71 -0
  31. package/dist/index.js +10 -3
  32. package/dist/lib/system-messages.js +101 -0
  33. package/dist/livestream-store/chat.js +111 -18
  34. package/dist/livestream-store/livestream-store.js +3 -0
  35. package/dist/livestream-store/problems.js +76 -0
  36. package/dist/livestream-store/websocket-consumer.js +39 -4
  37. package/dist/player-store/player-store.js +33 -4
  38. package/dist/streamplace-store/block.js +51 -12
  39. package/dist/streamplace-store/stream.js +44 -23
  40. package/dist/ui/index.js +79 -0
  41. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  42. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
  43. package/node-compile-cache/{v22.15.0-x64-92db9086-0 → v22.15.0-x64-efe9a9df-0}/67b1eb60 +0 -0
  44. package/node-compile-cache/{v22.15.0-x64-92db9086-0 → v22.15.0-x64-efe9a9df-0}/7c275f90 +0 -0
  45. package/package.json +6 -2
  46. package/src/components/chat/chat-box.tsx +295 -25
  47. package/src/components/chat/chat-message.tsx +6 -7
  48. package/src/components/chat/chat.tsx +192 -41
  49. package/src/components/chat/emoji-suggestions.tsx +94 -0
  50. package/src/components/chat/mod-view.tsx +119 -40
  51. package/src/components/chat/system-message.tsx +38 -0
  52. package/src/components/icons/bluesky-icon.tsx +9 -0
  53. package/src/components/keep-awake.native.tsx +13 -0
  54. package/src/components/keep-awake.tsx +3 -0
  55. package/src/components/mobile-player/fullscreen.native.tsx +12 -3
  56. package/src/components/mobile-player/fullscreen.tsx +10 -3
  57. package/src/components/mobile-player/player.tsx +28 -36
  58. package/src/components/mobile-player/props.tsx +1 -0
  59. package/src/components/mobile-player/ui/index.ts +2 -0
  60. package/src/components/mobile-player/ui/report-modal.tsx +195 -0
  61. package/src/components/mobile-player/ui/streamer-loading-overlay.tsx +154 -0
  62. package/src/components/mobile-player/ui/viewer-context-menu.tsx +31 -3
  63. package/src/components/mobile-player/ui/viewer-loading-overlay.tsx +66 -0
  64. package/src/components/mobile-player/use-webrtc.tsx +10 -2
  65. package/src/components/mobile-player/video-retry.tsx +28 -0
  66. package/src/components/mobile-player/video.native.tsx +24 -10
  67. package/src/components/mobile-player/video.tsx +100 -21
  68. package/src/components/share/sharesheet.tsx +185 -0
  69. package/src/components/ui/dialog.tsx +1 -1
  70. package/src/components/ui/dropdown.tsx +13 -13
  71. package/src/components/ui/index.ts +2 -0
  72. package/src/components/ui/primitives/modal.tsx +0 -1
  73. package/src/components/ui/resizeable.tsx +26 -15
  74. package/src/components/ui/slider.tsx +1 -0
  75. package/src/hooks/index.ts +1 -0
  76. package/src/hooks/usePointerDevice.ts +89 -0
  77. package/src/index.tsx +11 -2
  78. package/src/lib/system-messages.ts +135 -0
  79. package/src/livestream-store/chat.tsx +145 -17
  80. package/src/livestream-store/livestream-state.tsx +10 -0
  81. package/src/livestream-store/livestream-store.tsx +3 -0
  82. package/src/livestream-store/problems.tsx +96 -0
  83. package/src/livestream-store/websocket-consumer.tsx +44 -4
  84. package/src/player-store/player-state.tsx +25 -4
  85. package/src/player-store/player-store.tsx +43 -5
  86. package/src/streamplace-store/block.tsx +55 -13
  87. package/src/streamplace-store/stream.tsx +66 -35
  88. package/src/ui/index.ts +86 -0
  89. package/tsconfig.tsbuildinfo +1 -1
  90. package/node-compile-cache/v22.15.0-x64-92db9086-0/37be0eec +0 -0
  91. package/node-compile-cache/v22.15.0-x64-92db9086-0/56540125 +0 -0
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ /**
3
+ * @streamplace/components/ui - Streamplace ZeroCSS
4
+ *
5
+ * Clean export path for ZeroCSS styling utilities, design tokens, and atomic styles.
6
+ * ZeroCSS provides a zero-config, atomic styling system optimized for React Native.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.atomsNS = exports.theme = exports.useTheme = exports.usePlatformTypography = exports.lightTheme = exports.darkTheme = exports.createThemedStyles = exports.createThemeStyles = exports.createThemeIcons = exports.createThemeColors = exports.ThemeProvider = exports.responsiveValue = exports.platformStyle = exports.mergeStyles = exports.debounce = exports.typography = exports.spacing = exports.shadows = exports.colors = exports.breakpoints = exports.borderRadius = exports.w = exports.top = exports.text = exports.right = exports.r = exports.py = exports.px = exports.pt = exports.pr = exports.position = exports.pl = exports.pb = exports.p = exports.my = exports.mx = exports.mt = exports.mr = exports.ml = exports.mb = exports.m = exports.left = exports.layout = exports.h = exports.gap = exports.flex = exports.bottom = exports.borders = exports.bg = exports.atoms = void 0;
10
+ exports.utils = exports.tokens = void 0;
11
+ const tslib_1 = require("tslib");
12
+ // Export the most commonly used ZeroCSS utilities
13
+ var atoms_1 = require("../lib/theme/atoms");
14
+ // Core atoms object
15
+ Object.defineProperty(exports, "atoms", { enumerable: true, get: function () { return atoms_1.atoms; } });
16
+ // Common shorthand utilities
17
+ Object.defineProperty(exports, "bg", { enumerable: true, get: function () { return atoms_1.bg; } });
18
+ // Border utilities
19
+ Object.defineProperty(exports, "borders", { enumerable: true, get: function () { return atoms_1.borders; } });
20
+ Object.defineProperty(exports, "bottom", { enumerable: true, get: function () { return atoms_1.bottom; } });
21
+ // Flex utilities
22
+ Object.defineProperty(exports, "flex", { enumerable: true, get: function () { return atoms_1.flex; } });
23
+ // Gap utilities (React Native 0.71+)
24
+ Object.defineProperty(exports, "gap", { enumerable: true, get: function () { return atoms_1.gap; } });
25
+ Object.defineProperty(exports, "h", { enumerable: true, get: function () { return atoms_1.h; } });
26
+ // Layout utilities
27
+ Object.defineProperty(exports, "layout", { enumerable: true, get: function () { return atoms_1.layout; } });
28
+ Object.defineProperty(exports, "left", { enumerable: true, get: function () { return atoms_1.left; } });
29
+ Object.defineProperty(exports, "m", { enumerable: true, get: function () { return atoms_1.m; } });
30
+ Object.defineProperty(exports, "mb", { enumerable: true, get: function () { return atoms_1.mb; } });
31
+ Object.defineProperty(exports, "ml", { enumerable: true, get: function () { return atoms_1.ml; } });
32
+ Object.defineProperty(exports, "mr", { enumerable: true, get: function () { return atoms_1.mr; } });
33
+ Object.defineProperty(exports, "mt", { enumerable: true, get: function () { return atoms_1.mt; } });
34
+ Object.defineProperty(exports, "mx", { enumerable: true, get: function () { return atoms_1.mx; } });
35
+ Object.defineProperty(exports, "my", { enumerable: true, get: function () { return atoms_1.my; } });
36
+ Object.defineProperty(exports, "p", { enumerable: true, get: function () { return atoms_1.p; } });
37
+ Object.defineProperty(exports, "pb", { enumerable: true, get: function () { return atoms_1.pb; } });
38
+ Object.defineProperty(exports, "pl", { enumerable: true, get: function () { return atoms_1.pl; } });
39
+ // Position utilities
40
+ Object.defineProperty(exports, "position", { enumerable: true, get: function () { return atoms_1.position; } });
41
+ Object.defineProperty(exports, "pr", { enumerable: true, get: function () { return atoms_1.pr; } });
42
+ Object.defineProperty(exports, "pt", { enumerable: true, get: function () { return atoms_1.pt; } });
43
+ Object.defineProperty(exports, "px", { enumerable: true, get: function () { return atoms_1.px; } });
44
+ Object.defineProperty(exports, "py", { enumerable: true, get: function () { return atoms_1.py; } });
45
+ Object.defineProperty(exports, "r", { enumerable: true, get: function () { return atoms_1.r; } });
46
+ Object.defineProperty(exports, "right", { enumerable: true, get: function () { return atoms_1.right; } });
47
+ Object.defineProperty(exports, "text", { enumerable: true, get: function () { return atoms_1.text; } });
48
+ Object.defineProperty(exports, "top", { enumerable: true, get: function () { return atoms_1.top; } });
49
+ Object.defineProperty(exports, "w", { enumerable: true, get: function () { return atoms_1.w; } });
50
+ // Export ZeroCSS design tokens
51
+ var tokens_1 = require("../lib/theme/tokens");
52
+ Object.defineProperty(exports, "borderRadius", { enumerable: true, get: function () { return tokens_1.borderRadius; } });
53
+ Object.defineProperty(exports, "breakpoints", { enumerable: true, get: function () { return tokens_1.breakpoints; } });
54
+ Object.defineProperty(exports, "colors", { enumerable: true, get: function () { return tokens_1.colors; } });
55
+ Object.defineProperty(exports, "shadows", { enumerable: true, get: function () { return tokens_1.shadows; } });
56
+ Object.defineProperty(exports, "spacing", { enumerable: true, get: function () { return tokens_1.spacing; } });
57
+ Object.defineProperty(exports, "typography", { enumerable: true, get: function () { return tokens_1.typography; } });
58
+ // Export ZeroCSS utility functions
59
+ var utils_1 = require("../lib/utils");
60
+ Object.defineProperty(exports, "debounce", { enumerable: true, get: function () { return utils_1.debounce; } });
61
+ Object.defineProperty(exports, "mergeStyles", { enumerable: true, get: function () { return utils_1.mergeStyles; } });
62
+ Object.defineProperty(exports, "platformStyle", { enumerable: true, get: function () { return utils_1.platformStyle; } });
63
+ Object.defineProperty(exports, "responsiveValue", { enumerable: true, get: function () { return utils_1.responsiveValue; } });
64
+ // Export ZeroCSS theme system
65
+ var theme_1 = require("../lib/theme/theme");
66
+ Object.defineProperty(exports, "ThemeProvider", { enumerable: true, get: function () { return theme_1.ThemeProvider; } });
67
+ Object.defineProperty(exports, "createThemeColors", { enumerable: true, get: function () { return theme_1.createThemeColors; } });
68
+ Object.defineProperty(exports, "createThemeIcons", { enumerable: true, get: function () { return theme_1.createThemeIcons; } });
69
+ Object.defineProperty(exports, "createThemeStyles", { enumerable: true, get: function () { return theme_1.createThemeStyles; } });
70
+ Object.defineProperty(exports, "createThemedStyles", { enumerable: true, get: function () { return theme_1.createThemedStyles; } });
71
+ Object.defineProperty(exports, "darkTheme", { enumerable: true, get: function () { return theme_1.darkTheme; } });
72
+ Object.defineProperty(exports, "lightTheme", { enumerable: true, get: function () { return theme_1.lightTheme; } });
73
+ Object.defineProperty(exports, "usePlatformTypography", { enumerable: true, get: function () { return theme_1.usePlatformTypography; } });
74
+ Object.defineProperty(exports, "useTheme", { enumerable: true, get: function () { return theme_1.useTheme; } });
75
+ // Namespace exports for power users
76
+ exports.theme = tslib_1.__importStar(require("../lib/theme"));
77
+ exports.atomsNS = tslib_1.__importStar(require("../lib/theme/atoms"));
78
+ exports.tokens = tslib_1.__importStar(require("../lib/theme/tokens"));
79
+ exports.utils = tslib_1.__importStar(require("../lib/utils"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamplace/components",
3
- "version": "0.7.2",
3
+ "version": "0.7.7",
4
4
  "description": "Streamplace React (Native) Components",
5
5
  "main": "dist/index.js",
6
6
  "types": "src/index.tsx",
@@ -28,11 +28,14 @@
28
28
  "dependencies": {
29
29
  "@atproto/api": "^0.15.7",
30
30
  "@atproto/crypto": "^0.4.4",
31
+ "@emoji-mart/react": "^1.1.1",
31
32
  "@gorhom/bottom-sheet": "^5.1.6",
32
33
  "@react-navigation/native": "^6.1.18",
33
34
  "@rn-primitives/dropdown-menu": "^1.2.0",
34
35
  "@rn-primitives/portal": "^1.3.0",
36
+ "@rn-primitives/slider": "^1.2.0",
35
37
  "class-variance-authority": "^0.6.1",
38
+ "expo-keep-awake": "~14.1.4",
36
39
  "expo-video": "~2.2.1",
37
40
  "hls.js": "^1.5.17",
38
41
  "lucide-react-native": "^0.514.0",
@@ -41,6 +44,7 @@
41
44
  "react-native-gesture-handler": "~2.26.0",
42
45
  "react-native-reanimated": "~3.18.0",
43
46
  "react-native-safe-area-context": "5.4.1",
47
+ "react-native-svg": "15.12.0",
44
48
  "react-native-webrtc": "git+https://github.com/streamplace/react-native-webrtc.git#6b8472a771ac47f89217d327058a8a4124a6ae56",
45
49
  "react-use-websocket": "^4.13.0",
46
50
  "streamplace": "0.7.2",
@@ -50,5 +54,5 @@
50
54
  "peerDependencies": {
51
55
  "react": "*"
52
56
  },
53
- "gitHead": "b07a68160ae6da34cf47aa397b4ef452248d79e5"
57
+ "gitHead": "8c2cad31c840efeda8aa7f797f91dddbedb95397"
54
58
  }
@@ -1,6 +1,7 @@
1
- import { X } from "lucide-react-native";
1
+ import Picker from "@emoji-mart/react";
2
+ import { AtSignIcon, ExternalLink, X } from "lucide-react-native";
2
3
  import { useEffect, useMemo, useRef, useState } from "react";
3
- import { Pressable, TextInput } from "react-native";
4
+ import { Platform, Pressable, TextInput } from "react-native";
4
5
  import { ChatMessageViewHydrated } from "streamplace";
5
6
  import {
6
7
  Button,
@@ -8,30 +9,60 @@ import {
8
9
  Text,
9
10
  useChat,
10
11
  useCreateChatMessage,
12
+ useLivestream,
11
13
  useReplyToMessage,
12
14
  useSetReplyToMessage,
13
15
  View,
14
16
  } from "../../";
15
- import { bg, flex, gap, h, layout, mb, pl, pr, w } from "../../lib/theme/atoms";
17
+ import {
18
+ bg,
19
+ flex,
20
+ gap,
21
+ h,
22
+ layout,
23
+ mb,
24
+ mr,
25
+ pl,
26
+ pr,
27
+ py,
28
+ w,
29
+ } from "../../lib/theme/atoms";
16
30
  import { usePDSAgent } from "../../streamplace-store/xrpc";
17
31
  import { Textarea } from "../ui/textarea";
18
32
  import { RenderChatMessage } from "./chat-message";
33
+ import { EmojiData, EmojiSuggestions } from "./emoji-suggestions";
19
34
  import { MentionSuggestions } from "./mention-suggestions";
20
35
 
36
+ const COOL_EMOJI_LIST = [
37
+ ..."😀🥸😍😘😁🥸😆🥸😜🥸😂😅🥸🙂🤫😱🥸🤣😗😄🥸😎🤓😲😯😰🥸😥🥸😣🥸😞😓🥸😩😩🥸😤🥱",
38
+ ];
39
+
21
40
  export function ChatBox({
22
41
  isPopout,
23
42
  chatBoxStyle,
43
+ emojiData,
44
+ setIsChatVisible,
24
45
  }: {
25
46
  isPopout?: boolean;
26
47
  chatBoxStyle?: any;
48
+ emojiData: EmojiData;
49
+ setIsChatVisible?: (visible: boolean) => void;
27
50
  }) {
28
51
  const [submitting, setSubmitting] = useState(false);
29
52
  const [message, setMessage] = useState("");
30
53
  const [showSuggestions, setShowSuggestions] = useState(false);
54
+ const [showEmojiSuggestions, setShowEmojiSuggestions] = useState(false);
55
+ const [showEmojiSelector, setShowEmojiSelector] = useState(false);
56
+ const [emojiIconIndex, setEmojiIconIndex] = useState(
57
+ Math.floor(Math.random() * COOL_EMOJI_LIST.length),
58
+ );
31
59
  const [highlightedIndex, setHighlightedIndex] = useState(0);
32
60
  const [filteredAuthors, setFilteredAuthors] = useState<Map<string, any>>(
33
61
  new Map(),
34
62
  );
63
+ const [filteredEmojis, setFilteredEmojis] = useState<any[]>([]);
64
+
65
+ let linfo = useLivestream();
35
66
 
36
67
  const chat = useChat();
37
68
  const createChatMessage = useCreateChatMessage();
@@ -63,25 +94,145 @@ export function ChatBox({
63
94
  setShowSuggestions(false);
64
95
  };
65
96
 
97
+ const handleEmojiSelect = (emoji: any) => {
98
+ const beforeColon = message.slice(0, message.lastIndexOf(":"));
99
+ setMessage(`${beforeColon}${emoji.skins[0]?.native} `);
100
+ setShowEmojiSuggestions(false);
101
+ };
102
+
66
103
  const updateSuggestions = (text: string) => {
104
+ // Handle mentions
67
105
  const atIndex = text.lastIndexOf("@");
68
- if (atIndex === -1 || !authors) {
106
+ if (atIndex !== -1 && authors) {
107
+ const searchText = text.slice(atIndex + 1).toLowerCase();
108
+ const filteredAuthorsMap = new Map(
109
+ Array.from(authors.entries()).filter(([handle]) =>
110
+ handle.toLowerCase().includes(searchText),
111
+ ),
112
+ );
113
+ setFilteredAuthors(filteredAuthorsMap);
114
+ setHighlightedIndex(0);
115
+ setShowSuggestions(filteredAuthorsMap.size > 0);
116
+ setShowEmojiSuggestions(false);
117
+ } else {
69
118
  setShowSuggestions(false);
70
- return;
71
119
  }
72
120
 
73
- const searchText = text.slice(atIndex + 1).toLowerCase();
121
+ const colonIndex = text.lastIndexOf(":");
122
+ if (colonIndex !== -1) {
123
+ const searchText = text.slice(colonIndex + 1).toLowerCase();
124
+ if (searchText.length > 0) {
125
+ const aliasMatches = Object.entries(emojiData.aliases)
126
+ .map(([alias, emojiId]) => {
127
+ const aliasLower = alias.toLowerCase();
128
+ if (aliasLower === searchText) {
129
+ return { emojiId, alias, matchType: 0, index: 0 };
130
+ } else if (aliasLower.startsWith(searchText)) {
131
+ return { emojiId, alias, matchType: 1, index: 0 };
132
+ } else if (aliasLower.includes(searchText)) {
133
+ return {
134
+ emojiId,
135
+ alias,
136
+ matchType: 2,
137
+ index: aliasLower.indexOf(searchText),
138
+ }; // includes
139
+ }
140
+ return null;
141
+ })
142
+ .filter(Boolean);
143
+
144
+ // Map emojiId to best alias match info
145
+ const bestAliasMatch: Record<
146
+ string,
147
+ { matchType: number; index: number; alias: string }
148
+ > = {};
149
+ for (const match of aliasMatches) {
150
+ if (!match) continue;
151
+ const prev = bestAliasMatch[match.emojiId];
152
+ if (
153
+ !prev ||
154
+ match?.matchType < prev.matchType ||
155
+ (match.matchType === prev.matchType && match.index < prev.index)
156
+ ) {
157
+ bestAliasMatch[match.emojiId] = match;
158
+ }
159
+ }
74
160
 
75
- const filteredAuthorsMap = new Map(
76
- Array.from(authors.entries()).filter(([handle]) =>
77
- handle.toLowerCase().includes(searchText),
78
- ),
79
- );
161
+ // Collect all matching emojis by id, name, keywords, or alias
162
+ const allEmojis = Object.values(emojiData.emojis);
163
+ const filtered = allEmojis
164
+ .map((emoji: any) => {
165
+ // Check alias match
166
+ const aliasMatch = bestAliasMatch[emoji.id];
167
+ if (aliasMatch) {
168
+ return {
169
+ emoji,
170
+ sort: [aliasMatch.matchType, aliasMatch.index, 0],
171
+ };
172
+ }
173
+ // Check id, name, keywords
174
+ if (emoji.id.toLowerCase() === searchText) {
175
+ return { emoji, sort: [3, 0, 0] }; // exact id
176
+ }
177
+ if (emoji.id.toLowerCase().startsWith(searchText)) {
178
+ return { emoji, sort: [4, 0, 0] }; // startsWith id
179
+ }
180
+ if (emoji.id.toLowerCase().includes(searchText)) {
181
+ return {
182
+ emoji,
183
+ sort: [5, emoji.id.toLowerCase().indexOf(searchText), 0],
184
+ }; // includes id
185
+ }
186
+ if (emoji.name.toLowerCase().includes(searchText)) {
187
+ return {
188
+ emoji,
189
+ sort: [6, emoji.name.toLowerCase().indexOf(searchText), 0],
190
+ };
191
+ }
192
+ if (
193
+ emoji.keywords &&
194
+ emoji.keywords.some((keyword: string) =>
195
+ keyword.toLowerCase().includes(searchText),
196
+ )
197
+ ) {
198
+ return { emoji, sort: [7, 0, 0] };
199
+ }
200
+ return null;
201
+ })
202
+ .filter(Boolean)
203
+ // Remove duplicates by emoji id (keep best match)
204
+ .reduce((acc: any[], curr: any) => {
205
+ if (!acc.find((e) => e.emoji.id === curr.emoji.id)) {
206
+ acc.push(curr);
207
+ }
208
+ return acc;
209
+ }, [])
210
+ // Sort by alias match type, then position, then fallback
211
+ .sort((a, b) => {
212
+ for (let i = 0; i < a.sort.length; ++i) {
213
+ if (a.sort[i] !== b.sort[i]) return a.sort[i] - b.sort[i];
214
+ }
215
+ return 0;
216
+ })
217
+ .slice(0, 10) // Limit to 10 results
218
+ .map((entry) => entry.emoji);
80
219
 
81
- setFilteredAuthors(filteredAuthorsMap);
220
+ setFilteredEmojis(filtered);
221
+ setHighlightedIndex(0);
222
+ setShowEmojiSuggestions(filtered.length > 0);
223
+ setShowSuggestions(false);
224
+ } else {
225
+ setShowEmojiSuggestions(false);
226
+ }
227
+ } else {
228
+ setShowEmojiSuggestions(false);
229
+ }
82
230
 
83
- setHighlightedIndex(0);
84
- setShowSuggestions(filteredAuthorsMap.size > 0);
231
+ // If neither mention nor emoji, hide all suggestions
232
+ if (atIndex === -1 && colonIndex === -1) {
233
+ setShowSuggestions(false);
234
+ setShowEmojiSuggestions(false);
235
+ }
85
236
  };
86
237
 
87
238
  const submit = () => {
@@ -110,10 +261,11 @@ export function ChatBox({
110
261
  layout.flex.row,
111
262
  layout.flex.alignCenter,
112
263
  layout.flex.spaceBetween,
113
- h[12],
114
264
  pl[2],
115
- pr[10],
265
+ pr[6],
266
+ mr[6],
116
267
  mb[2],
268
+ py[1],
117
269
  bg.gray[800],
118
270
  { borderRadius: 16 },
119
271
  ]}
@@ -140,6 +292,35 @@ export function ChatBox({
140
292
  </Pressable>
141
293
  </View>
142
294
  )}
295
+ {showEmojiSelector && (
296
+ <>
297
+ {/* Overlay to catch outside clicks */}
298
+ <Pressable
299
+ style={{
300
+ position: "absolute",
301
+ top: 0,
302
+ left: 0,
303
+ right: 0,
304
+ bottom: 0,
305
+ zIndex: 200,
306
+ }}
307
+ onPress={() => setShowEmojiSelector(false)}
308
+ />
309
+ <View
310
+ style={{
311
+ position: "absolute",
312
+ bottom: "100%",
313
+ left: 0,
314
+ zIndex: 2001,
315
+ }}
316
+ >
317
+ <Picker
318
+ data={emojiData}
319
+ onEmojiSelect={(e) => setMessage(message + e.native)}
320
+ />
321
+ </View>
322
+ </>
323
+ )}
143
324
  <View style={[layout.flex.row, layout.flex.alignCenter, gap.all[2]]}>
144
325
  <Textarea
145
326
  ref={textAreaRef}
@@ -160,16 +341,40 @@ export function ChatBox({
160
341
  if (handles.length > 0) {
161
342
  handleMentionSelect(handles[highlightedIndex]);
162
343
  }
163
- } else submit();
344
+ } else if (showEmojiSuggestions) {
345
+ k.preventDefault();
346
+ if (filteredEmojis.length > 0) {
347
+ handleEmojiSelect(filteredEmojis[highlightedIndex]);
348
+ }
349
+ } else {
350
+ submit();
351
+ }
164
352
  } else if (k.nativeEvent.key === "ArrowUp") {
165
- setHighlightedIndex((prev) => Math.max(prev - 1, 0));
353
+ if (showSuggestions || showEmojiSuggestions) {
354
+ k.preventDefault();
355
+ setHighlightedIndex((prev) => Math.max(prev - 1, 0));
356
+ }
166
357
  } else if (k.nativeEvent.key === "ArrowDown") {
167
- setHighlightedIndex((prev) =>
168
- Math.min(
169
- prev + 1,
170
- Array.from(filteredAuthors.keys()).length - 1,
171
- ),
172
- );
358
+ if (showSuggestions) {
359
+ k.preventDefault();
360
+ setHighlightedIndex((prev) =>
361
+ Math.min(
362
+ prev + 1,
363
+ Array.from(filteredAuthors.keys()).length - 1,
364
+ ),
365
+ );
366
+ } else if (showEmojiSuggestions) {
367
+ k.preventDefault();
368
+ setHighlightedIndex((prev) =>
369
+ Math.min(prev + 1, filteredEmojis.length - 1),
370
+ );
371
+ }
372
+ } else if (k.nativeEvent.key === "Escape") {
373
+ if (showSuggestions || showEmojiSuggestions) {
374
+ k.preventDefault();
375
+ setShowSuggestions(false);
376
+ setShowEmojiSuggestions(false);
377
+ }
173
378
  }
174
379
  }}
175
380
  style={[chatBoxStyle]}
@@ -185,11 +390,76 @@ export function ChatBox({
185
390
  </View>
186
391
  {showSuggestions && (
187
392
  <MentionSuggestions
188
- authors={filteredAuthors || []}
393
+ authors={filteredAuthors || new Map()}
189
394
  highlightedIndex={highlightedIndex}
190
395
  onSelect={handleMentionSelect}
191
396
  />
192
397
  )}
398
+ {showEmojiSuggestions && (
399
+ <EmojiSuggestions
400
+ emojis={filteredEmojis}
401
+ highlightedIndex={highlightedIndex}
402
+ onSelect={handleEmojiSelect}
403
+ />
404
+ )}
405
+ {Platform.OS === "web" && (
406
+ <View
407
+ style={[
408
+ layout.flex.row,
409
+ mb[2],
410
+ gap.all[2],
411
+ { justifyContent: "flex-end" },
412
+ ]}
413
+ >
414
+ <Button
415
+ variant="secondary"
416
+ style={{ borderRadius: 16, height: 36, maxWidth: 36 }}
417
+ onPress={() => {
418
+ // if the last character is not @, add it
419
+ !message.endsWith("@") && setMessage(message + "@");
420
+ // get all the text after the last @
421
+ const atIndex = message.lastIndexOf("@");
422
+ const searchText = message.slice(atIndex + 1).toLowerCase();
423
+ updateSuggestions(searchText);
424
+ setShowSuggestions(true);
425
+ // focus the textarea
426
+ textAreaRef.current?.focus();
427
+ }}
428
+ >
429
+ <AtSignIcon size={20} color="white" />
430
+ </Button>
431
+ <Pressable
432
+ onHoverOut={() => {
433
+ setEmojiIconIndex(
434
+ Math.floor(Math.random() * COOL_EMOJI_LIST.length),
435
+ );
436
+ }}
437
+ >
438
+ <Button
439
+ variant="secondary"
440
+ style={{ borderRadius: 16, height: 36, maxWidth: 36 }}
441
+ onPress={() => setShowEmojiSelector(!showEmojiSelector)}
442
+ >
443
+ <Text>{COOL_EMOJI_LIST[emojiIconIndex]}</Text>
444
+ </Button>
445
+ </Pressable>
446
+ {!isPopout && (
447
+ <Button
448
+ variant="secondary"
449
+ style={{ borderRadius: 16, height: 36, maxWidth: 36 }}
450
+ onPress={() => {
451
+ if (!linfo) return;
452
+ const u = new URL(window.location.href);
453
+ u.pathname = `/chat-popout/${linfo?.author?.did}`;
454
+ window.open(u.toString(), "_blank", "popup=true");
455
+ setIsChatVisible?.(false);
456
+ }}
457
+ >
458
+ <ExternalLink size={16} />
459
+ </Button>
460
+ )}
461
+ </View>
462
+ )}
193
463
  </View>
194
464
  );
195
465
  }
@@ -17,7 +17,7 @@ import {
17
17
  pl,
18
18
  w,
19
19
  } from "../../lib/theme/atoms";
20
- import { atoms, layout } from "../ui";
20
+ import { atoms, colors, layout } from "../ui";
21
21
 
22
22
  interface Facet {
23
23
  index: {
@@ -35,7 +35,7 @@ import { useLivestreamStore } from "../../livestream-store";
35
35
  import { Text } from "../ui/text";
36
36
 
37
37
  const getRgbColor = (color?: { red: number; green: number; blue: number }) =>
38
- color ? `rgb(${color.red}, ${color.green}, ${color.blue})` : "$accentColor";
38
+ color ? `rgb(${color.red}, ${color.green}, ${color.blue})` : colors.gray[500];
39
39
 
40
40
  const segmentedObject = (
41
41
  obj: RichtextSegment,
@@ -59,7 +59,6 @@ const segmentedObject = (
59
59
  } else if (ftr.$type === "app.bsky.richtext.facet#mention") {
60
60
  let mtnftr = ftr as $Typed<Mention>;
61
61
  const profile = userCache?.[mtnftr.did];
62
- console.log(profile, mtnftr.did, userCache);
63
62
  return (
64
63
  <Text
65
64
  key={`mention-${index}`}
@@ -115,7 +114,6 @@ export const RenderChatMessage = memo(
115
114
  hour12: false,
116
115
  });
117
116
  }, []);
118
-
119
117
  return (
120
118
  <>
121
119
  {item.replyTo && showReply && (
@@ -142,7 +140,7 @@ export const RenderChatMessage = memo(
142
140
  </Text>{" "}
143
141
  <Text
144
142
  style={{
145
- color: atoms.colors.gray[300],
143
+ color: colors.gray[300],
146
144
  fontStyle: "italic",
147
145
  }}
148
146
  >
@@ -156,7 +154,7 @@ export const RenderChatMessage = memo(
156
154
  <Text
157
155
  style={{
158
156
  fontVariant: ["tabular-nums"],
159
- color: atoms.colors.gray[300],
157
+ color: colors.gray[400],
160
158
  }}
161
159
  >
162
160
  {formatTime(item.record.createdAt)}
@@ -186,7 +184,8 @@ export const RenderChatMessage = memo(
186
184
  (prevProps, nextProps) => {
187
185
  return (
188
186
  prevProps.item.author.handle === nextProps.item.author.handle &&
189
- prevProps.item.record.text === nextProps.item.record.text
187
+ prevProps.item.record.text === nextProps.item.record.text &&
188
+ prevProps.item.uri === nextProps.item.uri
190
189
  );
191
190
  },
192
191
  );