Merge branch 'main' into gm_conversion_on_reconnect_handler

This commit is contained in:
harshil Sharma 2023-11-23 11:17:30 +05:30
commit 28949d5605
18 changed files with 171 additions and 94 deletions

View file

@ -16,6 +16,35 @@ import {forceLogoutIfNecessary} from './session';
import type {Model} from '@nozbe/watermelondb';
import type PostModel from '@typings/database/models/servers/post';
export async function getIsReactionAlreadyAddedToPost(serverUrl: string, postId: string, emojiName: string) {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const currentUserId = await getCurrentUserId(database);
const emojiAlias = getEmojiFirstAlias(emojiName);
return await queryReaction(database, emojiAlias, postId, currentUserId).fetchCount() > 0;
} catch (error) {
logDebug('error on getIsReactionAlreadyAddedToPost', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function toggleReaction(serverUrl: string, postId: string, emojiName: string) {
try {
const isReactionAlreadyAddedToPost = await getIsReactionAlreadyAddedToPost(serverUrl, postId, emojiName);
if (isReactionAlreadyAddedToPost) {
return removeReaction(serverUrl, postId, emojiName);
}
return addReaction(serverUrl, postId, emojiName);
} catch (error) {
logDebug('error on toggleReaction', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function addReaction(serverUrl: string, postId: string, emojiName: string) {
try {
const client = NetworkManager.getClient(serverUrl);

View file

@ -5,7 +5,7 @@ import React, {useCallback, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {Keyboard, TouchableOpacity} from 'react-native';
import {addReaction, removeReaction} from '@actions/remote/reactions';
import {addReaction, removeReaction, toggleReaction} from '@actions/remote/reactions';
import CompassIcon from '@components/compass_icon';
import {Screens} from '@constants';
import {MAX_ALLOWED_REACTIONS} from '@constants/emoji';
@ -100,8 +100,8 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
return {reactionsByName, highlightedReactions};
}, [sortedReactions, reactions]);
const handleAddReactionToPost = (emoji: string) => {
addReaction(serverUrl, postId, emoji);
const handleToggleReactionToPost = (emoji: string) => {
toggleReaction(serverUrl, postId, emoji);
};
const handleAddReaction = useCallback(preventDoubleTap(() => {
@ -110,7 +110,7 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
screen: Screens.EMOJI_PICKER,
theme,
title: formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}),
props: {onEmojiPress: handleAddReactionToPost},
props: {onEmojiPress: handleToggleReactionToPost},
});
}), [formatMessage, theme]);

View file

@ -7,4 +7,5 @@ export default {
PLUGIN_DISMISSED_POST_ERROR: 'plugin.message_will_be_posted.dismiss_post',
SEND_EMAIL_WITH_DEFAULTS_ERROR: 'api.team.invite_members.unable_to_send_email_with_defaults.app_error',
TEAM_MEMBERSHIP_DENIAL_ERROR_ID: 'api.team.add_members.user_denied',
DUPLICATE_CHANNEL_NAME: 'store.sql_channel.save_channel.exists.app_error',
};

View file

@ -22,14 +22,22 @@ const style = StyleSheet.create({
});
type Props = {
channelId: string;
showJoinCallBanner: boolean;
showIncomingCalls: boolean;
isInACall: boolean;
channelId?: string;
showJoinCallBanner?: boolean;
showIncomingCalls?: boolean;
isInACall?: boolean;
threadScreen?: boolean;
channelsScreen?: boolean;
}
const FloatingCallContainer = ({channelId, showJoinCallBanner, showIncomingCalls, isInACall, threadScreen}: Props) => {
const FloatingCallContainer = ({
channelId,
showJoinCallBanner,
showIncomingCalls,
isInACall,
threadScreen,
channelsScreen,
}: Props) => {
const serverUrl = useServerUrl();
const insets = useSafeAreaInsets();
const isTablet = useIsTablet();
@ -39,10 +47,13 @@ const FloatingCallContainer = ({channelId, showJoinCallBanner, showIncomingCalls
const wrapperTop = {
top: insets.top + topBarForTablet + topBarChannel,
};
const wrapperBottom = {
bottom: 8,
};
return (
<View style={[style.wrapper, wrapperTop]}>
{showJoinCallBanner &&
<View style={[style.wrapper, channelsScreen ? wrapperBottom : wrapperTop]}>
{showJoinCallBanner && channelId &&
<JoinCallBanner
serverUrl={serverUrl}
channelId={channelId}

View file

@ -15,7 +15,7 @@ const style = StyleSheet.create({
});
type Props = {
channelId: string;
channelId?: string;
}
export const IncomingCallsContainer = ({

View file

@ -3,7 +3,13 @@
import {distinctUntilChanged, switchMap, combineLatest, Observable, of as of$} from 'rxjs';
import {observeCallsConfig, observeCallsState, observeCurrentCall} from '@calls/state';
import {
observeCallsConfig,
observeCallsState,
observeChannelsWithCalls,
observeCurrentCall,
observeIncomingCalls,
} from '@calls/state';
import {fillUserModels, userIds} from '@calls/utils';
import {License} from '@constants';
import DatabaseManager from '@database/manager';
@ -88,3 +94,41 @@ export const observeCurrentSessionsDict = () => {
)),
) as Observable<Dictionary<CallSession>>;
};
export const observeCallStateInChannel = (serverUrl: string, database: Database, channelId: Observable<string>) => {
const isCallInCurrentChannel = combineLatest([channelId, observeChannelsWithCalls(serverUrl)]).pipe(
switchMap(([id, calls]) => of$(Boolean(calls[id]))),
distinctUntilChanged(),
);
const currentCall = observeCurrentCall();
const ccChannelId = currentCall.pipe(
switchMap((call) => of$(call?.channelId)),
distinctUntilChanged(),
);
const isInACall = currentCall.pipe(
switchMap((call) => of$(Boolean(call?.connected))),
distinctUntilChanged(),
);
const dismissed = combineLatest([channelId, observeCallsState(serverUrl)]).pipe(
switchMap(([id, state]) => of$(Boolean(state.calls[id]?.dismissed[state.myUserId]))),
distinctUntilChanged(),
);
const isInCurrentChannelCall = combineLatest([channelId, ccChannelId]).pipe(
switchMap(([id, ccId]) => of$(id === ccId)),
distinctUntilChanged(),
);
const showJoinCallBanner = combineLatest([isCallInCurrentChannel, dismissed, isInCurrentChannelCall]).pipe(
switchMap(([isCall, dism, inCurrCall]) => of$(Boolean(isCall && !dism && !inCurrCall))),
distinctUntilChanged(),
);
const showIncomingCalls = observeIncomingCalls().pipe(
switchMap((ics) => of$(ics.incomingCalls.length > 0)),
distinctUntilChanged(),
);
return {
showJoinCallBanner,
isInACall,
showIncomingCalls,
};
};

View file

@ -3,15 +3,9 @@
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {combineLatest, distinctUntilChanged, of as of$, switchMap} from 'rxjs';
import {of as of$, switchMap} from 'rxjs';
import {observeIsCallsEnabledInChannel} from '@calls/observers';
import {
observeCallsState,
observeChannelsWithCalls,
observeCurrentCall,
observeIncomingCalls,
} from '@calls/state';
import {observeCallStateInChannel, observeIsCallsEnabledInChannel} from '@calls/observers';
import {Preferences} from '@constants';
import {withServerUrl} from '@context/server';
import {observeCurrentChannel} from '@queries/servers/channel';
@ -29,37 +23,6 @@ type EnhanceProps = WithDatabaseArgs & {
const enhanced = withObservables([], ({database, serverUrl}: EnhanceProps) => {
const channelId = observeCurrentChannelId(database);
const isCallInCurrentChannel = combineLatest([channelId, observeChannelsWithCalls(serverUrl)]).pipe(
switchMap(([id, calls]) => of$(Boolean(calls[id]))),
distinctUntilChanged(),
);
const currentCall = observeCurrentCall();
const ccChannelId = currentCall.pipe(
switchMap((call) => of$(call?.channelId)),
distinctUntilChanged(),
);
const isInACall = currentCall.pipe(
switchMap((call) => of$(Boolean(call?.connected))),
distinctUntilChanged(),
);
const dismissed = combineLatest([channelId, observeCallsState(serverUrl)]).pipe(
switchMap(([id, state]) => of$(Boolean(state.calls[id]?.dismissed[state.myUserId]))),
distinctUntilChanged(),
);
const isInCurrentChannelCall = combineLatest([channelId, ccChannelId]).pipe(
switchMap(([id, ccId]) => of$(id === ccId)),
distinctUntilChanged(),
);
const showJoinCallBanner = combineLatest([isCallInCurrentChannel, dismissed, isInCurrentChannelCall]).pipe(
switchMap(([isCall, dism, inCurrCall]) => of$(Boolean(isCall && !dism && !inCurrCall))),
distinctUntilChanged(),
);
const showIncomingCalls = observeIncomingCalls().pipe(
switchMap((ics) => of$(ics.incomingCalls.length > 0)),
distinctUntilChanged(),
);
const dismissedGMasDMNotice = queryPreferencesByCategoryAndName(database, Preferences.CATEGORIES.SYSTEM_NOTICE, Preferences.NOTICES.GM_AS_DM).observe();
const channelType = observeCurrentChannel(database).pipe(switchMap((c) => of$(c?.type)));
const currentUserId = observeCurrentUserId(database);
@ -67,9 +30,7 @@ const enhanced = withObservables([], ({database, serverUrl}: EnhanceProps) => {
return {
channelId,
showJoinCallBanner,
isInACall,
showIncomingCalls,
...observeCallStateInChannel(serverUrl, database, channelId),
isCallsEnabledInChannel: observeIsCallsEnabledInChannel(database, serverUrl, channelId),
dismissedGMasDMNotice,
channelType,

View file

@ -8,9 +8,10 @@ import {Text, View} from 'react-native';
import {convertGroupMessageToPrivateChannel, switchToChannelById} from '@actions/remote/channel';
import Button from '@components/button';
import Loading from '@components/loading';
import {ServerErrors} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {isErrorWithMessage} from '@utils/errors';
import {isErrorWithMessage, isServerError} from '@utils/errors';
import {logError} from '@utils/log';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
@ -65,6 +66,7 @@ export const ConvertGMToChannelForm = ({
const [selectedTeam, setSelectedTeam] = useState<Team>(commonTeams[0]);
const [newChannelName, setNewChannelName] = useState<string>('');
const [errorMessage, setErrorMessage] = useState<string>('');
const [channelNameErrorMessage, setChannelNameErrorMessage] = useState<string>('');
const [conversionInProgress, setConversionInProgress] = useState(false);
const userDisplayNames = useMemo(() => profiles.map((profile) => displayUsername(profile, locale, teammateNameDisplay)), [profiles, teammateNameDisplay, locale]);
@ -79,7 +81,9 @@ export const ConvertGMToChannelForm = ({
const {updatedChannel, error} = await convertGroupMessageToPrivateChannel(serverUrl, channelId, selectedTeam.id, newChannelName);
if (error) {
if (isErrorWithMessage(error)) {
if (isServerError(error) && error.server_error_id === ServerErrors.DUPLICATE_CHANNEL_NAME && isErrorWithMessage(error)) {
setChannelNameErrorMessage(error.message);
} else if (isErrorWithMessage(error)) {
setErrorMessage(error.message);
} else {
setErrorMessage(formatMessage({id: 'channel_info.convert_gm_to_channel.conversion_error', defaultMessage: 'Something went wrong. Failed to convert Group Message to Private Channel.'}));
@ -153,7 +157,10 @@ export const ConvertGMToChannelForm = ({
selectedTeamId={selectedTeam?.id}
/>
}
<ChannelNameInput onChange={setNewChannelName}/>
<ChannelNameInput
onChange={setNewChannelName}
error={channelNameErrorMessage}
/>
<Button
onPress={handleOnPress}
text={confirmButtonText}

View file

@ -10,6 +10,7 @@ import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated';
import {type Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
import {refetchCurrentUser} from '@actions/remote/user';
import FloatingCallContainer from '@calls/components/floating_call_container';
import AnnouncementBanner from '@components/announcement_banner';
import ConnectionBanner from '@components/connection_banner';
import TeamSidebar from '@components/team_sidebar';
@ -40,6 +41,7 @@ type ChannelProps = {
coldStart?: boolean;
currentUserId?: string;
hasCurrentUser: boolean;
showIncomingCalls: boolean;
};
const edges: Edge[] = ['bottom', 'left', 'right'];
@ -197,6 +199,12 @@ const ChannelListScreen = (props: ChannelProps) => {
{isTablet &&
<AdditionalTabletView/>
}
{props.showIncomingCalls && !isTablet &&
<FloatingCallContainer
showIncomingCalls={props.showIncomingCalls}
channelsScreen={true}
/>
}
</Animated.View>
</View>
</SafeAreaView>

View file

@ -6,6 +6,7 @@ import withObservables from '@nozbe/with-observables';
import {of as of$} from 'rxjs';
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
import {observeIncomingCalls} from '@calls/state';
import {queryAllMyChannelsForTeam} from '@queries/servers/channel';
import {observeCurrentTeamId, observeCurrentUserId, observeLicense} from '@queries/servers/system';
import {queryMyTeams} from '@queries/servers/team';
@ -24,6 +25,11 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
const teamsCount = queryMyTeams(database).observeCount(false);
const showIncomingCalls = observeIncomingCalls().pipe(
switchMap((ics) => of$(ics.incomingCalls.length > 0)),
distinctUntilChanged(),
);
return {
isCRTEnabled: observeIsCRTEnabled(database),
hasTeams: teamsCount.pipe(
@ -46,6 +52,7 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
switchMap((u) => of$(Boolean(u))),
distinctUntilChanged(),
),
showIncomingCalls,
};
});

View file

@ -5,7 +5,7 @@ import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {useWindowDimensions, View} from 'react-native';
import {addReaction} from '@actions/remote/reactions';
import {toggleReaction} from '@actions/remote/reactions';
import {Screens} from '@constants';
import {
LARGE_CONTAINER_SIZE,
@ -57,7 +57,7 @@ const ReactionBar = ({bottomSheetId, recentEmojis = [], postId}: QuickReactionPr
const handleEmojiPress = useCallback(async (emoji: string) => {
await dismissBottomSheet(bottomSheetId);
addReaction(serverUrl, postId, emoji);
toggleReaction(serverUrl, postId, emoji);
}, [bottomSheetId, postId, serverUrl]);
const openEmojiPicker = useCallback(async () => {

View file

@ -3,9 +3,9 @@
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {distinctUntilChanged, switchMap, combineLatest, of as of$} from 'rxjs';
import {distinctUntilChanged, switchMap, of as of$} from 'rxjs';
import {observeCallsState, observeChannelsWithCalls, observeCurrentCall, observeIncomingCalls} from '@calls/state';
import {observeCallStateInChannel} from '@calls/observers';
import {withServerUrl} from '@context/server';
import {observePost} from '@queries/servers/post';
import {observeIsCRTEnabled} from '@queries/servers/thread';
@ -28,41 +28,10 @@ const enhanced = withObservables(['rootId'], ({database, serverUrl, rootId}: Enh
switchMap((r) => of$(r?.channelId || '')),
distinctUntilChanged(),
);
const isCallInCurrentChannel = combineLatest([channelId, observeChannelsWithCalls(serverUrl)]).pipe(
switchMap(([id, calls]) => of$(Boolean(calls[id]))),
distinctUntilChanged(),
);
const currentCall = observeCurrentCall();
const ccChannelId = currentCall.pipe(
switchMap((call) => of$(call?.channelId)),
distinctUntilChanged(),
);
const isInACall = currentCall.pipe(
switchMap((call) => of$(Boolean(call?.connected))),
distinctUntilChanged(),
);
const dismissed = combineLatest([channelId, observeCallsState(serverUrl)]).pipe(
switchMap(([id, state]) => of$(Boolean(state.calls[id]?.dismissed[state.myUserId]))),
distinctUntilChanged(),
);
const isInCurrentChannelCall = combineLatest([channelId, ccChannelId]).pipe(
switchMap(([id, ccId]) => of$(id === ccId)),
distinctUntilChanged(),
);
const showJoinCallBanner = combineLatest([isCallInCurrentChannel, dismissed, isInCurrentChannelCall]).pipe(
switchMap(([isCall, dism, inCurrCall]) => of$(Boolean(isCall && !dism && !inCurrCall))),
distinctUntilChanged(),
);
const showIncomingCalls = observeIncomingCalls().pipe(
switchMap((ics) => of$(ics.incomingCalls.length > 0)),
distinctUntilChanged(),
);
return {
isCRTEnabled: observeIsCRTEnabled(database),
showJoinCallBanner,
isInACall,
showIncomingCalls,
...observeCallStateInChannel(serverUrl, database, channelId),
rootId: of$(rId),
rootPost,
};

View file

@ -112,6 +112,8 @@
"channel_info.archive_description.cannot_view_archived": "Dies wird den Kanal für das Team archivieren und aus der Benutzeroberfläche entfernen. Archivierte Kanäle könne bei Bedard wieder aktiviert werden. \n \nBist du dir sicher, dass du {term} {name} archivieren möchtest?",
"channel_info.archive_failed": "Beim Versuch, den Kanal {displayName} zu archivieren, ist ein Fehler aufgetreten",
"channel_info.archive_title": "{term} archivieren",
"channel_info.channel_auto_follow_threads": "Allen Unterhaltungen in diesem Kanal folgen",
"channel_info.channel_auto_follow_threads_failed": "Beim Versuch, allen Unterhaltungen im Kanal {displayName} automatisch zu verfolgen, ist ein Fehler aufgetreten",
"channel_info.channel_files": "Dateien",
"channel_info.close": "Schliessen",
"channel_info.close_dm": "Direktnachricht schließen",

View file

@ -166,6 +166,7 @@
"global_threads.markAllRead.cancel": "Odustani",
"global_threads.options.mark_as_read": "Označi kao pročitano",
"global_threads.options.open_in_channel": "Otvori u kanalu",
"global_threads.unreads": "Nepročitani",
"home.header.plus_menu": "Opcije",
"integration_selector.multiselect.submit": "Obavljeno",
"interactive_dialog.submit": "Pošalji",
@ -200,6 +201,8 @@
"last_users_message.first": "{firstUser} i ",
"last_users_message.others": "{numOthers} drugih ",
"login.email": "E-mail",
"login.forgot": "Ne sjećaš se lozinke?",
"login.ldapUsername": "AD/LDAP korisničko ime",
"login.or": "ili",
"login.password": "Lozinka",
"login.signIn": "Prijavi se",
@ -228,6 +231,7 @@
"mobile.channel_info.alertNo": "Ne",
"mobile.channel_info.alertYes": "Da",
"mobile.channel_list.recent": "Nedavno",
"mobile.channel_list.unreads": "Nepročitani",
"mobile.components.select_server_view.connect": "Poveži",
"mobile.components.select_server_view.connecting": "Povezivanje",
"mobile.components.select_server_view.enterServerUrl": "Upiši URL servera",
@ -323,6 +327,9 @@
"notification_settings.mentions": "Spominjanja",
"notification_settings.mentions_replies": "Spominjanja i odgovori",
"notification_settings.ooo_auto_responder": "Automatki odgovori",
"password_send.description": "Za resetiranje lozinke upiši korištenu e-mail adresu za registraciju",
"password_send.error": "Upiši važeću e-mail adresu.",
"password_send.link": "Ako račun postoji, poslat će se e-mail za ponovno postavljanje lozinke na:",
"password_send.reset": "Resetiraj tvoju lozinku",
"permalink.error.cancel": "Odustani",
"permalink.error.okay": "U redu",

View file

@ -112,6 +112,8 @@
"channel_info.archive_description.cannot_view_archived": "これによりチームからチャンネルがアーカイブされ、ユーザーインターフェースから削除されます。アーカイブされたチャンネルは、復元することもできます。\n \n{term} {name} を本当にアーカイブしますか?",
"channel_info.archive_failed": "チャンネル {displayName} をアーカイブする際にエラーが発生しました",
"channel_info.archive_title": "{term} をアーカイブする",
"channel_info.channel_auto_follow_threads": "このチャンネルのすべてのスレッドをフォローする",
"channel_info.channel_auto_follow_threads_failed": "このチャンネル {displayName} のすべてのスレッドを自動フォローする際にエラーが発生しました",
"channel_info.channel_files": "ファイル",
"channel_info.close": "閉じる",
"channel_info.close_dm": "ダイレクトメッセージを閉じる",
@ -119,12 +121,25 @@
"channel_info.close_gm": "グループメッセージを閉じる",
"channel_info.close_gm_channel": "本当にこのグループメッセージを閉じますか? ホーム画面から削除されますが、いつでも再び開くことができます。",
"channel_info.convert_failed": "{displayName} を非公開チャンネルに変更できませんでした。",
"channel_info.convert_gm_to_channel": "非公開チャンネルに変換する",
"channel_info.convert_gm_to_channel.button_text": "非公開チャンネルに変換する",
"channel_info.convert_gm_to_channel.button_text_converting": "変換中...",
"channel_info.convert_gm_to_channel.conversion_error": "問題が発生しました。グループメッセージを非公開チャンネルへ変換できませんでした。",
"channel_info.convert_gm_to_channel.loading.footer": "詳細を取得中...",
"channel_info.convert_gm_to_channel.screen_title": "非公開チャンネルに変換する",
"channel_info.convert_gm_to_channel.team_selector_list.title": "チームを選択する",
"channel_info.convert_gm_to_channel.warning.body.yourself": "あなた自身",
"channel_info.convert_gm_to_channel.warning.bodyXXXX": "{memberNames} とのグループメッセージをチャンネルに変換しようとしています。この操作は元に戻すことはできません。",
"channel_info.convert_gm_to_channel.warning.header": "チャンネルメンバー全員が会話の履歴を見れるようになります",
"channel_info.convert_gm_to_channel.warning.no_teams.body": "メンバーが同じチームに所属していないため、グループメッセージをチャンネルに変換できません。このグループメッセージをチャンネルに変換するには、すべてのメンバーを1つのチームに追加してください。",
"channel_info.convert_gm_to_channel.warning.no_teams.header": "グループメンバーが異なるチームに所属しているため、チャンネルに変換できませんでした",
"channel_info.convert_private": "非公開チャンネルに変更する",
"channel_info.convert_private_description": "{displayName} を非公開チャンネルに変更すると、履歴やメンバーシップは保持されます。共有されたファイルはリンクを持つ全員がアクセス可能なままになります。非公開チャンネルのメンバーシップは招待によってのみ追加されます。\n\nこの変更は永続的なものであり、元に戻すことはできません。\n\n本当に {displayName} を非公開チャンネルに変更しますか?",
"channel_info.convert_private_success": "{displayName} は非公開チャンネルになりました。",
"channel_info.convert_private_title": "{displayName} を非公開チャンネルに変更しますか?",
"channel_info.copied": "コピーしました",
"channel_info.copy_link": "リンクをコピー",
"channel_info.copy_purpose_text": "目的のテキストをコピーする",
"channel_info.custom_status": "カスタムステータス:",
"channel_info.edit_header": "ヘッダーを編集する",
"channel_info.error_close": "閉じる",
@ -157,6 +172,8 @@
"channel_info.unarchive_description": "{term} {name} を本当にアーカイブから復元しますか?",
"channel_info.unarchive_failed": "チャンネル {displayName} のアーカイブから復元する際にエラーが発生しました",
"channel_info.unarchive_title": "{term} をアーカイブから復元",
"channel_into.convert_gm_to_channel.team_selector.label": "チーム",
"channel_into.convert_gm_to_channel.team_selector.placeholder": "チームを選択",
"channel_intro.createdBy": "{user} によって {date} に作成されました",
"channel_intro.createdOn": "{date} に作成されました",
"channel_list.channels_category": "チャンネル",
@ -319,6 +336,7 @@
"general_settings.help": "ヘルプ",
"general_settings.notifications": "通知",
"general_settings.report_problem": "問題を報告する",
"generic.back": "戻る",
"get_post_link_modal.title": "リンクをコピーする",
"global_threads.allThreads": "全スレッド",
"global_threads.emptyThreads.message": "あなたがメンションされた、または参加したことのあるスレッドが、フォローしたスレッドとともにここに表示されます。",
@ -611,6 +629,7 @@
"mobile.managed.settings": "設定へ移動する",
"mobile.markdown.code.copy_code": "コードのコピー",
"mobile.markdown.code.plusMoreLines": "あと {count, number} {count, plural, one {line} other {lines}} あります",
"mobile.markdown.copy_header": "ヘッダーテキストをコピーする",
"mobile.markdown.image.too_large": "画像が {maxWidth} x {maxHeight} の上限を超えています:",
"mobile.markdown.link.copy_url": "URL のコピー",
"mobile.mention.copy_mention": "メンションをコピーする",
@ -708,6 +727,9 @@
"mobile.server_url.empty": "有効なサーバーURLを入力してください",
"mobile.server_url.invalid_format": "URLはhttp://またはhttps://から始まらなくてはいけません",
"mobile.session_expired.title": "セッション有効期限切れ",
"mobile.session_expired_days": "引き続き通知を受け取るにはログインしてください。{siteName} のセッションは {daysCount, number} {daysCount, plural, one {日} other {日}} 毎に期限切れになるよう設定されています。",
"mobile.session_expired_days_hrs": "引き続き通知を受け取るにはログインしてください。{siteName} のセッションは {daysCount, number} {daysCount, plural, one {日} other {日}} {hoursCount, number} {hoursCount, plural, one {時間} other {時間}} 毎に期限切れになるよう設定されています。",
"mobile.session_expired_hrs": "引き続き通知を受け取るにはログインしてください。{siteName} のセッションは {hoursCount, number} {hoursCount, plural, one {時間} other {時間}} 毎に期限切れになるよう設定されています。",
"mobile.set_status.dnd": "取り込み中",
"mobile.storage_permission_denied_description": "サーバーにファイルをアップロードします。設定を開き、{applicationName} にこのデバイスのファイルへの読み取りと書き込みのアクセス権を付与してください。",
"mobile.storage_permission_denied_title": "{applicationName} があなたのファイルへアクセスしようとしています",

View file

@ -112,6 +112,8 @@
"channel_info.archive_description.cannot_view_archived": "Dit archiveert het kanaal van het team en verwijdert het uit de gebruikersinterface. Gearchiveerde kanalen kunnen worden gearchiveerd als ze weer nodig zijn.\n\nWeet je zeker dat je de {term} {name} wil archiveren?",
"channel_info.archive_failed": "Er is een fout opgetreden bij het archiveren van het kanaal {displayName}",
"channel_info.archive_title": "Archiveer {term}",
"channel_info.channel_auto_follow_threads": "Volg alle draadjes in dit kanaal",
"channel_info.channel_auto_follow_threads_failed": "Er is een fout opgetreden bij het automatisch volgen van alle draadjes in kanaal {displayName}",
"channel_info.channel_files": "Bestanden",
"channel_info.close": "Sluiten",
"channel_info.close_dm": "Direct bericht sluiten",

View file

@ -112,6 +112,8 @@
"channel_info.archive_description.cannot_view_archived": "Spowoduje to zarchiwizowanie kanału z zespołu i usunięcie go z interfejsu użytkownika. Zarchiwizowane kanały mogą zostać odarchiwizowane, jeśli będą ponownie potrzebne.\n\nCzy na pewno chcesz zarchiwizować kanał {term} {name}?",
"channel_info.archive_failed": "Wystąpił błąd podczas próby archiwizacji kanału {displayName}",
"channel_info.archive_title": "Archiwizuj {term}",
"channel_info.channel_auto_follow_threads": "Śledź wszystkie wątki na tym kanale",
"channel_info.channel_auto_follow_threads_failed": "Wystąpił błąd podczas próby automatycznego śledzenia wszystkich wątków na kanale {displayName}",
"channel_info.channel_files": "Pliki",
"channel_info.close": "Zamknij",
"channel_info.close_dm": "Zamknij wiadomość bezpośrednią",
@ -710,6 +712,9 @@
"mobile.server_url.empty": "Proszę wprowadzić prawidłowy adres URL serwera",
"mobile.server_url.invalid_format": "Adres musi zaczynać się od http:// lub https://",
"mobile.session_expired.title": "Sesja Wygasła",
"mobile.session_expired_days": "Zaloguj się, aby nadal otrzymywać powiadomienia. Sesje dla {siteName} są skonfigurowane tak, aby wygasały co {daysCount, number} {daysCount, plural, one {dzień} other {dni}}.",
"mobile.session_expired_days_hrs": "Zaloguj się, aby nadal otrzymywać powiadomienia. Sesje dla {siteName} są skonfigurowane tak, aby wygasały co {daysCount, number} {daysCount, plural, one {dzień} other {dni}} i {hoursCount, number} {hoursCount, plural, one {godzinę} other {godzin}}.",
"mobile.session_expired_hrs": "Zaloguj się, aby nadal otrzymywać powiadomienia. Sesje dla {siteName} są skonfigurowane tak, aby wygasały co {hoursCount, number} {hoursCount, plural, one {godzinę} other {godziny/godzin}}.",
"mobile.set_status.dnd": "Nie przeszkadzać",
"mobile.storage_permission_denied_description": "Prześlij pliki na swój serwer. Otwórz Ustawienia, aby przyznać {applicationName} dostęp do odczytu i zapisu plików na tym urządzeniu.",
"mobile.storage_permission_denied_title": "{applicationName} chce otrzymać dostęp do twoich plików",

View file

@ -112,6 +112,8 @@
"channel_info.archive_description.cannot_view_archived": "Detta kommer att arkivera kanalen från teamet och ta bort den från användargränssnittet. Arkiverade kanaler kan hämtas från arkivet om de behövs igen.\n\nÄr du säker på att du vill arkivera {term} {name}?",
"channel_info.archive_failed": "Ett fel inträffade när kanalen {displayName} skulle arkiveras",
"channel_info.archive_title": "Arkivera {term}",
"channel_info.channel_auto_follow_threads": "Följ alla trådar i den här kanalen",
"channel_info.channel_auto_follow_threads_failed": "Ett fel uppstod när alla trådar i kanalen {displayName} skulle följas",
"channel_info.channel_files": "Filer",
"channel_info.close": "Stäng",
"channel_info.close_dm": "Stäng direktmeddelande",