1.0.60
All checks were successful
Build and Release / build-and-release (push) Successful in 12m29s

This commit is contained in:
Bryan1029384756
2026-04-14 20:03:54 -05:00
parent b7a4cf4ce8
commit 965048f7d2
47 changed files with 2558 additions and 135 deletions

View File

@@ -41,9 +41,43 @@ function extractUrls(text: string): string[] {
return Array.from(new Set(cleaned));
}
/** True when a message body is entirely made up of one or more GIF
* URLs plus whitespace — i.e. the user posted a GIF from the
* picker and there's nothing worth showing as text. The render
* path hides the <MessageContent> block in that case so only the
* embedded preview appears. */
function isGifOnlyContent(text: string): boolean {
const urls = extractUrls(text);
if (urls.length === 0) return false;
if (!urls.every((u) => /\.gif(\?|#|$)/i.test(u))) return false;
let remainder = text;
for (const u of urls) {
remainder = remainder.split(u).join('');
}
return remainder.trim().length === 0;
}
/**
* Discord-style relative timestamp:
* - Same calendar day → `Today at 7:08 PM`
* - Previous calendar day → `Yesterday at 7:08 PM`
* - Anything else → `4/11/2026, 7:08 PM`
*/
function formatTime(ts: number): string {
const date = new Date(ts);
return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
const now = new Date();
const time = date.toLocaleTimeString([], {
hour: 'numeric',
minute: '2-digit',
});
const isToday = date.toDateString() === now.toDateString();
if (isToday) return `Today at ${time}`;
const yesterday = new Date(now);
yesterday.setDate(yesterday.getDate() - 1);
if (date.toDateString() === yesterday.toDateString()) {
return `Yesterday at ${time}`;
}
return `${date.toLocaleDateString()}, ${time}`;
}
function formatFullTime(ts: number): string {
@@ -96,12 +130,17 @@ export function MessageGroup({ messages, channelId, onReply }: MessageGroupProps
} | null>(null);
// Image lightbox state — tracks the decrypted blob URL + the
// full attachment metadata of the image that was clicked so the
// lightbox info card can render filename / size / dimensions.
// Null means the lightbox is closed.
// optional attachment metadata of the image that was clicked so
// the lightbox info card can render filename / size / dimensions.
// Null means the lightbox is closed. For inline GIFs posted via
// a URL (no encrypted attachment), the metadata is absent — the
// lightbox gracefully hides the star / detail chrome in that
// case.
const [lightboxItem, setLightboxItem] = useState<{
src: string;
attachment: AttachmentMetadata;
attachment?: AttachmentMetadata;
filename?: string;
mimeType?: string;
} | null>(null);
// Right-click context menu state. When set, the MessageActionBar for
@@ -437,7 +476,7 @@ export function MessageGroup({ messages, channelId, onReply }: MessageGroupProps
})}
</span>
)}
{msg.content && (
{msg.content && !isGifOnlyContent(msg.content) && (
<div className={styles.text}>
<MessageContent
content={msg.content}
@@ -453,7 +492,17 @@ export function MessageGroup({ messages, channelId, onReply }: MessageGroupProps
extractUrls(msg.content)
.slice(0, 3)
.map((url, idx) => (
<LinkEmbed key={`embed-${idx}-${url}`} url={url} />
<LinkEmbed
key={`embed-${idx}-${url}`}
url={url}
onOpenGif={(gifUrl) =>
setLightboxItem({
src: gifUrl,
filename: gifUrl.split('/').pop() || 'gif',
mimeType: 'image/gif',
})
}
/>
))}
{msg.attachments.length > 0 && (
<div className={styles.attachments}>
@@ -578,17 +627,13 @@ export function MessageGroup({ messages, channelId, onReply }: MessageGroupProps
alt={`:${r.emoji}:`}
title={`:${r.emoji}:`}
draggable={false}
style={{
width: 16,
height: 16,
objectFit: 'contain',
verticalAlign: 'middle',
}}
className={styles.reactionEmoji}
/>
) : (
<TwemojiImg
emoji={resolveReactionKeyToUnicode(r.emoji)}
size={16}
className={styles.reactionEmoji}
/>
)}
<span className={styles.reactionCount}>{r.count}</span>
@@ -623,11 +668,15 @@ export function MessageGroup({ messages, channelId, onReply }: MessageGroupProps
<ImageLightbox
isOpen={!!lightboxItem}
src={lightboxItem?.src ?? ''}
filename={lightboxItem?.attachment.filename}
mimeType={lightboxItem?.attachment.mimeType}
size={lightboxItem?.attachment.size}
width={lightboxItem?.attachment.width}
height={lightboxItem?.attachment.height}
filename={
lightboxItem?.attachment?.filename ?? lightboxItem?.filename
}
mimeType={
lightboxItem?.attachment?.mimeType ?? lightboxItem?.mimeType
}
size={lightboxItem?.attachment?.size}
width={lightboxItem?.attachment?.width}
height={lightboxItem?.attachment?.height}
attachment={lightboxItem?.attachment}
onClose={() => setLightboxItem(null)}
/>