@tribe-nest/forge 3.26.0 → 3.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/client/_tests/localeHeader.spec.tsx +98 -0
- package/src/client/activeLocale.ts +34 -0
- package/src/client/createForgeClient.ts +13 -0
- package/src/contexts/CartContext.tsx +76 -0
- package/src/contexts/_tests/CartContext.spec.tsx +210 -0
- package/src/data/queries/useCheckouts.ts +83 -0
- package/src/data/queries/useWebsite.ts +7 -0
- package/src/i18n/CONVENTION.md +184 -0
- package/src/i18n/_tests/translationKeys.spec.ts +153 -0
- package/src/i18n/de.json +960 -0
- package/src/i18n/en.json +960 -0
- package/src/i18n/index.ts +125 -0
- package/src/index.ts +21 -0
- package/src/provider/ForgeAppProvider.tsx +2 -1
- package/src/provider/SiteConfigProvider.tsx +11 -0
- package/src/ui/headless/checkout/_tests/cartResume.spec.ts +291 -0
- package/src/ui/headless/checkout/_tests/useCartResume.spec.tsx +294 -0
- package/src/ui/headless/checkout/resumedCoupon.ts +61 -0
- package/src/ui/headless/checkout/useCartResume.ts +340 -0
- package/src/ui/headless/checkout/useCheckout.ts +9 -2
- package/src/ui/headless/index.ts +18 -0
- package/src/ui/index.ts +2 -0
- package/src/ui/shell/TribeNestApp.tsx +36 -27
- package/src/ui/styled/AccountDashboard.tsx +197 -93
- package/src/ui/styled/AiAgentWidget.tsx +10 -6
- package/src/ui/styled/AudioPlayer.tsx +14 -6
- package/src/ui/styled/BlogCategory.tsx +4 -2
- package/src/ui/styled/BlogComments.tsx +27 -20
- package/src/ui/styled/BlogList.tsx +5 -3
- package/src/ui/styled/BlogPost.tsx +14 -11
- package/src/ui/styled/BundleConfirmation.tsx +32 -23
- package/src/ui/styled/CancellationTerms.tsx +3 -1
- package/src/ui/styled/Cart.tsx +32 -14
- package/src/ui/styled/ChatRoom.tsx +65 -20
- package/src/ui/styled/Checkout.tsx +151 -89
- package/src/ui/styled/CheckoutConfirmation.tsx +66 -35
- package/src/ui/styled/CoachingBooking.tsx +71 -44
- package/src/ui/styled/CoachingConfirmation.tsx +56 -19
- package/src/ui/styled/CoachingDetail.tsx +15 -11
- package/src/ui/styled/CohortPage.tsx +10 -8
- package/src/ui/styled/ConfirmSubscription.tsx +14 -8
- package/src/ui/styled/ContactForm.tsx +31 -12
- package/src/ui/styled/CookieConsent.tsx +19 -15
- package/src/ui/styled/CourseAccess.tsx +54 -47
- package/src/ui/styled/CourseCheckout.tsx +56 -38
- package/src/ui/styled/CourseConfirmation.tsx +39 -17
- package/src/ui/styled/CourseDetail.tsx +21 -15
- package/src/ui/styled/CoursesGrid.tsx +4 -2
- package/src/ui/styled/CurrencySwitcher.tsx +4 -1
- package/src/ui/styled/DiscountCode.tsx +22 -18
- package/src/ui/styled/DonationButton.tsx +52 -17
- package/src/ui/styled/DonationPage.tsx +4 -4
- package/src/ui/styled/EmailListForm.tsx +22 -9
- package/src/ui/styled/EventConfirmation.tsx +37 -14
- package/src/ui/styled/EventCountdown.tsx +10 -6
- package/src/ui/styled/EventDetail.tsx +8 -6
- package/src/ui/styled/EventSeriesDetail.tsx +18 -13
- package/src/ui/styled/EventTickets.tsx +232 -108
- package/src/ui/styled/EventWaitlist.tsx +42 -33
- package/src/ui/styled/EventsList.tsx +15 -13
- package/src/ui/styled/ForgotPasswordForm.tsx +12 -12
- package/src/ui/styled/FormRenderer.tsx +12 -7
- package/src/ui/styled/InstallBanner.tsx +21 -18
- package/src/ui/styled/InvoiceConfirmation.tsx +13 -5
- package/src/ui/styled/InvoicePayment.tsx +37 -16
- package/src/ui/styled/LeadMagnet.tsx +9 -7
- package/src/ui/styled/Loading.tsx +7 -1
- package/src/ui/styled/LoginForm.tsx +22 -14
- package/src/ui/styled/MembershipCheckout.tsx +32 -13
- package/src/ui/styled/MembershipTiers.tsx +27 -18
- package/src/ui/styled/OfferButton.tsx +33 -13
- package/src/ui/styled/PaymentLinkConfirmation.tsx +11 -5
- package/src/ui/styled/PaymentLinkPayment.tsx +30 -10
- package/src/ui/styled/PodcastEpisode.tsx +20 -9
- package/src/ui/styled/PodcastList.tsx +9 -3
- package/src/ui/styled/PodcastShow.tsx +29 -7
- package/src/ui/styled/PostsFeed.tsx +10 -8
- package/src/ui/styled/PresaleCode.tsx +8 -11
- package/src/ui/styled/ProductBrowseNav.tsx +21 -11
- package/src/ui/styled/ProductDetail.tsx +58 -28
- package/src/ui/styled/ReactionBar.tsx +4 -2
- package/src/ui/styled/ReplayList.tsx +24 -13
- package/src/ui/styled/ResetPasswordForm.tsx +27 -15
- package/src/ui/styled/ResumeCart.tsx +289 -0
- package/src/ui/styled/ReviewForm.tsx +52 -58
- package/src/ui/styled/SignupForm.tsx +29 -16
- package/src/ui/styled/TicketTransfer.tsx +55 -54
- package/src/ui/styled/UserMenu.tsx +16 -14
- package/src/ui/styled/_tests/ResumeCart.spec.tsx +183 -0
package/src/ui/styled/Cart.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { useEffect } from "react";
|
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
3
|
import { ShoppingBag, X, Trash2 } from "lucide-react";
|
|
4
4
|
import { useCart } from "../../contexts/CartContext";
|
|
5
|
+
import { useForgeT } from "../../i18n";
|
|
5
6
|
import { useThemeTokens } from "../theme/ForgeThemeProvider";
|
|
6
7
|
import { readableTextOn } from "../theme/contrast";
|
|
7
8
|
import { useAmountFormatter } from "../format/useFormatCurrency";
|
|
@@ -42,6 +43,9 @@ const DRAWER_Z = 2147483001;
|
|
|
42
43
|
*/
|
|
43
44
|
export function Cart({ icon, checkoutPath = "/i/checkout", productHref, formatAmount, className, style }: CartProps) {
|
|
44
45
|
const t = useThemeTokens();
|
|
46
|
+
// Named `translate` rather than the usual `t`: `t` is already the theme
|
|
47
|
+
// tokens here, and in the `ticketTotal` parameter below.
|
|
48
|
+
const translate = useForgeT();
|
|
45
49
|
const fmt = useAmountFormatter(formatAmount);
|
|
46
50
|
// Inclusive stores show a muted "incl. tax" caption on the cart total; the
|
|
47
51
|
// exact tax figure only exists once checkout quotes it (display-only).
|
|
@@ -77,7 +81,7 @@ export function Cart({ icon, checkoutPath = "/i/checkout", productHref, formatAm
|
|
|
77
81
|
<button
|
|
78
82
|
type="button"
|
|
79
83
|
onClick={() => setCartOpen((p) => !p)}
|
|
80
|
-
aria-label="
|
|
84
|
+
aria-label={translate("forge.cart.aria_open")}
|
|
81
85
|
data-testid="cart-button"
|
|
82
86
|
className={className}
|
|
83
87
|
style={{
|
|
@@ -123,7 +127,7 @@ export function Cart({ icon, checkoutPath = "/i/checkout", productHref, formatAm
|
|
|
123
127
|
<style>{KEYFRAMES}</style>
|
|
124
128
|
<div
|
|
125
129
|
onClick={() => setCartOpen(false)}
|
|
126
|
-
aria-label="
|
|
130
|
+
aria-label={translate("forge.cart.aria_close")}
|
|
127
131
|
style={{ position: "fixed", inset: 0, zIndex: OVERLAY_Z, background: "rgba(0,0,0,0.3)" }}
|
|
128
132
|
/>
|
|
129
133
|
<aside
|
|
@@ -154,11 +158,13 @@ export function Cart({ icon, checkoutPath = "/i/checkout", productHref, formatAm
|
|
|
154
158
|
borderBottom: `1px solid ${t.border}`,
|
|
155
159
|
}}
|
|
156
160
|
>
|
|
157
|
-
<h2 style={{ fontSize: 18, fontWeight: 800, margin: 0, fontFamily: t.headingFontFamily }}>
|
|
161
|
+
<h2 style={{ fontSize: 18, fontWeight: 800, margin: 0, fontFamily: t.headingFontFamily }}>
|
|
162
|
+
{translate("forge.cart.title")}
|
|
163
|
+
</h2>
|
|
158
164
|
<button
|
|
159
165
|
type="button"
|
|
160
166
|
onClick={() => setCartOpen(false)}
|
|
161
|
-
aria-label="
|
|
167
|
+
aria-label={translate("forge.cart.aria_close")}
|
|
162
168
|
style={{ background: "transparent", border: "none", cursor: "pointer", color: t.text }}
|
|
163
169
|
>
|
|
164
170
|
<X size={22} />
|
|
@@ -166,7 +172,9 @@ export function Cart({ icon, checkoutPath = "/i/checkout", productHref, formatAm
|
|
|
166
172
|
</div>
|
|
167
173
|
|
|
168
174
|
<div style={{ flex: 1, overflowY: "auto", padding: 16, display: "flex", flexDirection: "column", gap: 16 }}>
|
|
169
|
-
{count === 0 &&
|
|
175
|
+
{count === 0 && (
|
|
176
|
+
<p style={{ textAlign: "center", opacity: 0.7 }}>{translate("forge.cart.empty_title")}</p>
|
|
177
|
+
)}
|
|
170
178
|
|
|
171
179
|
{ticketItems.map((ticket) => (
|
|
172
180
|
<div
|
|
@@ -199,7 +207,10 @@ export function Cart({ icon, checkoutPath = "/i/checkout", productHref, formatAm
|
|
|
199
207
|
</div>
|
|
200
208
|
{Object.entries(ticket.tickets).map(([id, qty]) => (
|
|
201
209
|
<div key={id} style={{ fontSize: 12, marginTop: 4, opacity: 0.75 }}>
|
|
202
|
-
{
|
|
210
|
+
{translate("forge.cart.ticket_line", {
|
|
211
|
+
ticket_title: ticket.ticketMeta[id]?.title ?? translate("forge.cart.ticket_fallback_title"),
|
|
212
|
+
quantity: qty,
|
|
213
|
+
})}
|
|
203
214
|
</div>
|
|
204
215
|
))}
|
|
205
216
|
<div style={{ fontSize: 14, marginTop: 4, opacity: 0.8 }}>{fmt(ticketTotal(ticket))}</div>
|
|
@@ -210,7 +221,7 @@ export function Cart({ icon, checkoutPath = "/i/checkout", productHref, formatAm
|
|
|
210
221
|
// cannot be bought on their own, and leaving them would
|
|
211
222
|
// produce a checkout the server refuses.
|
|
212
223
|
onClick={() => removeTickets(ticket.eventId)}
|
|
213
|
-
aria-label="
|
|
224
|
+
aria-label={translate("forge.cart.aria_remove_tickets")}
|
|
214
225
|
style={{ position: "absolute", top: 0, right: 0, background: "transparent", border: "none", cursor: "pointer", color: t.primary, padding: 4 }}
|
|
215
226
|
>
|
|
216
227
|
<Trash2 size={16} />
|
|
@@ -270,18 +281,25 @@ export function Cart({ icon, checkoutPath = "/i/checkout", productHref, formatAm
|
|
|
270
281
|
<CartLineOptions item={item} accent={t.primary} />
|
|
271
282
|
{item.isGift && (
|
|
272
283
|
<div style={{ fontSize: 12, marginTop: 4, color: t.primary }}>
|
|
273
|
-
|
|
284
|
+
{translate("forge.cart.gift_for", {
|
|
285
|
+
recipient_name: item.recipientName ?? "",
|
|
286
|
+
recipient_email: item.recipientEmail ?? "",
|
|
287
|
+
})}
|
|
274
288
|
</div>
|
|
275
289
|
)}
|
|
276
|
-
<div style={{ fontSize: 12, marginTop: 4, opacity: 0.7 }}>
|
|
290
|
+
<div style={{ fontSize: 12, marginTop: 4, opacity: 0.7 }}>
|
|
291
|
+
{translate("forge.cart.quantity", { quantity: item.quantity })}
|
|
292
|
+
</div>
|
|
277
293
|
{item.attachedTo && (
|
|
278
|
-
<div style={{ fontSize: 12, marginTop: 4, color: t.primary }}>
|
|
294
|
+
<div style={{ fontSize: 12, marginTop: 4, color: t.primary }}>
|
|
295
|
+
{translate("forge.cart.attached_to_tickets")}
|
|
296
|
+
</div>
|
|
279
297
|
)}
|
|
280
298
|
</div>
|
|
281
299
|
<button
|
|
282
300
|
type="button"
|
|
283
301
|
onClick={() => removeFromCart(item.productVariantId, item.isGift, item.recipientEmail)}
|
|
284
|
-
aria-label="
|
|
302
|
+
aria-label={translate("forge.cart.aria_remove_item")}
|
|
285
303
|
style={{ position: "absolute", top: 0, right: 0, background: "transparent", border: "none", cursor: "pointer", color: t.primary, padding: 4 }}
|
|
286
304
|
>
|
|
287
305
|
<Trash2 size={16} />
|
|
@@ -299,12 +317,12 @@ export function Cart({ icon, checkoutPath = "/i/checkout", productHref, formatAm
|
|
|
299
317
|
}}
|
|
300
318
|
>
|
|
301
319
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
|
|
302
|
-
<span style={{ fontSize: 18, fontWeight: 600 }}>
|
|
320
|
+
<span style={{ fontSize: 18, fontWeight: 600 }}>{translate("forge.cart.total")}</span>
|
|
303
321
|
<span style={{ fontSize: 18, fontWeight: 800, color: t.primary, textAlign: "right" }}>
|
|
304
322
|
{fmt(total)}
|
|
305
323
|
{pricesIncludeTax && (
|
|
306
324
|
<span style={{ display: "block", fontSize: 11, fontWeight: 400, color: t.text, opacity: 0.65 }}>
|
|
307
|
-
|
|
325
|
+
{translate("forge.cart.incl_tax")}
|
|
308
326
|
</span>
|
|
309
327
|
)}
|
|
310
328
|
</span>
|
|
@@ -323,7 +341,7 @@ export function Cart({ icon, checkoutPath = "/i/checkout", productHref, formatAm
|
|
|
323
341
|
color: onPrimary,
|
|
324
342
|
}}
|
|
325
343
|
>
|
|
326
|
-
|
|
344
|
+
{translate("forge.cart.checkout")}
|
|
327
345
|
</button>
|
|
328
346
|
</div>
|
|
329
347
|
)}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
import { Lock } from "lucide-react";
|
|
3
3
|
import type { ChatChannel, ChatMessage } from "../../data/queries/useChat";
|
|
4
|
+
import { useForgeT, type ForgeT } from "../../i18n";
|
|
4
5
|
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
5
6
|
import { readableTextOn } from "../theme/contrast";
|
|
6
7
|
import { Loading } from "./Loading";
|
|
@@ -12,19 +13,20 @@ import { NewDmDialog } from "./chat/NewDmDialog";
|
|
|
12
13
|
|
|
13
14
|
export interface ChatRoomProps {}
|
|
14
15
|
|
|
15
|
-
const channelLabel = (c: ChatChannel): string => {
|
|
16
|
+
const channelLabel = (c: ChatChannel, t: ForgeT): string => {
|
|
16
17
|
if (c.type === "dm") {
|
|
17
18
|
const p = c.otherParticipant;
|
|
18
19
|
if (p) return `${p.firstName ?? ""} ${p.lastName ?? ""}`.trim() || p.email;
|
|
19
|
-
return c.name ?? "
|
|
20
|
+
return c.name ?? t("forge.chat_room.dm_fallback");
|
|
20
21
|
}
|
|
21
|
-
return `# ${c.name ?? "
|
|
22
|
+
return `# ${c.name ?? t("forge.chat_room.channel_fallback")}`;
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
// Reference implementation of the community chat, built entirely on the
|
|
25
26
|
// `useChatRoom` primitive (channels + messages + realtime socket + read
|
|
26
27
|
// receipts + threads + member DMs). A creator can rebuild this UI from the same hook.
|
|
27
28
|
export function ChatRoom(_props: ChatRoomProps) {
|
|
29
|
+
const t = useForgeT();
|
|
28
30
|
const theme = useForgeTheme();
|
|
29
31
|
const room = useChatRoom();
|
|
30
32
|
const [showSidebar, setShowSidebar] = useState(true);
|
|
@@ -69,7 +71,7 @@ export function ChatRoom(_props: ChatRoomProps) {
|
|
|
69
71
|
>
|
|
70
72
|
{icon}
|
|
71
73
|
<span style={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
|
|
72
|
-
{icon ? c.name ?? "
|
|
74
|
+
{icon ? (c.name ?? t("forge.chat_room.channel_fallback")) : channelLabel(c, t)}
|
|
73
75
|
</span>
|
|
74
76
|
</span>
|
|
75
77
|
{unread > 0 && (
|
|
@@ -111,37 +113,63 @@ export function ChatRoom(_props: ChatRoomProps) {
|
|
|
111
113
|
if (room.channelsLoading) return <Loading fullPage />;
|
|
112
114
|
|
|
113
115
|
return (
|
|
114
|
-
<div
|
|
115
|
-
{
|
|
116
|
+
<div
|
|
117
|
+
style={{
|
|
118
|
+
display: "flex",
|
|
119
|
+
height: "calc(100vh - 200px)",
|
|
120
|
+
border,
|
|
121
|
+
borderRadius: theme.cornerRadius,
|
|
122
|
+
overflow: "hidden",
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
{/* Channel list: full-width on mobile, fixed rail on desktop */}
|
|
116
126
|
<aside
|
|
117
127
|
className={`${showSidebar ? "flex" : "hidden"} md:flex w-full md:w-[260px] flex-col`}
|
|
118
128
|
style={{ borderRight: border, overflowY: "auto" }}
|
|
119
129
|
>
|
|
120
|
-
<div
|
|
121
|
-
|
|
130
|
+
<div
|
|
131
|
+
style={{
|
|
132
|
+
display: "flex",
|
|
133
|
+
alignItems: "center",
|
|
134
|
+
justifyContent: "space-between",
|
|
135
|
+
padding: "10px 16px",
|
|
136
|
+
borderBottom: border,
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
<h2 style={{ fontWeight: 700, margin: 0 }}>{t("forge.chat_room.channels_title")}</h2>
|
|
122
140
|
<button
|
|
123
141
|
onClick={() => setShowNewDm(true)}
|
|
124
|
-
title="
|
|
125
|
-
style={{
|
|
142
|
+
title={t("forge.chat_room.new_message")}
|
|
143
|
+
style={{
|
|
144
|
+
background: theme.colors.primary,
|
|
145
|
+
color: readableTextOn(theme.colors.primary),
|
|
146
|
+
border: "none",
|
|
147
|
+
borderRadius: theme.cornerRadius,
|
|
148
|
+
width: 28,
|
|
149
|
+
height: 28,
|
|
150
|
+
cursor: "pointer",
|
|
151
|
+
fontSize: 18,
|
|
152
|
+
lineHeight: 1,
|
|
153
|
+
}}
|
|
126
154
|
>
|
|
127
155
|
+
|
|
128
156
|
</button>
|
|
129
157
|
</div>
|
|
130
158
|
{renderGroup(
|
|
131
|
-
"
|
|
159
|
+
t("forge.chat_room.group_public"),
|
|
132
160
|
room.channels.filter((c) => c.type === "public"),
|
|
133
161
|
)}
|
|
134
162
|
{renderGroup(
|
|
135
|
-
"
|
|
163
|
+
t("forge.chat_room.group_private"),
|
|
136
164
|
room.channels.filter((c) => c.type === "private"),
|
|
137
165
|
<Lock size={14} style={{ opacity: 0.7, flexShrink: 0 }} />,
|
|
138
166
|
)}
|
|
139
167
|
{renderGroup(
|
|
140
|
-
"
|
|
168
|
+
t("forge.chat_room.group_dms"),
|
|
141
169
|
room.channels.filter((c) => c.type === "dm"),
|
|
142
170
|
)}
|
|
143
171
|
{room.channels.length === 0 && (
|
|
144
|
-
<div style={{ padding: 16, opacity: 0.5, fontSize: 13 }}>
|
|
172
|
+
<div style={{ padding: 16, opacity: 0.5, fontSize: 13 }}>{t("forge.chat_room.empty_title")}</div>
|
|
145
173
|
)}
|
|
146
174
|
</aside>
|
|
147
175
|
|
|
@@ -151,7 +179,7 @@ export function ChatRoom(_props: ChatRoomProps) {
|
|
|
151
179
|
<MessagePanel room={room} onBack={() => setShowSidebar(true)} border={border} />
|
|
152
180
|
) : (
|
|
153
181
|
<div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", opacity: 0.5 }}>
|
|
154
|
-
|
|
182
|
+
{t("forge.chat_room.select_channel")}
|
|
155
183
|
</div>
|
|
156
184
|
)}
|
|
157
185
|
</section>
|
|
@@ -193,6 +221,7 @@ function MessagePanel({
|
|
|
193
221
|
onBack: () => void;
|
|
194
222
|
border: string;
|
|
195
223
|
}) {
|
|
224
|
+
const t = useForgeT();
|
|
196
225
|
const theme = useForgeTheme();
|
|
197
226
|
const listRef = useRef<HTMLDivElement | null>(null);
|
|
198
227
|
|
|
@@ -205,12 +234,15 @@ function MessagePanel({
|
|
|
205
234
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
206
235
|
}, [room.messages.length, room.selectedChannel?.id]);
|
|
207
236
|
|
|
208
|
-
const title = room.selectedChannel ? channelLabel(room.selectedChannel) : "
|
|
237
|
+
const title = room.selectedChannel ? channelLabel(room.selectedChannel, t) : t("forge.chat_room.channel_fallback");
|
|
209
238
|
|
|
210
239
|
return (
|
|
211
240
|
<>
|
|
212
241
|
<header style={{ padding: "12px 16px", borderBottom: border, display: "flex", alignItems: "center", gap: 12 }}>
|
|
213
|
-
<button
|
|
242
|
+
<button
|
|
243
|
+
onClick={onBack}
|
|
244
|
+
style={{ background: "none", border: "none", cursor: "pointer", color: theme.colors.text }}
|
|
245
|
+
>
|
|
214
246
|
←
|
|
215
247
|
</button>
|
|
216
248
|
<strong>{title}</strong>
|
|
@@ -232,14 +264,27 @@ function MessagePanel({
|
|
|
232
264
|
/>
|
|
233
265
|
))}
|
|
234
266
|
{room.hasMore && (
|
|
235
|
-
<button
|
|
236
|
-
|
|
267
|
+
<button
|
|
268
|
+
onClick={() => room.fetchNextPage()}
|
|
269
|
+
style={{
|
|
270
|
+
alignSelf: "center",
|
|
271
|
+
background: "none",
|
|
272
|
+
border,
|
|
273
|
+
borderRadius: 8,
|
|
274
|
+
padding: "4px 10px",
|
|
275
|
+
cursor: "pointer",
|
|
276
|
+
color: theme.colors.text,
|
|
277
|
+
}}
|
|
278
|
+
>
|
|
279
|
+
{t("forge.chat_room.load_earlier")}
|
|
237
280
|
</button>
|
|
238
281
|
)}
|
|
239
282
|
</div>
|
|
240
283
|
|
|
241
284
|
{room.typingUsers.length > 0 && (
|
|
242
|
-
<div style={{ padding: "0 16px 6px", fontSize: 12, opacity: 0.6 }}>
|
|
285
|
+
<div style={{ padding: "0 16px 6px", fontSize: 12, opacity: 0.6 }}>
|
|
286
|
+
{t("forge.chat_room.typing", { users: room.typingUsers.join(", ") })}
|
|
287
|
+
</div>
|
|
243
288
|
)}
|
|
244
289
|
|
|
245
290
|
<ChatComposer
|