Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# [1.29.0](https://github.com/rodrigogs/whats-reader/compare/v1.28.0...v1.29.0) (2026-01-05)


### Features

* improve bookmark button positioning and UI responsiveness ([68327cb](https://github.com/rodrigogs/whats-reader/commit/68327cb6d0119ce28ea1d66828a66f272199bc85))

# [1.28.0](https://github.com/rodrigogs/whats-reader/compare/v1.27.0...v1.28.0) (2026-01-05)


Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "whats-reader",
"productName": "WhatsApp Backup Reader",
"version": "1.28.0",
"version": "1.29.0",
"description": "A desktop app to read and visualize WhatsApp chat exports",
"license": "AGPL-3.0",
"author": {
Expand Down
82 changes: 32 additions & 50 deletions src/lib/components/MessageBubble.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,7 @@ $effect(() => {
});

const bubbleClass = $derived(
isOwn
? 'bg-[var(--color-message-out)] ml-auto'
: 'bg-[var(--color-message-in)] mr-auto',
isOwn ? 'bg-[var(--color-message-out)]' : 'bg-[var(--color-message-in)]',
);

// Use wider bubble for audio messages to fit the audio player
Expand Down Expand Up @@ -209,8 +207,16 @@ function highlightText(text: string, query: string): string {

// Combine and sort all markers
const markers: Array<
| { type: 'url'; pos: number; data: { start: number; end: number; url: string } }
| { type: 'search'; pos: number; data: { start: number; end: number; text: string } }
| {
type: 'url';
pos: number;
data: { start: number; end: number; url: string };
}
| {
type: 'search';
pos: number;
data: { start: number; end: number; text: string };
}
> = [];

urls.forEach((url) => {
Expand Down Expand Up @@ -277,7 +283,7 @@ function linkifyText(text: string): string {
let lastIndex = 0;
let result = '';
let match;

while ((match = urlRegex.exec(text)) !== null) {
// Add text before the URL, escaped
result += escapeHtml(text.slice(lastIndex, match.index));
Expand All @@ -286,7 +292,7 @@ function linkifyText(text: string): string {
result += `<a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer" class="text-[#00897B] dark:text-[#4FC3F7] underline break-all hover:text-[#00695C] dark:hover:text-[#29B6F6]">${escapeHtml(url)}</a>`;
lastIndex = match.index + url.length;
}

// Add the rest of the text, escaped
result += escapeHtml(text.slice(lastIndex));
return result;
Expand Down Expand Up @@ -326,6 +332,22 @@ async function transcribeVoiceMessage() {
}
</script>

{#snippet bookmarkButton(isBookmarked: boolean, className: string)}
<button
type="button"
class="bookmark-btn p-1 rounded cursor-pointer hover:bg-black/10 dark:hover:bg-white/10 {className}"
class:bookmarked={isBookmarked}
onclick={handleBookmarkClick}
title={isBookmarked ? 'Edit bookmark' : 'Add bookmark'}
>
{#if isBookmarked}
<Icon name="bookmark" size="sm" class="text-[var(--color-whatsapp-teal)]" filled />
{:else}
<Icon name="bookmark-outline" size="sm" class="text-gray-400 dark:text-gray-500" />
{/if}
</button>
{/snippet}

{#if message.isSystemMessage}
<!-- System message -->
{@const isBookmarked = bookmarkedMessageIds.has(message.id)}
Expand All @@ -340,19 +362,7 @@ async function transcribeVoiceMessage() {
</div>
<!-- Bookmark button for system messages -->
<div class="flex items-center ml-1 self-center">
<button
type="button"
class="bookmark-btn p-1 rounded cursor-pointer hover:bg-black/10 dark:hover:bg-white/10"
class:bookmarked={isBookmarked}
onclick={handleBookmarkClick}
title={isBookmarked ? 'Edit bookmark' : 'Add bookmark'}
>
{#if isBookmarked}
<Icon name="bookmark" size="sm" class="text-[var(--color-whatsapp-teal)]" filled />
{:else}
<Icon name="bookmark-outline" size="sm" class="text-gray-400 dark:text-gray-500" />
{/if}
</button>
{@render bookmarkButton(isBookmarked, '')}
</div>
</div>
</div>
Expand All @@ -363,21 +373,7 @@ async function transcribeVoiceMessage() {
<div class="message-container flex {isOwn ? 'justify-end' : 'justify-start'} mb-1 group">
<!-- Bookmark button (left side for own messages) -->
{#if isOwn}
<div class="flex items-center mr-1 self-center">
<button
type="button"
class="bookmark-btn p-1 rounded cursor-pointer hover:bg-black/10 dark:hover:bg-white/10"
class:bookmarked={isBookmarked}
onclick={handleBookmarkClick}
title={isBookmarked ? 'Edit bookmark' : 'Add bookmark'}
>
{#if isBookmarked}
<Icon name="bookmark" size="sm" class="text-[var(--color-whatsapp-teal)]" filled />
{:else}
<Icon name="bookmark-outline" size="sm" class="text-gray-400 dark:text-gray-500" />
{/if}
</button>
</div>
{@render bookmarkButton(isBookmarked, 'mr-1 self-center')}
{/if}

<div
Expand Down Expand Up @@ -593,21 +589,7 @@ async function transcribeVoiceMessage() {

<!-- Bookmark button (right side for other's messages) -->
{#if !isOwn}
<div class="flex items-center ml-1 self-center">
<button
type="button"
class="bookmark-btn p-1 rounded cursor-pointer hover:bg-black/10 dark:hover:bg-white/10"
class:bookmarked={isBookmarked}
onclick={handleBookmarkClick}
title={isBookmarked ? 'Edit bookmark' : 'Add bookmark'}
>
{#if isBookmarked}
<Icon name="bookmark" size="sm" class="text-[var(--color-whatsapp-teal)]" filled />
{:else}
<Icon name="bookmark-outline" size="sm" class="text-gray-400 dark:text-gray-500" />
{/if}
</button>
</div>
{@render bookmarkButton(isBookmarked, 'ml-1 self-center')}
{/if}
</div>
</div>
Expand Down
30 changes: 16 additions & 14 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -738,21 +738,23 @@ const currentUser = $derived.by(() => {
>
<Icon name="chart-bar" size="md" />
</IconButton>
<LocaleSwitcher variant="header" />
<IconButton
theme="dark"
size="md" rounded="full"
onclick={toggleDarkMode}
aria-label={m.toggle_dark_mode()}
title={isDarkMode ? m.theme_switch_to_light() : m.theme_switch_to_dark()}
>
{#if isDarkMode}
<Icon name="sun" size="md" class="text-yellow-300" />
{:else}
<Icon name="moon" size="md" class="text-white/80" />
{/if}
</IconButton>
</div>

<!-- Theme toggle and language selector (always visible) -->
<LocaleSwitcher variant="header" />
<IconButton
theme="dark"
size="md" rounded="full"
onclick={toggleDarkMode}
aria-label={m.toggle_dark_mode()}
title={isDarkMode ? m.theme_switch_to_light() : m.theme_switch_to_dark()}
>
{#if isDarkMode}
<Icon name="sun" size="md" class="text-yellow-300" />
{:else}
<Icon name="moon" size="md" class="text-white/80" />
{/if}
</IconButton>
</div>
</div>
{:else}
Expand Down