+554
-293
templates/status.html
+554
-293
templates/status.html
···
190
190
</form>
191
191
</div>
192
192
193
-
<!-- Emoji Picker (hidden by default) -->
194
-
<div class="emoji-picker" id="emoji-picker" style="display: none;">
195
-
<div class="emoji-search-container">
196
-
<input type="text"
197
-
id="emoji-search"
198
-
class="emoji-search"
199
-
placeholder="Search emojis..."
200
-
autocomplete="off">
201
-
</div>
202
-
<div class="emoji-categories" id="emoji-categories">
203
-
<button class="category-btn active" data-category="frequent">Frequent</button>
204
-
<button class="category-btn" data-category="custom">Custom</button>
205
-
<button class="category-btn" data-category="people">People</button>
206
-
<button class="category-btn" data-category="nature">Nature</button>
207
-
<button class="category-btn" data-category="food">Food</button>
208
-
<button class="category-btn" data-category="activity">Activity</button>
209
-
<button class="category-btn" data-category="travel">Travel</button>
210
-
<button class="category-btn" data-category="objects">Objects</button>
211
-
<button class="category-btn" data-category="symbols">Symbols</button>
212
-
<button class="category-btn" data-category="flags">Flags</button>
213
-
</div>
214
-
<div class="emoji-grid" id="emoji-grid">
215
-
<!-- Will be populated by JavaScript -->
193
+
<!-- Emoji Picker Modal -->
194
+
<div class="emoji-picker-overlay hidden" id="emoji-picker-overlay" aria-hidden="true">
195
+
<div class="emoji-picker" id="emoji-picker" role="dialog" aria-modal="true" aria-labelledby="emoji-picker-title">
196
+
<div class="emoji-picker-header">
197
+
<div>
198
+
<h2 id="emoji-picker-title">pick an emoji</h2>
199
+
<p class="emoji-picker-subtitle">custom and unicode options side by side</p>
200
+
</div>
201
+
<button type="button" class="emoji-picker-close" id="emoji-picker-close" aria-label="close emoji picker">✕</button>
202
+
</div>
203
+
<div class="emoji-search-container">
204
+
<input type="text"
205
+
id="emoji-search"
206
+
class="emoji-search"
207
+
placeholder="Search emojis..."
208
+
autocomplete="off">
209
+
</div>
210
+
<div class="emoji-picker-body">
211
+
<div class="emoji-categories" id="emoji-categories">
212
+
<button type="button" class="category-btn active" data-category="frequent">Frequent</button>
213
+
<button type="button" class="category-btn" data-category="custom">Custom</button>
214
+
<button type="button" class="category-btn" data-category="people">People</button>
215
+
<button type="button" class="category-btn" data-category="nature">Nature</button>
216
+
<button type="button" class="category-btn" data-category="food">Food</button>
217
+
<button type="button" class="category-btn" data-category="activity">Activity</button>
218
+
<button type="button" class="category-btn" data-category="travel">Travel</button>
219
+
<button type="button" class="category-btn" data-category="objects">Objects</button>
220
+
<button type="button" class="category-btn" data-category="symbols">Symbols</button>
221
+
<button type="button" class="category-btn" data-category="flags">Flags</button>
222
+
</div>
223
+
<div class="emoji-grid" id="emoji-grid">
224
+
<!-- Will be populated by JavaScript -->
225
+
</div>
226
+
</div>
216
227
</div>
217
228
</div>
218
229
···
457
468
font-family: var(--font-family) !important;
458
469
}
459
470
471
+
body.modal-open {
472
+
overflow: hidden;
473
+
position: fixed;
474
+
width: 100%;
475
+
}
476
+
460
477
:root {
461
478
--bg-primary: #ffffff;
462
479
--bg-secondary: #f8f9fa;
···
607
624
.handle-link::before {
608
625
content: '';
609
626
position: absolute;
627
+
top: -2px;
628
+
right: -2px;
629
+
bottom: -2px;
630
+
left: -2px;
610
631
inset: -2px;
611
632
border-radius: var(--radius-sm);
612
633
background: linear-gradient(45deg, var(--accent), transparent);
···
1049
1070
}
1050
1071
1051
1072
/* Emoji Picker */
1073
+
.emoji-picker-overlay {
1074
+
position: fixed;
1075
+
top: 0;
1076
+
right: 0;
1077
+
bottom: 0;
1078
+
left: 0;
1079
+
inset: 0; /* Modern browsers */
1080
+
background: rgba(6, 6, 8, 0.75);
1081
+
backdrop-filter: blur(6px);
1082
+
display: flex;
1083
+
align-items: center;
1084
+
justify-content: center;
1085
+
padding: clamp(1rem, 6vw, 2.5rem);
1086
+
z-index: 1400;
1087
+
}
1088
+
1089
+
.emoji-picker-overlay.hidden {
1090
+
display: none;
1091
+
}
1092
+
1052
1093
.emoji-picker {
1053
-
position: absolute;
1094
+
position: relative;
1095
+
z-index: 1;
1096
+
width: min(960px, 94vw);
1097
+
height: min(90vh, 820px);
1054
1098
background: var(--bg-secondary);
1055
1099
border: 1px solid var(--border-color);
1056
-
border-radius: var(--radius);
1057
-
padding: 1rem;
1058
-
box-shadow: var(--shadow-md);
1059
-
width: 320px;
1060
-
max-height: 400px;
1061
-
overflow-y: auto;
1062
-
z-index: 1000;
1100
+
border-radius: clamp(var(--radius), 2vw, 24px);
1101
+
box-shadow: 0 32px 80px rgba(0, 0, 0, 0.45);
1102
+
display: flex;
1103
+
flex-direction: column;
1104
+
gap: 1.25rem;
1105
+
padding: clamp(1.25rem, 5vw, 2.5rem);
1106
+
overflow: hidden;
1107
+
}
1108
+
1109
+
1110
+
.emoji-picker-header {
1111
+
display: flex;
1112
+
align-items: flex-start;
1113
+
justify-content: space-between;
1114
+
gap: 1rem;
1115
+
}
1116
+
1117
+
.emoji-picker-header h2 {
1118
+
margin: 0;
1119
+
font-size: 1.5rem;
1120
+
color: var(--text-primary);
1121
+
}
1122
+
1123
+
.emoji-picker-subtitle {
1124
+
margin: 0.25rem 0 0;
1125
+
font-size: 0.875rem;
1126
+
color: var(--text-tertiary);
1127
+
}
1128
+
1129
+
.emoji-picker-close {
1130
+
border: 1px solid var(--border-color);
1131
+
background: var(--bg-tertiary);
1132
+
color: var(--text-secondary);
1133
+
border-radius: 999px;
1134
+
width: 2.25rem;
1135
+
height: 2.25rem;
1136
+
display: flex;
1137
+
align-items: center;
1138
+
justify-content: center;
1139
+
cursor: pointer;
1140
+
transition: all 0.2s ease;
1141
+
}
1142
+
1143
+
.emoji-picker-close:hover,
1144
+
.emoji-picker-close:focus-visible {
1145
+
color: white;
1146
+
background: var(--accent);
1147
+
border-color: transparent;
1148
+
outline: none;
1063
1149
}
1064
1150
1065
1151
.emoji-search-container {
1066
-
margin-bottom: 1rem;
1152
+
margin: 0;
1067
1153
}
1068
1154
1069
1155
.emoji-search {
1070
1156
width: 100%;
1071
-
padding: 0.5rem 0.75rem;
1157
+
padding: 0.75rem 1rem;
1072
1158
background: var(--bg-primary);
1073
1159
border: 1px solid var(--border-color);
1074
-
border-radius: var(--radius-sm);
1160
+
border-radius: var(--radius);
1075
1161
color: var(--text-primary);
1076
-
font-size: 0.875rem;
1162
+
font-size: 0.95rem;
1077
1163
outline: none;
1078
-
transition: border-color 0.2s;
1164
+
transition: border-color 0.2s ease, box-shadow 0.2s ease;
1079
1165
}
1080
1166
1081
1167
.emoji-search:focus {
1082
1168
border-color: var(--accent);
1083
-
box-shadow: 0 0 0 2px rgba(74, 158, 255, 0.1);
1169
+
box-shadow: 0 0 0 2px rgba(74, 158, 255, 0.12);
1084
1170
}
1085
1171
1086
1172
.emoji-search::placeholder {
1087
1173
color: var(--text-tertiary);
1088
1174
}
1089
1175
1176
+
.emoji-picker-body {
1177
+
flex: 1;
1178
+
display: flex;
1179
+
flex-direction: column;
1180
+
gap: 0.75rem;
1181
+
min-height: 0;
1182
+
}
1183
+
1090
1184
.emoji-categories {
1091
1185
display: flex;
1092
-
gap: 0.25rem;
1093
-
margin-bottom: 1rem;
1186
+
gap: 0.75rem;
1094
1187
overflow-x: auto;
1095
1188
padding-bottom: 0.5rem;
1189
+
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
1190
+
scrollbar-width: none;
1096
1191
}
1097
1192
1098
1193
.emoji-categories.hidden {
1099
1194
display: none;
1100
1195
}
1101
1196
1197
+
.emoji-categories::-webkit-scrollbar {
1198
+
display: none;
1199
+
}
1200
+
1102
1201
.category-btn {
1103
1202
background: transparent;
1104
-
border: none;
1203
+
border: 1px solid transparent;
1105
1204
color: var(--text-secondary);
1106
-
font-size: 0.75rem;
1107
-
padding: 0.25rem 0.5rem;
1205
+
font-size: 0.8rem;
1206
+
padding: 0.4rem 0.75rem;
1108
1207
cursor: pointer;
1109
1208
white-space: nowrap;
1110
-
border-radius: var(--radius-sm);
1111
-
transition: all 0.2s;
1209
+
border-radius: 999px;
1210
+
transition: all 0.2s ease;
1112
1211
}
1113
1212
1114
-
.category-btn:hover {
1115
-
background: var(--bg-tertiary);
1213
+
.category-btn:hover,
1214
+
.category-btn:focus-visible {
1215
+
border-color: var(--accent);
1216
+
color: var(--accent);
1217
+
outline: none;
1116
1218
}
1117
1219
1118
1220
.category-btn.active {
1119
1221
background: var(--accent);
1120
1222
color: white;
1223
+
border-color: transparent;
1121
1224
}
1122
1225
1123
1226
.emoji-grid {
1227
+
flex: 1;
1124
1228
display: grid;
1125
-
grid-template-columns: repeat(auto-fill, 2rem);
1126
-
gap: 0.25rem;
1127
-
justify-content: start;
1229
+
grid-template-columns: repeat(auto-fill, minmax(min(84px, 18vw), 1fr));
1230
+
gap: 0.75rem;
1231
+
padding-right: 0.25rem;
1232
+
overflow-y: auto;
1233
+
scrollbar-width: none;
1234
+
}
1235
+
1236
+
.emoji-grid::-webkit-scrollbar {
1237
+
display: none;
1128
1238
}
1129
1239
1130
1240
.emoji-option {
1131
1241
background: transparent;
1132
1242
border: none;
1133
-
font-size: 1.5rem;
1134
-
padding: 0.25rem;
1243
+
font-size: 2.4rem;
1135
1244
cursor: pointer;
1136
-
border-radius: var(--radius-sm);
1137
-
transition: background 0.2s;
1245
+
transition: transform 0.15s ease;
1138
1246
display: flex;
1139
1247
align-items: center;
1140
1248
justify-content: center;
1141
-
width: 2rem;
1142
-
height: 2rem;
1249
+
width: 100%;
1250
+
aspect-ratio: 1;
1143
1251
}
1144
1252
1145
1253
.emoji-option:hover {
1146
-
background: var(--bg-tertiary);
1254
+
transform: translateY(-3px) scale(1.05);
1147
1255
}
1148
1256
1149
-
/* Custom emoji styles */
1150
-
.emoji-option.custom-emoji {
1151
-
padding: 0.125rem;
1257
+
.emoji-option:focus-visible {
1258
+
outline: 2px solid var(--accent);
1259
+
outline-offset: 6px;
1260
+
border-radius: 16px;
1152
1261
}
1153
1262
1154
1263
.emoji-option.custom-emoji img {
1155
-
width: 1.75rem;
1156
-
height: 1.75rem;
1264
+
width: 75%;
1265
+
height: 75%;
1157
1266
object-fit: contain;
1267
+
}
1268
+
1269
+
.emoji-grid-placeholder,
1270
+
.emoji-loading-message {
1271
+
text-align: center;
1272
+
color: var(--text-tertiary);
1273
+
padding: 2rem 1rem;
1274
+
font-size: 0.95rem;
1275
+
}
1276
+
1277
+
.emoji-picker-overlay::before {
1278
+
content: '';
1279
+
position: fixed;
1280
+
top: 0;
1281
+
right: 0;
1282
+
bottom: 0;
1283
+
left: 0;
1284
+
}
1285
+
1286
+
@media (max-width: 640px) {
1287
+
.emoji-picker-overlay {
1288
+
padding: 0;
1289
+
align-items: stretch;
1290
+
}
1291
+
1292
+
.emoji-picker {
1293
+
width: 100%;
1294
+
height: 100%;
1295
+
max-height: none;
1296
+
border-radius: 0;
1297
+
padding: 1.5rem 1rem;
1298
+
gap: 1rem;
1299
+
}
1300
+
1301
+
.emoji-grid {
1302
+
grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
1303
+
gap: 0.5rem;
1304
+
}
1305
+
}
1306
+
1307
+
@media (prefers-reduced-motion: reduce) {
1308
+
.emoji-picker-overlay {
1309
+
backdrop-filter: none;
1310
+
background: rgba(6, 6, 8, 0.85);
1311
+
}
1312
+
1313
+
.emoji-picker,
1314
+
.emoji-option {
1315
+
transition: none;
1316
+
}
1158
1317
}
1159
1318
1160
1319
/* Clear Picker */
···
1477
1636
.webhook-modal.hidden { display: none; }
1478
1637
.webhook-modal {
1479
1638
position: fixed;
1480
-
inset: 0;
1639
+
top: 0;
1640
+
right: 0;
1641
+
bottom: 0;
1642
+
left: 0;
1643
+
inset: 0; /* Modern browsers */
1481
1644
background: rgba(0,0,0,0.6);
1482
1645
z-index: 1000;
1483
1646
display: flex;
···
1937
2100
1938
2101
// Emoji picker
1939
2102
const emojiTrigger = document.getElementById('emoji-trigger');
2103
+
const emojiPickerOverlay = document.getElementById('emoji-picker-overlay');
1940
2104
const emojiPicker = document.getElementById('emoji-picker');
2105
+
const emojiPickerClose = document.getElementById('emoji-picker-close');
1941
2106
const emojiGrid = document.getElementById('emoji-grid');
1942
2107
const selectedEmoji = document.getElementById('selected-emoji');
1943
2108
const statusInput = document.getElementById('status-input');
1944
-
2109
+
const emojiSearch = document.getElementById('emoji-search');
2110
+
const emojiCategories = document.getElementById('emoji-categories');
2111
+
let lastFocusBeforeEmojiPicker = null;
2112
+
let scrollPosition = 0;
2113
+
let isScrollLocked = false;
2114
+
let keydownBound = false;
2115
+
1945
2116
// Clear time picker
1946
2117
const clearAfterBtn = document.getElementById('clear-after-btn');
1947
2118
const clearPicker = document.getElementById('clear-picker');
1948
2119
const clearText = document.getElementById('clear-text');
1949
2120
const expiresSelect = document.getElementById('expires_in');
2121
+
2122
+
const isEmojiPickerOpen = () => emojiPickerOverlay && !emojiPickerOverlay.classList.contains('hidden');
2123
+
2124
+
const handleKeydown = (e) => {
2125
+
if (e.key === 'Escape' && isEmojiPickerOpen()) {
2126
+
closeEmojiPicker();
2127
+
}
2128
+
};
1950
2129
2130
+
// Clean up event listener on page unload
2131
+
window.addEventListener('beforeunload', () => {
2132
+
if (keydownBound) {
2133
+
document.removeEventListener('keydown', handleKeydown);
2134
+
keydownBound = false;
2135
+
}
2136
+
});
2137
+
2138
+
const openEmojiPicker = () => {
2139
+
if (!emojiPickerOverlay || !emojiPicker) return;
2140
+
2141
+
lastFocusBeforeEmojiPicker = document.activeElement;
2142
+
scrollPosition = window.scrollY || document.documentElement.scrollTop || 0;
2143
+
2144
+
emojiPickerOverlay.classList.remove('hidden');
2145
+
emojiPickerOverlay.setAttribute('aria-hidden', 'false');
2146
+
document.body.classList.add('modal-open');
2147
+
document.body.style.top = `-${scrollPosition}px`;
2148
+
isScrollLocked = true;
2149
+
if (clearPicker) clearPicker.style.display = 'none';
2150
+
2151
+
if (emojiSearch) {
2152
+
emojiSearch.value = '';
2153
+
if (emojiCategories) emojiCategories.classList.remove('hidden');
2154
+
requestAnimationFrame(() => {
2155
+
try { emojiSearch.focus(); } catch (_) {}
2156
+
});
2157
+
}
2158
+
2159
+
ensureCustomEmojis({ showLoading: false, showErrors: false }).then(() => {
2160
+
loadEmojiCategory('frequent');
2161
+
});
2162
+
2163
+
if (!keydownBound) {
2164
+
document.addEventListener('keydown', handleKeydown);
2165
+
keydownBound = true;
2166
+
}
2167
+
};
2168
+
2169
+
const closeEmojiPicker = () => {
2170
+
if (!emojiPickerOverlay) return;
2171
+
emojiPickerOverlay.classList.add('hidden');
2172
+
emojiPickerOverlay.setAttribute('aria-hidden', 'true');
2173
+
document.body.classList.remove('modal-open');
2174
+
document.body.style.top = '';
2175
+
if (isScrollLocked) {
2176
+
window.scrollTo(0, scrollPosition);
2177
+
}
2178
+
scrollPosition = 0;
2179
+
isScrollLocked = false;
2180
+
if (lastFocusBeforeEmojiPicker && typeof lastFocusBeforeEmojiPicker.focus === 'function') {
2181
+
const focusTarget = lastFocusBeforeEmojiPicker;
2182
+
requestAnimationFrame(() => {
2183
+
try { focusTarget.focus(); } catch (_) {}
2184
+
});
2185
+
}
2186
+
lastFocusBeforeEmojiPicker = null;
2187
+
2188
+
if (keydownBound) {
2189
+
document.removeEventListener('keydown', handleKeydown);
2190
+
keydownBound = false;
2191
+
}
2192
+
};
2193
+
1951
2194
// Show emoji picker
1952
-
if (emojiTrigger && emojiPicker) {
2195
+
if (emojiTrigger && emojiPickerOverlay) {
1953
2196
emojiTrigger.addEventListener('click', (e) => {
2197
+
e.preventDefault();
1954
2198
e.stopPropagation();
1955
-
const wasHidden = emojiPicker.style.display === 'none';
1956
-
emojiPicker.style.display = wasHidden ? 'block' : 'none';
1957
-
clearPicker.style.display = 'none';
1958
-
1959
-
if (wasHidden) {
1960
-
// Position picker
1961
-
const rect = emojiTrigger.getBoundingClientRect();
1962
-
emojiPicker.style.top = rect.bottom + 'px';
1963
-
emojiPicker.style.left = rect.left + 'px';
1964
-
1965
-
// Clear search and show categories
1966
-
const emojiSearch = document.getElementById('emoji-search');
1967
-
if (emojiSearch) {
1968
-
emojiSearch.value = '';
1969
-
document.getElementById('emoji-categories').classList.remove('hidden');
1970
-
}
1971
-
1972
-
// Load frequent emojis by default (wait for custom emojis to be loaded first)
1973
-
loadCustomEmojis().then(() => {
1974
-
loadEmojiCategory('frequent');
1975
-
});
1976
-
1977
-
// Focus search for easy typing
1978
-
setTimeout(() => {
1979
-
if (emojiSearch) emojiSearch.focus();
1980
-
}, 50);
2199
+
if (isEmojiPickerOpen()) {
2200
+
closeEmojiPicker();
2201
+
} else {
2202
+
openEmojiPicker();
2203
+
}
2204
+
});
2205
+
}
2206
+
2207
+
if (emojiPickerClose) {
2208
+
emojiPickerClose.addEventListener('click', (e) => {
2209
+
e.preventDefault();
2210
+
closeEmojiPicker();
2211
+
});
2212
+
}
2213
+
2214
+
if (emojiPickerOverlay) {
2215
+
emojiPickerOverlay.addEventListener('click', (e) => {
2216
+
// Only close if clicking the overlay itself, not the picker content
2217
+
if (e.target === emojiPickerOverlay) {
2218
+
closeEmojiPicker();
1981
2219
}
1982
2220
});
1983
2221
}
1984
2222
1985
-
// Store custom emojis
2223
+
// Emoji data caches
1986
2224
let customEmojis = [];
1987
-
1988
-
// Load custom emojis from API
1989
-
const loadCustomEmojis = async () => {
1990
-
try {
1991
-
const response = await fetch('/api/custom-emojis');
1992
-
customEmojis = await response.json();
1993
-
} catch (e) {
1994
-
console.error('Failed to load custom emojis:', e);
2225
+
let customEmojiMap = new Map();
2226
+
let customEmojiFetchPromise = null;
2227
+
let searchDebounce;
2228
+
const SEARCH_DEBOUNCE_MS = 120;
2229
+
2230
+
const showGridMessage = (message, className = 'emoji-grid-placeholder') => {
2231
+
if (!emojiGrid) return;
2232
+
const placeholder = document.createElement('div');
2233
+
placeholder.className = className;
2234
+
placeholder.textContent = message;
2235
+
emojiGrid.replaceChildren(placeholder);
2236
+
};
2237
+
2238
+
const createCustomEmojiButton = (emoji) => {
2239
+
const button = document.createElement('button');
2240
+
button.type = 'button';
2241
+
button.className = 'emoji-option custom-emoji';
2242
+
button.dataset.emoji = `custom:${emoji.name}`;
2243
+
button.dataset.name = emoji.name;
2244
+
2245
+
const img = document.createElement('img');
2246
+
img.src = `/emojis/${emoji.filename}`;
2247
+
img.alt = emoji.name;
2248
+
img.title = emoji.name;
2249
+
button.appendChild(img);
2250
+
return button;
2251
+
};
2252
+
2253
+
const createStandardEmojiButton = (emoji, title = '') => {
2254
+
const button = document.createElement('button');
2255
+
button.type = 'button';
2256
+
button.className = 'emoji-option';
2257
+
button.dataset.emoji = emoji;
2258
+
if (title) {
2259
+
button.dataset.name = title;
2260
+
button.title = title;
1995
2261
}
2262
+
button.textContent = emoji;
2263
+
return button;
2264
+
};
2265
+
2266
+
const renderEmojiButtons = (buttons) => {
2267
+
if (!emojiGrid) return;
2268
+
const fragment = document.createDocumentFragment();
2269
+
buttons.forEach(btn => fragment.appendChild(btn));
2270
+
emojiGrid.replaceChildren(fragment);
2271
+
};
2272
+
2273
+
const fetchCustomEmojis = async () => {
2274
+
const response = await fetch('/api/custom-emojis');
2275
+
const data = await response.json();
2276
+
if (!Array.isArray(data)) {
2277
+
throw new Error('Invalid custom emoji payload');
2278
+
}
2279
+
customEmojis = data;
2280
+
customEmojiMap = new Map(data.map(emoji => [emoji.name, emoji]));
1996
2281
return customEmojis;
1997
2282
};
1998
-
1999
-
// Load custom emojis on page load
2000
-
loadCustomEmojis();
2001
2283
2002
-
// Refresh custom emojis when an admin uploads a new one
2003
-
document.addEventListener('custom-emoji-uploaded', async (e) => {
2004
-
await loadCustomEmojis();
2005
-
// If picker visible, refresh current category
2006
-
if (emojiPicker && emojiPicker.style.display !== 'none') {
2007
-
const active = document.querySelector('.emoji-categories .category-btn.active');
2008
-
const cat = active ? active.getAttribute('data-category') : 'frequent';
2009
-
loadEmojiCategory(cat || 'frequent');
2284
+
const ensureCustomEmojis = async ({ showLoading = false, showErrors = true } = {}) => {
2285
+
if (customEmojis.length) return customEmojis;
2286
+
if (showLoading) {
2287
+
showGridMessage('loading custom emojis…', 'emoji-loading-message');
2288
+
}
2289
+
if (!customEmojiFetchPromise) {
2290
+
customEmojiFetchPromise = fetchCustomEmojis()
2291
+
.catch(err => {
2292
+
console.error('Failed to load custom emojis:', err);
2293
+
customEmojis = [];
2294
+
customEmojiMap = new Map();
2295
+
if (showErrors) {
2296
+
showGridMessage('failed to load custom emojis');
2297
+
}
2298
+
throw err;
2299
+
})
2300
+
.finally(() => {
2301
+
customEmojiFetchPromise = null;
2302
+
});
2010
2303
}
2011
-
});
2012
-
2013
-
// Load emoji category
2304
+
try {
2305
+
await customEmojiFetchPromise;
2306
+
} catch (_) {
2307
+
return [];
2308
+
}
2309
+
return customEmojis;
2310
+
};
2311
+
2312
+
// Quietly preload custom emojis so the custom tab feels instant
2313
+
ensureCustomEmojis({ showLoading: false, showErrors: false }).catch(() => {});
2314
+
2315
+
const updateActiveCategory = (category) => {
2316
+
document.querySelectorAll('.category-btn').forEach(btn => {
2317
+
btn.classList.toggle('active', btn.getAttribute('data-category') === category);
2318
+
});
2319
+
};
2320
+
2014
2321
const loadEmojiCategory = async (category) => {
2322
+
if (!emojiGrid) return;
2323
+
2324
+
const buttons = [];
2325
+
const emojis = emojiData[category] || [];
2326
+
2015
2327
if (category === 'frequent') {
2016
-
// For frequent tab, we need to handle both regular and custom emojis
2017
-
const emojis = emojiData[category] || [];
2018
-
let html = '';
2019
-
2328
+
if (emojis.some(emoji => emoji.startsWith('custom:'))) {
2329
+
await ensureCustomEmojis({ showLoading: false, showErrors: false });
2330
+
}
2020
2331
for (const emoji of emojis) {
2021
2332
if (emoji.startsWith('custom:')) {
2022
-
// This is a custom emoji
2023
-
const emojiName = emoji.replace('custom:', '');
2024
-
// Find the custom emoji data
2025
-
const customEmoji = customEmojis.find(e => e.name === emojiName);
2026
-
if (customEmoji) {
2027
-
html += `<button type="button" class="emoji-option custom-emoji" data-emoji="${emoji}" data-name="${emojiName}">
2028
-
<img src="/emojis/${customEmoji.filename}" alt="${emojiName}" title="${emojiName}">
2029
-
</button>`;
2333
+
const name = emoji.replace('custom:', '');
2334
+
const custom = customEmojiMap.get(name);
2335
+
if (custom) {
2336
+
buttons.push(createCustomEmojiButton(custom));
2030
2337
}
2031
2338
} else {
2032
-
// Regular Unicode emoji
2033
-
html += `<button type="button" class="emoji-option" data-emoji="${emoji}">${emoji}</button>`;
2339
+
const slug = (window.__emojiSlugs && window.__emojiSlugs[emoji]) || '';
2340
+
buttons.push(createStandardEmojiButton(emoji, slug));
2034
2341
}
2035
2342
}
2036
-
2037
-
emojiGrid.innerHTML = html;
2038
-
2039
-
// Add click handlers for both types
2040
-
emojiGrid.querySelectorAll('.emoji-option').forEach(btn => {
2041
-
btn.addEventListener('click', (e) => {
2042
-
e.preventDefault();
2043
-
const emoji = btn.getAttribute('data-emoji');
2044
-
2045
-
if (emoji.startsWith('custom:')) {
2046
-
const img = btn.querySelector('img');
2047
-
selectedEmoji.innerHTML = `<img src="${img.src}" alt="${img.alt}" style="width: 100%; height: 100%; object-fit: contain;">`;
2048
-
} else {
2049
-
selectedEmoji.textContent = emoji;
2050
-
}
2051
-
2052
-
statusInput.value = emoji;
2053
-
emojiPicker.style.display = 'none';
2054
-
checkForChanges();
2055
-
});
2056
-
});
2057
2343
} else if (category === 'custom') {
2058
-
// Load custom image emojis
2059
-
if (customEmojis.length === 0) {
2060
-
await loadCustomEmojis();
2344
+
const data = await ensureCustomEmojis({ showLoading: true });
2345
+
if (!data.length) {
2346
+
showGridMessage('no custom emojis yet');
2347
+
updateActiveCategory(category);
2348
+
return;
2061
2349
}
2062
-
2063
-
emojiGrid.innerHTML = customEmojis.map(emoji =>
2064
-
`<button type="button" class="emoji-option custom-emoji" data-emoji="custom:${emoji.name}" data-name="${emoji.name}">
2065
-
<img src="/emojis/${emoji.filename}" alt="${emoji.name}" title="${emoji.name}">
2066
-
</button>`
2067
-
).join('');
2068
-
2069
-
// Add click handlers for custom emojis
2070
-
emojiGrid.querySelectorAll('.custom-emoji').forEach(btn => {
2071
-
btn.addEventListener('click', (e) => {
2072
-
e.preventDefault();
2073
-
const emojiName = btn.getAttribute('data-name');
2074
-
const emojiValue = btn.getAttribute('data-emoji');
2075
-
const img = btn.querySelector('img');
2076
-
2077
-
// Display the image in the selected emoji area
2078
-
selectedEmoji.innerHTML = `<img src="${img.src}" alt="${img.alt}" style="width: 100%; height: 100%; object-fit: contain;">`;
2079
-
statusInput.value = emojiValue;
2080
-
emojiPicker.style.display = 'none';
2081
-
checkForChanges();
2082
-
});
2083
-
});
2350
+
for (const emoji of data) {
2351
+
buttons.push(createCustomEmojiButton(emoji));
2352
+
}
2084
2353
} else {
2085
-
// Load standard Unicode emojis
2086
-
const emojis = emojiData[category] || [];
2087
-
emojiGrid.innerHTML = emojis.map(emoji => {
2354
+
for (const emoji of emojis) {
2088
2355
const slug = (window.__emojiSlugs && window.__emojiSlugs[emoji]) || '';
2089
-
return `<button type="button" class="emoji-option" data-emoji="${emoji}" data-name="${slug}" title="${slug}">${emoji}</button>`;
2090
-
}).join('');
2091
-
2092
-
// Add click handlers for standard emojis
2093
-
emojiGrid.querySelectorAll('.emoji-option').forEach(btn => {
2094
-
btn.addEventListener('click', (e) => {
2095
-
const emoji = e.target.getAttribute('data-emoji');
2096
-
selectedEmoji.textContent = emoji;
2097
-
statusInput.value = emoji;
2098
-
emojiPicker.style.display = 'none';
2099
-
checkForChanges();
2100
-
});
2101
-
});
2356
+
buttons.push(createStandardEmojiButton(emoji, slug));
2357
+
}
2358
+
}
2359
+
2360
+
if (buttons.length) {
2361
+
renderEmojiButtons(buttons);
2362
+
} else if (category === 'custom') {
2363
+
showGridMessage('no custom emojis yet');
2364
+
} else {
2365
+
showGridMessage('no emojis in this category');
2366
+
}
2367
+
2368
+
updateActiveCategory(category);
2369
+
};
2370
+
2371
+
const applyEmojiSelection = (button) => {
2372
+
if (!button || !selectedEmoji || !statusInput) return;
2373
+
const emojiValue = button.getAttribute('data-emoji');
2374
+
if (!emojiValue) return;
2375
+
2376
+
selectedEmoji.innerHTML = '';
2377
+
if (button.classList.contains('custom-emoji')) {
2378
+
const img = button.querySelector('img');
2379
+
if (img) {
2380
+
const previewImg = document.createElement('img');
2381
+
previewImg.src = img.src;
2382
+
previewImg.alt = img.alt;
2383
+
previewImg.title = img.title || img.alt || '';
2384
+
previewImg.style.width = '100%';
2385
+
previewImg.style.height = '100%';
2386
+
previewImg.style.objectFit = 'contain';
2387
+
selectedEmoji.appendChild(previewImg);
2388
+
}
2389
+
} else {
2390
+
selectedEmoji.textContent = emojiValue;
2102
2391
}
2103
-
2104
-
// Update active category
2105
-
document.querySelectorAll('.category-btn').forEach(btn => {
2106
-
btn.classList.toggle('active', btn.getAttribute('data-category') === category);
2392
+
2393
+
statusInput.value = emojiValue;
2394
+
if (emojiSearch) {
2395
+
emojiSearch.value = '';
2396
+
}
2397
+
if (emojiCategories) {
2398
+
emojiCategories.classList.remove('hidden');
2399
+
}
2400
+
closeEmojiPicker();
2401
+
checkForChanges();
2402
+
};
2403
+
2404
+
if (emojiGrid) {
2405
+
emojiGrid.addEventListener('click', (e) => {
2406
+
const button = e.target.closest('.emoji-option');
2407
+
if (!button || !emojiGrid.contains(button)) return;
2408
+
e.preventDefault();
2409
+
applyEmojiSelection(button);
2107
2410
});
2108
-
};
2109
-
2411
+
}
2412
+
2110
2413
// Category buttons
2111
2414
document.querySelectorAll('.category-btn').forEach(btn => {
2112
-
btn.addEventListener('click', () => {
2113
-
loadEmojiCategory(btn.getAttribute('data-category'));
2415
+
btn.addEventListener('click', async (event) => {
2416
+
event.preventDefault();
2417
+
event.stopPropagation(); // Prevent event bubbling that might close the modal
2418
+
const category = btn.getAttribute('data-category');
2419
+
await loadEmojiCategory(category);
2114
2420
});
2115
2421
});
2116
2422
···
2119
2425
clearAfterBtn.addEventListener('click', (e) => {
2120
2426
e.stopPropagation();
2121
2427
clearPicker.style.display = clearPicker.style.display === 'none' ? 'block' : 'none';
2122
-
emojiPicker.style.display = 'none';
2428
+
closeEmojiPicker();
2123
2429
2124
2430
// Position picker
2125
2431
const rect = clearAfterBtn.getBoundingClientRect();
···
2228
2534
2229
2535
// Close pickers on outside click
2230
2536
document.addEventListener('click', (e) => {
2231
-
if (!emojiPicker.contains(e.target) && e.target !== emojiTrigger) {
2232
-
emojiPicker.style.display = 'none';
2233
-
}
2234
-
if (!clearPicker.contains(e.target) && e.target !== clearAfterBtn) {
2537
+
if (clearPicker && !clearPicker.contains(e.target) && e.target !== clearAfterBtn) {
2235
2538
clearPicker.style.display = 'none';
2236
2539
}
2237
2540
});
2238
2541
2239
2542
// Search emoji function
2240
-
const searchEmojis = (query) => {
2543
+
const searchEmojis = async (rawQuery) => {
2544
+
if (!emojiGrid) return;
2545
+
const query = (rawQuery || '').trim();
2546
+
2241
2547
if (!query) {
2242
-
document.getElementById('emoji-categories').classList.remove('hidden');
2243
-
loadEmojiCategory('frequent');
2548
+
if (emojiCategories) emojiCategories.classList.remove('hidden');
2549
+
await loadEmojiCategory('frequent');
2244
2550
return;
2245
2551
}
2246
-
2247
-
// Hide categories when searching
2248
-
document.getElementById('emoji-categories').classList.add('hidden');
2249
-
2552
+
2553
+
if (emojiCategories) emojiCategories.classList.add('hidden');
2554
+
2250
2555
const lowerQuery = query.toLowerCase();
2251
-
const results = [];
2252
-
const customResults = [];
2253
-
2254
-
// Search through emoji keywords
2255
-
for (const [emoji, keywords] of Object.entries(emojiKeywords)) {
2256
-
// Check if emoji itself matches
2257
-
if (emoji.includes(query)) {
2258
-
results.push(emoji);
2259
-
continue;
2556
+
const buttons = [];
2557
+
const seen = new Set();
2558
+
2559
+
await ensureCustomEmojis({ showLoading: false, showErrors: false });
2560
+
2561
+
for (const emoji of customEmojis) {
2562
+
if (emoji.name.toLowerCase().includes(lowerQuery)) {
2563
+
buttons.push(createCustomEmojiButton(emoji));
2564
+
seen.add(`custom:${emoji.name}`);
2565
+
if (buttons.length >= 80) break;
2260
2566
}
2261
-
2262
-
// Check keywords
2263
-
for (const keyword of keywords) {
2264
-
if (keyword.includes(lowerQuery)) {
2265
-
results.push(emoji);
2266
-
break;
2567
+
}
2568
+
2569
+
if (buttons.length < 80) {
2570
+
for (const [emoji, keywords] of Object.entries(emojiKeywords)) {
2571
+
if (seen.has(emoji)) continue;
2572
+
if (emoji.includes(query) || keywords.some(keyword => keyword.includes(lowerQuery))) {
2573
+
buttons.push(createStandardEmojiButton(emoji));
2574
+
seen.add(emoji);
2575
+
if (buttons.length >= 80) break;
2267
2576
}
2268
2577
}
2269
2578
}
2270
-
2271
-
// Also add any emojis from all categories that aren't in keywords
2272
-
const allCategoryEmojis = Object.values(emojiData).flat();
2273
-
for (const emoji of allCategoryEmojis) {
2274
-
if (!emojiKeywords[emoji] && !results.includes(emoji)) {
2275
-
// For emojis without keywords, just do a basic include check
2276
-
if (emoji.includes(query)) {
2277
-
results.push(emoji);
2579
+
2580
+
if (buttons.length < 80) {
2581
+
outer: for (const categoryEmojis of Object.values(emojiData)) {
2582
+
for (const emoji of categoryEmojis) {
2583
+
if (seen.has(emoji)) continue;
2584
+
if (emoji.includes(query)) {
2585
+
buttons.push(createStandardEmojiButton(emoji));
2586
+
seen.add(emoji);
2587
+
if (buttons.length >= 80) break outer;
2588
+
}
2278
2589
}
2279
2590
}
2280
2591
}
2281
-
2282
-
// Search through custom emojis by name
2283
-
for (const customEmoji of customEmojis) {
2284
-
if (customEmoji.name.toLowerCase().includes(lowerQuery)) {
2285
-
customResults.push(customEmoji);
2286
-
}
2287
-
}
2288
-
2289
-
// Remove duplicates and limit results
2290
-
const uniqueResults = [...new Set(results)].slice(0, 50);
2291
-
const uniqueCustomResults = customResults.slice(0, 50);
2292
-
2293
-
// Display results - combine regular and custom emojis
2294
-
let resultsHtml = '';
2295
-
2296
-
// Add custom emoji results first
2297
-
if (uniqueCustomResults.length > 0) {
2298
-
resultsHtml += uniqueCustomResults.map(emoji =>
2299
-
`<button type="button" class="emoji-option custom-emoji" data-emoji="custom:${emoji.name}" data-name="${emoji.name}">
2300
-
<img src="/emojis/${emoji.filename}" alt="${emoji.name}" title="${emoji.name}">
2301
-
</button>`
2302
-
).join('');
2303
-
}
2304
-
2305
-
// Add regular emoji results
2306
-
if (uniqueResults.length > 0) {
2307
-
resultsHtml += uniqueResults.map(emoji =>
2308
-
`<button type="button" class="emoji-option" data-emoji="${emoji}">${emoji}</button>`
2309
-
).join('');
2310
-
}
2311
-
2312
-
if (resultsHtml) {
2313
-
emojiGrid.innerHTML = resultsHtml;
2592
+
2593
+
if (buttons.length) {
2594
+
renderEmojiButtons(buttons);
2314
2595
} else {
2315
-
emojiGrid.innerHTML = '<div style="text-align: center; color: var(--text-tertiary); padding: 2rem;">No emojis found</div>';
2596
+
showGridMessage('No emojis found');
2316
2597
}
2317
-
2318
-
// Add click handlers for both regular and custom emojis
2319
-
emojiGrid.querySelectorAll('.emoji-option').forEach(btn => {
2320
-
btn.addEventListener('click', (e) => {
2321
-
e.preventDefault();
2322
-
const emojiValue = btn.getAttribute('data-emoji');
2323
-
2324
-
if (btn.classList.contains('custom-emoji')) {
2325
-
// Handle custom emoji
2326
-
const img = btn.querySelector('img');
2327
-
selectedEmoji.innerHTML = `<img src="${img.src}" alt="${img.alt}" style="width: 100%; height: 100%; object-fit: contain;">`;
2328
-
} else {
2329
-
// Handle regular emoji
2330
-
selectedEmoji.textContent = emojiValue;
2331
-
}
2332
-
2333
-
statusInput.value = emojiValue;
2334
-
emojiPicker.style.display = 'none';
2335
-
checkForChanges();
2336
-
// Clear search when emoji is selected
2337
-
document.getElementById('emoji-search').value = '';
2338
-
});
2339
-
});
2340
2598
};
2341
-
2599
+
2342
2600
// Emoji search input handler
2343
-
const emojiSearch = document.getElementById('emoji-search');
2344
2601
if (emojiSearch) {
2345
2602
emojiSearch.addEventListener('input', (e) => {
2346
-
searchEmojis(e.target.value);
2603
+
const value = e.target.value;
2604
+
if (searchDebounce) clearTimeout(searchDebounce);
2605
+
searchDebounce = setTimeout(() => {
2606
+
searchEmojis(value);
2607
+
}, SEARCH_DEBOUNCE_MS);
2347
2608
});
2348
-
2609
+
2349
2610
// Focus search when picker opens
2350
2611
emojiSearch.addEventListener('focus', () => {
2351
2612
if (!emojiSearch.value) {