Compare commits

..

5 Commits

Author SHA1 Message Date
byGalax a560f24c45 chore(desktop): release v0.17.5 2026-05-13 22:35:43 +02:00
byGalax 87c2e45bb4 fix(preload): read appVersion from package.json instead of npm_package_version
`appVersion: process.env.npm_package_version ?? '0.0.0'` only worked in
dev — pnpm sets that env var while running its lifecycle scripts. In
the packaged Electron build npm_package_version is unset, so every
installed user saw `v0.0.0` on the Changelog page and the version badge
permanently flagged them as "Update verfügbar" against their own
actually-current version.

Replace with a static `import pkg from '../package.json'` so Vite
inlines the version string into the preload bundle at build time. The
release script bumps package.json before electron-builder runs, so the
inlined value always matches the freshly-released version.

resolveJsonModule + esModuleInterop are already on in tsconfig.node.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:33:18 +02:00
byGalax b65a3994f3 chore(desktop): release v0.17.4 2026-05-13 22:25:11 +02:00
byGalax aa609389fa feat(chat): render Media/Files + Group Info drawers inline, not overlaid
Both right-hand panels used absolute inset-y-0 right-0 and floated on
top of the conversation, hiding the messages directly underneath the
panel and looking unlike Discord's actual layout. Restructure:

* MediaFilesDrawer: drop absolute/z-index/shadow chrome, become a
  static flex column (w-[380px] shrink-0) with a left border. Internal
  layout unchanged.
* GroupInfoPanel: same treatment (w-[320px] shrink-0). Dropped the
  backdrop-blur and slide-up animation that only made sense as a modal.
* ConversationPage: wrap the chat content (voice rail, in-call panel,
  messages list, drag-overlay, input form) in a new
  `flex min-w-0 flex-1 flex-col` chat-column, and make that column a
  sibling of the drawers inside a new `flex flex-1 flex-row` row. The
  conversation header + search bar stay full-width above the row.

Result: opening a drawer narrows the chat column instead of covering
it, matching Discord's behaviour. The chat-column wrapper also carries
the `relative` anchor previously held by the outer wrapper so the
drag-and-drop overlay positions correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:21:05 +02:00
byGalax cb3cbd8827 feat(chat): show caller's name on call event pills in groups
In group conversations the "Outgoing/Incoming/Missed call" system pill
gave no clue WHO triggered the event — fine in a 1:1 where the only two
players are obvious, useless in a group with three+ members. Discord
puts the caller's name in the pill; mirror that.

CallEventRow now takes a senderDisplayName prop (plumbed through from
MessageBubble) and switches non-own labels to the name-aware variants:

* ended  + !mine + name → "{name} hat einen Anruf gestartet"
* missed + !mine + name → "Verpasster Anruf von {name}"
* declined + !mine + name → "Anruf von {name} abgelehnt"

Own events (mine) stay generic ("Outgoing call" / "No answer") since
the user already knows they were the initiator. Fallback path without
a name keeps the previous generic labels so nothing regresses if the
sender is unresolvable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:20:21 +02:00
6 changed files with 81 additions and 33 deletions
+8 -1
View File
@@ -28,6 +28,13 @@ import {
type UpdateCheckResult, type UpdateCheckResult,
type UpdateProgress, type UpdateProgress,
} from './ipc-types'; } from './ipc-types';
// Static import of the desktop package.json so the bundler inlines the
// version string at build time. The previous `process.env.npm_package_-
// version` approach worked in dev (pnpm sets it as a script env var) but
// fell back to '0.0.0' in packaged builds — every installed user got
// flagged as "Update verfügbar" against their own actually-current
// version. resolveJsonModule + esModuleInterop are on in tsconfig.node.
import pkg from '../package.json';
type Unsubscribe = () => void; type Unsubscribe = () => void;
@@ -40,7 +47,7 @@ function on<T>(channel: string, cb: (payload: T) => void): Unsubscribe {
const api = { const api = {
platform: ELECTRON_RUNTIME_MARKER, platform: ELECTRON_RUNTIME_MARKER,
osPlatform: process.platform as NodeJS.Platform, osPlatform: process.platform as NodeJS.Platform,
appVersion: process.env.npm_package_version ?? '0.0.0', appVersion: pkg.version,
// Screen sources --------------------------------------------------------- // Screen sources ---------------------------------------------------------
getScreenSources: (): Promise<ScreenSource[]> => ipcRenderer.invoke(CHANNELS.SCREEN_GET_SOURCES), getScreenSources: (): Promise<ScreenSource[]> => ipcRenderer.invoke(CHANNELS.SCREEN_GET_SOURCES),
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@chat-app/desktop", "name": "@chat-app/desktop",
"version": "0.17.3", "version": "0.17.5",
"private": true, "private": true,
"description": "Electron desktop client (Windows / macOS / Linux)", "description": "Electron desktop client (Windows / macOS / Linux)",
"type": "module", "type": "module",
@@ -93,7 +93,7 @@ export function GroupInfoPanel({ open, onClose, conversation }: Props) {
<aside <aside
role="dialog" role="dialog"
aria-label={t('app:group.info_title')} aria-label={t('app:group.info_title')}
className="absolute inset-y-0 right-0 z-20 flex w-[320px] flex-col border-l border-white/5 bg-ink-900/95 shadow-xl backdrop-blur-xl animate-slide-up" className="flex w-[320px] shrink-0 flex-col border-l border-white/5 bg-ink-900/95"
> >
<header className="flex items-center justify-between border-b border-white/5 px-5 py-4"> <header className="flex items-center justify-between border-b border-white/5 px-5 py-4">
<div> <div>
@@ -31,7 +31,7 @@ export function MediaFilesDrawer({ open, index, senderNameFor, onJumpToMessage,
if (!open) return null; if (!open) return null;
return ( return (
<aside className="absolute inset-y-0 right-0 z-40 flex w-full max-w-[380px] flex-col border-l border-line bg-surface-2 shadow-2xl dark:bg-[#2b2d31]"> <aside className="flex w-[380px] shrink-0 flex-col border-l border-line bg-surface-2 dark:bg-[#2b2d31]">
<header className="flex min-h-[65px] items-center gap-3 border-b border-line px-4"> <header className="flex min-h-[65px] items-center gap-3 border-b border-line px-4">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-accent/10 text-accent"> <div className="flex h-10 w-10 items-center justify-center rounded-xl bg-accent/10 text-accent">
<ImageIcon className="h-5 w-5" /> <ImageIcon className="h-5 w-5" />
+36 -4
View File
@@ -294,7 +294,14 @@ export function MessageBubble({
} }
if (parsed.kind === 'call_event') { if (parsed.kind === 'call_event') {
return <CallEventRow parsed={parsed} mine={mine} time={time} />; return (
<CallEventRow
parsed={parsed}
mine={mine}
time={time}
senderDisplayName={senderDisplayName ?? null}
/>
);
} }
return ( return (
@@ -866,10 +873,15 @@ function CallEventRow({
parsed, parsed,
mine, mine,
time, time,
senderDisplayName,
}: { }: {
parsed: { status: string; mediaKind: string; durationSec: number }; parsed: { status: string; mediaKind: string; durationSec: number };
mine: boolean; mine: boolean;
time: string; time: string;
/** Discord-parity: in group chats the system pill should say WHO
* started/missed the call. Null means we don't know (fall back to the
* legacy generic labels). */
senderDisplayName: string | null;
}) { }) {
const { t } = useTranslation(['app']); const { t } = useTranslation(['app']);
const status = parsed.status; const status = parsed.status;
@@ -880,16 +892,36 @@ function CallEventRow({
? 'border-rose-500/30 bg-rose-500/10 text-rose-700 dark:text-rose-200' ? 'border-rose-500/30 bg-rose-500/10 text-rose-700 dark:text-rose-200'
: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-200'; : 'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-200';
// For non-own events we prefer the name-aware label so group chats
// make it clear who triggered the call event. Own events stay generic
// ("Outgoing call" / "No answer") since the user already knows they
// were the initiator.
const hasName = !mine && !!senderDisplayName;
const label = const label =
status === 'ended' status === 'ended'
? mine ? mine
? t('app:chats.call_outgoing', { defaultValue: 'Outgoing call' }) ? t('app:chats.call_outgoing', { defaultValue: 'Outgoing call' })
: t('app:chats.call_incoming', { defaultValue: 'Incoming call' }) : hasName
? t('app:chats.call_started_by', {
name: senderDisplayName,
defaultValue: '{{name}} hat einen Anruf gestartet',
})
: t('app:chats.call_incoming', { defaultValue: 'Incoming call' })
: status === 'missed' : status === 'missed'
? mine ? mine
? t('app:chats.call_no_answer', { defaultValue: 'No answer' }) ? t('app:chats.call_no_answer', { defaultValue: 'No answer' })
: t('app:chats.call_missed', { defaultValue: 'Missed call' }) : hasName
: t('app:chats.call_declined', { defaultValue: 'Call declined' }); ? t('app:chats.call_missed_by', {
name: senderDisplayName,
defaultValue: 'Verpasster Anruf von {{name}}',
})
: t('app:chats.call_missed', { defaultValue: 'Missed call' })
: hasName
? t('app:chats.call_declined_by', {
name: senderDisplayName,
defaultValue: 'Anruf von {{name}} abgelehnt',
})
: t('app:chats.call_declined', { defaultValue: 'Call declined' });
const duration = parsed.durationSec > 0 ? formatDuration(parsed.durationSec) : null; const duration = parsed.durationSec > 0 ? formatDuration(parsed.durationSec) : null;
+34 -25
View File
@@ -643,31 +643,19 @@ export function ConversationPage() {
/> />
)} )}
{isGroup && conversation && ( {/* Layout row: chat column on the left grows to fill remaining width;
<GroupInfoPanel right-hand drawers (Media/Files, Group Info) render as inline
open={infoPanelOpen} siblings so opening one narrows the chat instead of floating on
onClose={() => setInfoPanelOpen(false)} top of it (Discord parity). The chat-column wrapper holds the
conversation={conversation} `relative` anchor for the drag-and-drop overlay further below. */}
/> <div className="flex min-h-0 flex-1 flex-row">
)} <div className="relative flex min-w-0 flex-1 flex-col">
{/* Discord-style persistent voice-channel rail. Always visible in groups
<MediaFilesDrawer so anyone can pop in without an invite-ring; hidden in 1:1s unless
open={mediaDrawerOpen} someone is already waiting. Hides automatically once we're in. */}
index={attachmentIndex} {conversation && !incomingHere && <VoiceChannelRail conversation={conversation} />}
senderNameFor={senderNameFor} {incomingHere && conversation && <IncomingCallPanel conversation={conversation} />}
onJumpToMessage={(messageId) => { {conversation && <InCallPanel conversation={conversation} />}
setMediaDrawerOpen(false);
jumpToMessage(messageId);
}}
onClose={() => setMediaDrawerOpen(false)}
/>
{/* Discord-style persistent voice-channel rail. Always visible in groups
so anyone can pop in without an invite-ring; hidden in 1:1s unless
someone is already waiting. Hides automatically once we're in. */}
{conversation && !incomingHere && <VoiceChannelRail conversation={conversation} />}
{incomingHere && conversation && <IncomingCallPanel conversation={conversation} />}
{conversation && <InCallPanel conversation={conversation} />}
<div <div
ref={scrollRef} ref={scrollRef}
@@ -1019,6 +1007,27 @@ export function ConversationPage() {
</button> </button>
</div> </div>
</form> </form>
</div>
{isGroup && conversation && (
<GroupInfoPanel
open={infoPanelOpen}
onClose={() => setInfoPanelOpen(false)}
conversation={conversation}
/>
)}
<MediaFilesDrawer
open={mediaDrawerOpen}
index={attachmentIndex}
senderNameFor={senderNameFor}
onJumpToMessage={(messageId) => {
setMediaDrawerOpen(false);
jumpToMessage(messageId);
}}
onClose={() => setMediaDrawerOpen(false)}
/>
</div>
<ForwardDialog <ForwardDialog
open={forwardTarget !== null} open={forwardTarget !== null}