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
48 changes: 36 additions & 12 deletions src/components/chat/new-conversation-sheet.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Check, Search, Users } from 'lucide-react-native';
import { Check, Search, Truck, Users } from 'lucide-react-native';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';

Expand Down Expand Up @@ -34,8 +34,15 @@ function recipientUserId(recipient: RecipientsResultData): string {
}

function isPersonRecipient(recipient: RecipientsResultData): boolean {
// Recipients with an empty Type are the server's pseudo-entries
// ({ Id: "0", Name: "Everyone" } / { Id: "-1", Name: "Nobody" }) — never DM targets.
const type = (recipient.Type ?? '').toLowerCase();
return type === 'personnel' || type === 'person' || type === 'user' || type === 'p' || type === '';
return type === 'personnel' || type === 'person' || type === 'user' || type === 'p';
}

function isUnitRecipient(recipient: RecipientsResultData): boolean {
const type = (recipient.Type ?? '').toLowerCase();
return type === 'unit' || type === 'units' || type === 'u';
}

export function NewConversationSheet({ isOpen, onClose, mode, onCreated }: NewConversationSheetProps) {
Expand All @@ -56,10 +63,13 @@ export function NewConversationSheet({ isOpen, onClose, mode, onCreated }: NewCo
setQuery('');
setLoadError(false);
setLoading(true);
getRecipients(true, false)
// DM mode also offers units (Dispatch can open a 1:1 with a unit);
// group membership only supports users, so group mode stays people-only.
const includeUnits = mode === 'dm';
getRecipients(true, includeUnits)
.then((result) => {
if (cancelled) return;
setRecipients((result.Data ?? []).filter(isPersonRecipient));
setRecipients((result.Data ?? []).filter((r) => isPersonRecipient(r) || (includeUnits && isUnitRecipient(r))));
Comment on lines +66 to +72

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use recipient-neutral copy in direct-message mode.

When includeUnits is true, the list contains people and units. The component still uses chat.search_people, chat.no_people, and chat.load_people_failed. A unit-only result then shows incorrect text. Add recipient-neutral keys or select mode-specific keys, and add them to every locale.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/chat/new-conversation-sheet.tsx` around lines 66 - 72, Update
the recipient loading and display logic in the new-conversation sheet to use
recipient-neutral or DM-specific translation keys whenever includeUnits is true,
replacing chat.search_people, chat.no_people, and chat.load_people_failed in
that mode. Add the corresponding keys and translations to every locale while
preserving the existing people-only copy for non-DM modes.

})
.catch((error) => {
if (cancelled) return;
Expand All @@ -73,7 +83,7 @@ export function NewConversationSheet({ isOpen, onClose, mode, onCreated }: NewCo
return () => {
cancelled = true;
};
}, [isOpen]);
}, [isOpen, mode]);

const filtered = useMemo(() => {
const q = query.trim().toLowerCase();
Expand All @@ -94,7 +104,8 @@ export function NewConversationSheet({ isOpen, onClose, mode, onCreated }: NewCo
async (recipient: RecipientsResultData) => {
setSubmitting(true);
try {
const response = await createDirectMessage({ TargetUserId: recipientUserId(recipient) });
const targetId = recipientUserId(recipient);
const response = await createDirectMessage(isUnitRecipient(recipient) ? { TargetUnitId: parseInt(targetId, 10) } : { TargetUserId: targetId });
Comment on lines +107 to +108

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg --glob '!node_modules/**' --glob '!dist/**' -n -C 8 \
  'TargetUnitId|CreateDirectMessageInput|recipientUserId|RecipientsResultData|getRecipients' .

Repository: Resgrid/Dispatch

Length of output: 17490


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- relevant implementation ---'
sed -n '1,125p' src/components/chat/new-conversation-sheet.tsx
printf '%s\n' '--- recipient model and API types ---'
cat src/models/v4/messages/recipientsResultData.ts
sed -n '1,90p' src/models/v4/chat/chatInputs.ts
printf '%s\n' '--- repository references to recipient ID formats and unit targets ---'
rg --glob '!node_modules/**' --glob '!dist/**' -n -C 3 \
  'includeUnits|RecipientsResultData|TargetUnitId|CreateDirectMessage|GetRecipients|Id: "[^"]*[:][^"]*"' .

Repository: Resgrid/Dispatch

Length of output: 15783


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

node - <<'JS'
function recipientUserId(id) {
  return id.includes(':') ? (id.split(':').pop() ?? id) : id;
}

const cases = ['U:12', 'U:12abc', 'U:12.5', 'U:', 'U:abc', '12', ' 12 '];
for (const raw of cases) {
  const targetId = recipientUserId(raw);
  const parsed = Number.parseInt(targetId, 10);
  const isDecimalInteger = /^[0-9]+$/.test(targetId);
  console.log(JSON.stringify({ raw, targetId, parsed, isDecimalInteger }));
}
JS

printf '%s\n' '--- repository origin ---'
git remote get-url origin 2>/dev/null || true

Repository: Resgrid/Dispatch

Length of output: 706


Validate the unit ID before building the request.

recipientUserId can produce malformed values, and parseInt accepts numeric prefixes such as "12abc" or "12.5" as 12. Require a decimal integer before sending TargetUnitId; otherwise reject the recipient.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/chat/new-conversation-sheet.tsx` around lines 107 - 108,
Validate targetId in the createDirectMessage path for unit recipients before
constructing the request, requiring a complete decimal integer rather than
relying on parseInt’s prefix parsing. Reject malformed values such as “12abc” or
“12.5” and avoid sending TargetUnitId when validation fails; preserve the
existing user-recipient request path.

if (response.Data?.ChatChannelId) {
onCreated(response.Data.ChatChannelId);
onClose();
Expand All @@ -110,9 +121,10 @@ export function NewConversationSheet({ isOpen, onClose, mode, onCreated }: NewCo
);

const createGroup = useCallback(async () => {
if (!groupName.trim() || selected.size === 0) return;
if (selected.size === 0) return;
setSubmitting(true);
try {
// Name is optional — the server auto-names the group after its members.
const response = await createAdHocChannel({ Name: groupName.trim(), MemberUserIds: Array.from(selected) });
Comment on lines +124 to 128

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Bug medium

WHAT: createGroup now sends Name: '' (empty string) when no group name is entered, because the !groupName.trim() guard was removed. WHY: an empty string is a distinct wire value from an omitted field — if the server auto-names based on field absence (or requires non-empty), sending "" either creates a blank-named group or returns a 400, defeating the optional-name feature. HOW: make Name optional in CreateAdHocChannelInput and omit it when blank, or confirm the server treats "" as the auto-name trigger.

// Make Name optional in CreateAdHocChannelInput and only send it when provided:
const input: CreateAdHocChannelInput = { MemberUserIds: Array.from(selected) };
const name = groupName.trim();
if (name) input.Name = name;
const response = await createAdHocChannel(input);
Prompt for LLM

File src/components/chat/new-conversation-sheet.tsx:

Line 124 to 128:

WHAT: createGroup now sends `Name: ''` (empty string) when no group name is entered, because the `!groupName.trim()` guard was removed. WHY: an empty string is a distinct wire value from an omitted field — if the server auto-names based on field absence (or requires non-empty), sending `""` either creates a blank-named group or returns a 400, defeating the optional-name feature. HOW: make `Name` optional in CreateAdHocChannelInput and omit it when blank, or confirm the server treats `""` as the auto-name trigger.

Suggested Code:

// Make Name optional in CreateAdHocChannelInput and only send it when provided:
const input: CreateAdHocChannelInput = { MemberUserIds: Array.from(selected) };
const name = groupName.trim();
if (name) input.Name = name;
const response = await createAdHocChannel(input);

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

if (response.Data?.ChatChannelId) {
onCreated(response.Data.ChatChannelId);
Expand All @@ -139,7 +151,7 @@ export function NewConversationSheet({ isOpen, onClose, mode, onCreated }: NewCo

{mode === 'group' ? (
<Input>
<InputField placeholder={t('chat.group_name')} value={groupName} onChangeText={setGroupName} />
<InputField placeholder={t('chat.group_name_optional')} value={groupName} onChangeText={setGroupName} />
</Input>
) : null}

Expand Down Expand Up @@ -167,16 +179,28 @@ export function NewConversationSheet({ isOpen, onClose, mode, onCreated }: NewCo
{filtered.map((recipient) => {
const userId = recipientUserId(recipient);
const isSelected = selected.has(userId);
const isUnit = isUnitRecipient(recipient);
return (
<Pressable key={recipient.Id} className="py-2" onPress={() => (mode === 'dm' ? startDirectMessage(recipient) : toggle(userId))} disabled={submitting}>
<HStack className="items-center justify-between">
<HStack className="flex-1 items-center" space="sm">
<Avatar size="sm">
<AvatarImage source={{ uri: getAvatarUrl(userId) }} />
</Avatar>
{isUnit ? (
<Center className="size-8 rounded-full bg-secondary-200">
<Truck size={16} color="#6b7280" />
</Center>
) : (
<Avatar size="sm">
<AvatarImage source={{ uri: getAvatarUrl(userId) }} />
</Avatar>
)}
<Text className="flex-1 text-typography-900" numberOfLines={1}>
{recipient.Name}
</Text>
{isUnit ? (
<Box className="rounded-full bg-secondary-200 px-2 py-0.5">
<Text className="text-xs text-typography-600">{t('chat.unit')}</Text>
</Box>
) : null}
</HStack>
{mode === 'group' && isSelected ? (
<Box className="rounded-full bg-primary-600 p-1">
Expand All @@ -191,7 +215,7 @@ export function NewConversationSheet({ isOpen, onClose, mode, onCreated }: NewCo
)}

{mode === 'group' ? (
<Button className="mb-2 w-full bg-primary-600" onPress={createGroup} isDisabled={submitting || !groupName.trim() || selected.size === 0}>
<Button className="mb-2 w-full bg-primary-600" onPress={createGroup} isDisabled={submitting || selected.size === 0}>
<Users size={18} color="#ffffff" />
<ButtonText className="ml-2">{t('chat.create_group_with', { count: selected.size })}</ButtonText>
</Button>
Expand Down
59 changes: 59 additions & 0 deletions src/stores/chat/__tests__/hub-invoke-args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* JoinChannel(string channelId, int? asUnitId)
* Typing(string channelId, string displayName, bool isTyping, int? asUnitId)
* MarkRead(string channelId, long seq, int? asUnitId)
* SetActiveChannel(string channelId, int? asUnitId)
*/
const mockInvoke = jest.fn().mockResolvedValue(undefined);

Expand Down Expand Up @@ -71,6 +72,15 @@ describe('chat hub invocations', () => {
expect(mockInvoke).toHaveBeenCalledWith('chatHub', 'JoinChannel', 'channel-1', null);
});

it('sends both SetActiveChannel arguments, with null clearing the marker', () => {
useChatStore.getState().setActiveChannel('channel-1');
expect(mockInvoke).toHaveBeenCalledWith('chatHub', 'SetActiveChannel', 'channel-1', null);

mockInvoke.mockClear();
useChatStore.getState().setActiveChannel(null);
expect(mockInvoke).toHaveBeenCalledWith('chatHub', 'SetActiveChannel', null, null);
});

it('sends all four Typing arguments in hub order', () => {
useChatStore.getState().sendTyping('channel-1', true);

Expand Down Expand Up @@ -110,6 +120,55 @@ describe('chat hub invocations', () => {
});
});

describe('active-channel marker resynchronization', () => {
// syncActiveChannelMarker settles on the microtask queue; two ticks drain it.
const flush = async () => {
await Promise.resolve();
await Promise.resolve();
};

beforeEach(async () => {
mockInvoke.mockClear();
mockInvoke.mockResolvedValue(undefined);
useChatStore.getState().reset();
await flush();
mockInvoke.mockClear();
});

it('re-asserts a non-null marker on reconnect', async () => {
useChatStore.getState().setActiveChannel('channel-1');
await flush();
mockInvoke.mockClear();

useChatStore.getState().handleChatConnected();

expect(mockInvoke).toHaveBeenCalledWith('chatHub', 'SetActiveChannel', 'channel-1', null);
});

it('retries a null marker that failed to send once reconnected', async () => {
mockInvoke.mockRejectedValue(new Error('disconnected'));
useChatStore.getState().setActiveChannel(null);
await flush();
mockInvoke.mockClear();
mockInvoke.mockResolvedValue(undefined);

useChatStore.getState().handleChatConnected();

expect(mockInvoke).toHaveBeenCalledWith('chatHub', 'SetActiveChannel', null, null);
});

it('does not resend a null marker the hub already confirmed', async () => {
useChatStore.getState().setActiveChannel(null);
await flush();
mockInvoke.mockClear();

useChatStore.getState().handleChatConnected();
await flush();

expect(mockInvoke).not.toHaveBeenCalledWith('chatHub', 'SetActiveChannel', expect.anything(), expect.anything());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify Jest documents that expect.anything() excludes null and undefined.
curl -fsSL https://jestjs.io/docs/expect |
  rg -i -C 2 'anything.*null|null.*anything'

Repository: Resgrid/Dispatch

Length of output: 37902


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="src/stores/chat/__tests__/hub-invoke-args.test.ts"
printf '%s\n' '--- test context ---'
cat -n "$file" | sed -n '130,185p'

printf '%s\n' '--- related symbols and calls ---'
rg -n -C 3 'handleChatConnected|SetActiveChannel|mockInvoke' src/stores/chat

Repository: Resgrid/Dispatch

Length of output: 12193


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- package and Jest configuration ---'
rg -n -C 2 '"jest"|jest.config|testMatch|testRegex' package.json '*jest*' 2>/dev/null || true

printf '%s\n' '--- exact test structure ---'
cat -n src/stores/chat/__tests__/hub-invoke-args.test.ts | sed -n '1,210p'

printf '%s\n' '--- handler definitions and call sites ---'
rg -n -C 8 'handleChatConnected|SetActiveChannel|mockInvoke' src --glob '*.{ts,tsx}'

Repository: Resgrid/Dispatch

Length of output: 25762


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path("src/stores/chat/__tests__/hub-invoke-args.test.ts")
text = p.read_text()
for i, line in enumerate(text.splitlines(), 1):
    if "SetActiveChannel" in line or "handleChatConnected" in line:
        print(f"{i}: {line}")
PY

printf '%s\n' '--- tracked candidate files ---'
git ls-files | rg '(^|/)(chat|hub).*|src/stores/chat' | head -80

Repository: Resgrid/Dispatch

Length of output: 1896


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="src/stores/chat/__tests__/hub-invoke-args.test.ts"
cat -n "$file" | sed -n '130,185p'
printf '\n--- related code ---\n'
rg -n -C 8 'handleChatConnected|SetActiveChannel|mockInvoke' src/stores/chat

Repository: Resgrid/Dispatch

Length of output: 17346


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- complete reconnect handler ---'
cat -n src/stores/chat/store.ts | sed -n '860,890p'

printf '%s\n' '--- matcher behavior for the calls relevant to this test ---'
node - <<'JS'
const anything = value => value !== null && value !== undefined;
const calls = [
  ['chatHub', 'SetActiveChannel', null, null],
  ['chatHub', 'SetActiveChannel', 'channel-1', null],
];

const matchesOriginal = call =>
  call[0] === 'chatHub' &&
  call[1] === 'SetActiveChannel' &&
  anything(call[2]) &&
  anything(call[3]);

const matchesNullFix = call =>
  call[0] === 'chatHub' &&
  call[1] === 'SetActiveChannel' &&
  call[2] === null &&
  call[3] === null;

for (const call of calls) {
  console.log(JSON.stringify(call), {
    originalNot: !matchesOriginal(call),
    nullFixNot: !matchesNullFix(call),
  });
}
JS

Repository: Resgrid/Dispatch

Length of output: 1791


Assert that the null marker is not resent.

expect.anything() excludes null, so this assertion does not detect SetActiveChannel(null, null). Use null for both arguments.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stores/chat/__tests__/hub-invoke-args.test.ts` at line 168, Update the
SetActiveChannel assertion in the hub invocation test to explicitly verify that
mockInvoke was not called with null as either argument, replacing the
expect.anything() matchers with null values.

Source: Coding guidelines

});
});

describe('incoming message normalization', () => {
beforeEach(() => {
useChatStore.setState({ messagesByChannel: {}, channels: [] });
Expand Down
28 changes: 28 additions & 0 deletions src/stores/chat/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,23 @@ async function safeInvoke(method: string, ...args: unknown[]): Promise<void> {
}
}

/** Active-channel marker the hub has not confirmed yet. Kept — null included, since
* null means "clear the marker" — until an invoke succeeds, so a send that failed
* while offline can be replayed on reconnect. */
let pendingActiveChannelSync: { channelId: string | null } | null = null;

async function syncActiveChannelMarker(channelId: string | null): Promise<void> {
const marker = { channelId };
pendingActiveChannelSync = marker;
try {
await signalRService.invoke(Env.CHAT_HUB_NAME, 'SetActiveChannel', channelId, null);
// Only clear if no newer marker superseded this one while in flight.
if (pendingActiveChannelSync === marker) pendingActiveChannelSync = null;
} catch (error) {
logger.debug({ message: 'chat: invoke SetActiveChannel skipped', context: { error } });
}
}

export const useChatStore = create<ChatState>()(
persist(
(set, get) => ({
Expand Down Expand Up @@ -285,6 +302,9 @@ export const useChatStore = create<ChatState>()(

setActiveChannel: (channelId: string | null) => {
set({ activeChannelId: channelId });
// Report the actively viewed conversation to the hub so the server can
// suppress push notifications for it; null clears the marker.
void syncActiveChannelMarker(channelId ?? null);
},

// ------------------------------------------------------------------
Expand Down Expand Up @@ -851,6 +871,13 @@ export const useChatStore = create<ChatState>()(
void get().joinChannel(activeChannelId);
void get().loadNewerMessages(activeChannelId);
}
// Re-report the active conversation so the server-side notification
// suppression marker survives reconnects. A pending null (screen closed
// while offline) is flushed too, so the server stops suppressing push
// for a channel no longer on screen.
if (activeChannelId !== null || pendingActiveChannelSync !== null) {
void syncActiveChannelMarker(activeChannelId);
}
},

reset: () => {
Expand All @@ -859,6 +886,7 @@ export const useChatStore = create<ChatState>()(
lastTypingSentAt.clear();
lastMarkedSeq.clear();
pendingChatbotMessages.clear();
pendingActiveChannelSync = null;
clearChatbotTypingTimeout();
if (outboxDrainTimer) {
clearTimeout(outboxDrainTimer);
Expand Down
2 changes: 2 additions & 0 deletions src/translations/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,8 @@
"open_assistant": "فتح المساعد",
"create_conversation_failed": "تعذّر بدء المحادثة",
"group_name": "اسم المجموعة",
"group_name_optional": "اسم المجموعة (اختياري)",
"unit": "الوحدة",
"search_people": "البحث عن أشخاص",
"no_people": "لم يتم العثور على أشخاص",
"create_group_with": "إنشاء مجموعة ({{count}})",
Expand Down
2 changes: 2 additions & 0 deletions src/translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,8 @@
"open_assistant": "Assistent öffnen",
"create_conversation_failed": "Unterhaltung konnte nicht gestartet werden",
"group_name": "Gruppenname",
"group_name_optional": "Gruppenname (optional)",
"unit": "Einheit",
"search_people": "Personen suchen",
"no_people": "Keine Personen gefunden",
"create_group_with": "Gruppe erstellen ({{count}})",
Expand Down
2 changes: 2 additions & 0 deletions src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,8 @@
"open_assistant": "Open Assistant",
"create_conversation_failed": "Could not start the conversation",
"group_name": "Group name",
"group_name_optional": "Group name (optional)",
"unit": "Unit",
"search_people": "Search people",
"no_people": "No people found",
"create_group_with": "Create group ({{count}})",
Expand Down
2 changes: 2 additions & 0 deletions src/translations/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,8 @@
"open_assistant": "Abrir asistente",
"create_conversation_failed": "No se pudo iniciar la conversación",
"group_name": "Nombre del grupo",
"group_name_optional": "Nombre del grupo (opcional)",
"unit": "Unidad",
"search_people": "Buscar personas",
"no_people": "No se encontraron personas",
"create_group_with": "Crear grupo ({{count}})",
Expand Down
2 changes: 2 additions & 0 deletions src/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,8 @@
"open_assistant": "Ouvrir l'assistant",
"create_conversation_failed": "Impossible de démarrer la conversation",
"group_name": "Nom du groupe",
"group_name_optional": "Nom du groupe (facultatif)",
"unit": "Unité",
"search_people": "Rechercher des personnes",
"no_people": "Aucune personne trouvée",
"create_group_with": "Créer un groupe ({{count}})",
Expand Down
2 changes: 2 additions & 0 deletions src/translations/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,8 @@
"open_assistant": "Apri assistente",
"create_conversation_failed": "Impossibile avviare la conversazione",
"group_name": "Nome del gruppo",
"group_name_optional": "Nome del gruppo (facoltativo)",
"unit": "Unità",
"search_people": "Cerca persone",
"no_people": "Nessuna persona trovata",
"create_group_with": "Crea gruppo ({{count}})",
Expand Down
2 changes: 2 additions & 0 deletions src/translations/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,8 @@
"open_assistant": "Otwórz asystenta",
"create_conversation_failed": "Nie można rozpocząć rozmowy",
"group_name": "Nazwa grupy",
"group_name_optional": "Nazwa grupy (opcjonalnie)",
"unit": "Jednostka",
"search_people": "Szukaj osób",
"no_people": "Nie znaleziono osób",
"create_group_with": "Utwórz grupę ({{count}})",
Expand Down
2 changes: 2 additions & 0 deletions src/translations/sv.json
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,8 @@
"open_assistant": "Öppna assistent",
"create_conversation_failed": "Det gick inte att starta konversationen",
"group_name": "Gruppnamn",
"group_name_optional": "Gruppnamn (valfritt)",
"unit": "Enhet",
"search_people": "Sök personer",
"no_people": "Inga personer hittades",
"create_group_with": "Skapa grupp ({{count}})",
Expand Down
2 changes: 2 additions & 0 deletions src/translations/uk.json
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,8 @@
"open_assistant": "Відкрити асистента",
"create_conversation_failed": "Не вдалося розпочати розмову",
"group_name": "Назва групи",
"group_name_optional": "Назва групи (необов'язково)",
"unit": "Підрозділ",
"search_people": "Пошук людей",
"no_people": "Людей не знайдено",
"create_group_with": "Створити групу ({{count}})",
Expand Down
Loading