Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a560f24c45 | |||
| 87c2e45bb4 | |||
| b65a3994f3 | |||
| aa609389fa | |||
| cb3cbd8827 |
@@ -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,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" />
|
||||||
|
|||||||
@@ -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,15 +892,35 @@ 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' })
|
||||||
|
: hasName
|
||||||
|
? t('app:chats.call_started_by', {
|
||||||
|
name: senderDisplayName,
|
||||||
|
defaultValue: '{{name}} hat einen Anruf gestartet',
|
||||||
|
})
|
||||||
: t('app:chats.call_incoming', { defaultValue: 'Incoming call' })
|
: 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' })
|
||||||
|
: hasName
|
||||||
|
? t('app:chats.call_missed_by', {
|
||||||
|
name: senderDisplayName,
|
||||||
|
defaultValue: 'Verpasster Anruf von {{name}}',
|
||||||
|
})
|
||||||
: t('app:chats.call_missed', { defaultValue: 'Missed call' })
|
: 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' });
|
: 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;
|
||||||
|
|||||||
@@ -643,25 +643,13 @@ 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">
|
||||||
|
|
||||||
<MediaFilesDrawer
|
|
||||||
open={mediaDrawerOpen}
|
|
||||||
index={attachmentIndex}
|
|
||||||
senderNameFor={senderNameFor}
|
|
||||||
onJumpToMessage={(messageId) => {
|
|
||||||
setMediaDrawerOpen(false);
|
|
||||||
jumpToMessage(messageId);
|
|
||||||
}}
|
|
||||||
onClose={() => setMediaDrawerOpen(false)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Discord-style persistent voice-channel rail. Always visible in groups
|
{/* Discord-style persistent voice-channel rail. Always visible in groups
|
||||||
so anyone can pop in without an invite-ring; hidden in 1:1s unless
|
so anyone can pop in without an invite-ring; hidden in 1:1s unless
|
||||||
someone is already waiting. Hides automatically once we're in. */}
|
someone is already waiting. Hides automatically once we're in. */}
|
||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user