diff --git a/.github/workflows/publish-gitea.yml b/.github/workflows/publish-gitea.yml
index 4cd9f086..bbb280f7 100644
--- a/.github/workflows/publish-gitea.yml
+++ b/.github/workflows/publish-gitea.yml
@@ -153,14 +153,31 @@ jobs:
# Remove só os anexos que esta plataforma vai repor, para os dois jobs
# não apagarem o trabalho um do outro.
+ #
+ # Padrão montado em bash puro: a versão anterior interpolava uma
+ # expressão do Actions dentro de um `case`, e quando ela não casou o
+ # laço passou em silêncio — a release ficou com dois latest.yml e o
+ # updater serviu o antigo, dizendo que a versão nova não existia.
+ if [ "${RUNNER_OS:-}" = "Windows" ]; then
+ MINE='\.exe$|^latest\.yml$'
+ else
+ MINE='\.AppImage$|\.deb$|^latest-linux\.yml$'
+ fi
+
R=$(api GET "/releases/$ID/assets")
- if [ "$(code "$R")" = "200" ]; then
- body "$R" | jq -r '.[] | "\(.id) \(.name)"' | while read -r aid aname; do
- case "$aname" in
- ${{ runner.os == 'Windows' && '*.exe|latest.yml' || '*.AppImage|*.deb|latest-linux.yml' }})
- echo "removendo anexo antigo: $aname"
- api DELETE "/releases/$ID/assets/$aid" > /dev/null ;;
- esac
+ if [ "$(code "$R")" != "200" ]; then
+ echo "::error::não foi possível listar os anexos — HTTP $(code "$R"): $(body "$R")"
+ exit 1
+ fi
+
+ OLD=$(body "$R" | jq -r '.[] | "\(.id) \(.name)"' | grep -E " .*($MINE)" || true)
+ echo "anexos desta plataforma já na release: $(printf '%s' "$OLD" | grep -c . || true)"
+ if [ -n "$OLD" ]; then
+ printf '%s\n' "$OLD" | while read -r aid aname; do
+ [ -n "$aid" ] || continue
+ echo "removendo anexo antigo: $aname"
+ D=$(api DELETE "/releases/$ID/assets/$aid")
+ [ "$(code "$D")" = "204" ] || echo "::warning::falha ao remover $aname — HTTP $(code "$D")"
done
fi
@@ -181,3 +198,16 @@ jobs:
done
[ "$sent" -gt 0 ] || { echo "::error::o build não produziu instaladores"; exit 1; }
echo "$sent arquivo(s) publicados"
+
+ # O updater busca latest.yml pelo nome. Duas cópias com o mesmo nome
+ # fazem o Gitea servir a mais antiga, e a atualização deixa de ser
+ # oferecida — sem erro em lugar nenhum. Falha aqui em vez de publicar
+ # uma release que parece boa e não atualiza.
+ if [ "${RUNNER_OS:-}" = "Windows" ]; then
+ R=$(api GET "/releases/$ID/assets")
+ DUP=$(body "$R" | jq -r '[.[] | select(.name == "latest.yml")] | length')
+ if [ "$DUP" != "1" ]; then
+ echo "::error::a release tem $DUP cópias de latest.yml — o updater serviria a errada"
+ exit 1
+ fi
+ echo "latest.yml: 1 cópia, como esperado"
diff --git a/packages/desktop/src/main.ts b/packages/desktop/src/main.ts
index 0a0dbbb5..fee93871 100644
--- a/packages/desktop/src/main.ts
+++ b/packages/desktop/src/main.ts
@@ -588,7 +588,9 @@ function createTray(): void {
function showNotification(title: string, body: string, onClick?: () => void): void {
if (!Notification.isSupported()) return;
- const notification = new Notification({ title, body, silent: false });
+ // silent: o som do sistema é o que mais incomoda numa notificação de chat, e
+ // o app toca o seu próprio efeito — que combina com os demais sons dele.
+ const notification = new Notification({ title, body, silent: true });
notification.on('click', onClick ?? (() => {
mainWindow?.show();
mainWindow?.focus();
diff --git a/packages/web/public/sounds/notification.ogg b/packages/web/public/sounds/notification.ogg
new file mode 100644
index 00000000..3bc818a4
Binary files /dev/null and b/packages/web/public/sounds/notification.ogg differ
diff --git a/packages/web/src/components/NotificationController.tsx b/packages/web/src/components/NotificationController.tsx
index 57b70535..2dedfab5 100644
--- a/packages/web/src/components/NotificationController.tsx
+++ b/packages/web/src/components/NotificationController.tsx
@@ -1,4 +1,8 @@
import { useEffect, useRef } from 'react';
+import { useInAppNotificationStore } from '../stores/inAppNotificationStore';
+import { AudioManager } from '../audio/AudioManager';
+import { getSfxVolume } from '../utils/sfx';
+import { translate, useLocaleStore } from '../i18n';
import { useChatStore } from '../stores/chatStore';
import { useVoiceStore } from '../stores/voiceStore';
import { useAuthStore } from '../stores/authStore';
@@ -51,19 +55,39 @@ export function NotificationController() {
const unsubscribeChat = useChatStore.subscribe((state, prevState) => {
if (isInitialMount.current) return;
- if (windowFocused.current) return;
+ // Antes daqui só havia aviso com a janela fora de foco. Agora a janela em
+ // foco também avisa, mas dentro do app e apenas para outro canal — avisar
+ // sobre a conversa que a pessoa está lendo seria ruído.
+ const focused = windowFocused.current;
+ const currentChannel = state.currentChannelId;
if (state.realtimeMessageEvents.length > prevState.realtimeMessageEvents.length) {
const newEvents = state.realtimeMessageEvents.slice(prevState.realtimeMessageEvents.length);
for (const { message } of newEvents) {
if (message.userId !== currentUser?.id) {
+ if (focused && message.channelId === currentChannel) break;
+
const displayName = message.user?.displayName || message.user?.username || 'Someone';
const body = message.content
? message.content.replace(/[*_~`>#\-\[\]]/g, '').slice(0, 100)
- : 'Sent an attachment';
- sendNotification(displayName, body, {
+ : translate(useLocaleStore.getState().locale, 'notify.attachment');
+
+ useInAppNotificationStore.getState().push({
+ title: displayName,
+ body,
+ avatar: message.user?.avatar ?? null,
+ userId: message.user?.id,
channelId: message.channelId,
});
+ // Efeito próprio, no lugar do som do sistema. O balão nativo agora
+ // é silencioso, então este é o único som que toca.
+ void AudioManager.getInstance().playSound('notification', { volume: getSfxVolume() });
+
+ // O balão do sistema só faz sentido quando a janela não está à
+ // vista: com ela em foco, o aviso dentro do app já cumpre o papel.
+ if (!focused) {
+ sendNotification(displayName, body, { channelId: message.channelId });
+ }
break; // one notification per batch
}
}
diff --git a/packages/web/src/components/layout/AppLayout.tsx b/packages/web/src/components/layout/AppLayout.tsx
index ff82fa6e..01314d7b 100644
--- a/packages/web/src/components/layout/AppLayout.tsx
+++ b/packages/web/src/components/layout/AppLayout.tsx
@@ -1,4 +1,5 @@
import React, { useEffect } from 'react';
+import { InAppNotifications } from '../ui/InAppNotifications';
import { UpdateBanner } from '../ui/UpdateBanner';
import { useExpressionStore } from '../../stores/expressionStore';
import { useParams, useNavigate } from 'react-router-dom';
@@ -430,6 +431,7 @@ export function AppLayout() {
the root). */}