Skip to content

Commit

Permalink
WIP ui: Use "channel" or "stream" based on server feature level
Browse files Browse the repository at this point in the history
TODO:
- use the right feature level
- Make sure servers at that feature level accept `@channel` for
  wildcard mentions. Discussion:
    https://chat.zulip.org/#narrow/stream/378-api-design/topic/stream.2Fchannel.20rename.3A.20.40stream.20wildcard.20mention/near/1752545
- write appropriate TODO(server-x) comments
- run tools/tx-sync

Done at the level of TranslationProvider, which (conveniently) can
access the feature level of the active account if any.

The messages_en.json changes were done with a handy Perl command
that Greg helped me work out:

$ perl -i -ne '
      print;
      print if (s/stream/channel/g || s/Stream/Channel/g);
    ' static/translations/messages_en.json

Fixes: zulip#5827
  • Loading branch information
chrisbobbe committed Mar 6, 2024
1 parent 69d77de commit 1dfc3e7
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 8 deletions.
2 changes: 2 additions & 0 deletions src/autocomplete/PeopleAutocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import WildcardMentionItem, {
} from './WildcardMentionItem';
import { TranslationContext } from '../boot/TranslationProvider';
import { getZulipFeatureLevel } from '../account/accountsSelectors';
import { streamChannelRenameFeatureLevel } from '../boot/streamChannelRenamesMap';

type Props = $ReadOnly<{|
filter: string,
Expand Down Expand Up @@ -74,6 +75,7 @@ export default function PeopleAutocomplete(props: Props): Node {
destinationNarrow,
// TODO(server-8.0)
zulipFeatureLevel >= 224,
zulipFeatureLevel >= streamChannelRenameFeatureLevel,
_,
);
const filteredUsers = getAutocompleteSuggestion(users, filter, ownUserId, mutedUsers);
Expand Down
32 changes: 25 additions & 7 deletions src/autocomplete/WildcardMentionItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import Touchable from '../common/Touchable';
import { createStyleSheet, ThemeContext } from '../styles';
import { caseNarrowDefault, isStreamOrTopicNarrow } from '../utils/narrow';
import { TranslationContext } from '../boot/TranslationProvider';
import { useSelector } from '../react-redux';
import { getZulipFeatureLevel } from '../account/accountsSelectors';
import { streamChannelRenameFeatureLevel } from '../boot/streamChannelRenamesMap';

/**
* A type of wildcard mention recognized by the server.
Expand Down Expand Up @@ -38,14 +41,17 @@ export enum WildcardMentionType {
// All of these should appear in messages_en.json so we can make the
// wildcard mentions discoverable in the people autocomplete in the client's
// own language. See getWildcardMentionsForQuery.
const englishCanonicalStringOf = (type: WildcardMentionType): string => {
const englishCanonicalStringOf = (
type: WildcardMentionType,
useChannelTerminology: boolean,
): string => {
switch (type) {
case WildcardMentionType.All:
return 'all';
case WildcardMentionType.Everyone:
return 'everyone';
case WildcardMentionType.Stream:
return 'stream';
return useChannelTerminology ? 'channel' : 'stream';
case WildcardMentionType.Topic:
return 'topic';
}
Expand Down Expand Up @@ -86,11 +92,20 @@ export const getWildcardMentionsForQuery = (
query: string,
destinationNarrow: Narrow,
topicMentionSupported: boolean,
useChannelTerminology: boolean,
_: GetText,
): $ReadOnlyArray<WildcardMentionType> => {
const queryMatchesWildcard = (type: WildcardMentionType): boolean =>
typeahead.query_matches_string(query, serverCanonicalStringOf(type), ' ')
|| typeahead.query_matches_string(query, _(englishCanonicalStringOf(type)), ' ');
typeahead.query_matches_string(
query,
serverCanonicalStringOf(type, useChannelTerminology),
' ',
)
|| typeahead.query_matches_string(
query,
_(englishCanonicalStringOf(type, useChannelTerminology)),
' ',
);

const results = [];

Expand Down Expand Up @@ -135,9 +150,12 @@ export default function WildcardMentionItem(props: Props): Node {

const _ = useContext(TranslationContext);

const zulipFeatureLevel = useSelector(getZulipFeatureLevel);
const useChannelTerminology = zulipFeatureLevel >= streamChannelRenameFeatureLevel;

const handlePress = useCallback(() => {
onPress(type, serverCanonicalStringOf(type));
}, [onPress, type]);
onPress(type, serverCanonicalStringOf(type, useChannelTerminology));
}, [onPress, type, useChannelTerminology]);

const themeContext = useContext(ThemeContext);

Expand Down Expand Up @@ -179,7 +197,7 @@ export default function WildcardMentionItem(props: Props): Node {
<View style={styles.textWrapper}>
<ZulipText
style={styles.text}
text={serverCanonicalStringOf(type)}
text={serverCanonicalStringOf(type, useChannelTerminology)}
numberOfLines={1}
ellipsizeMode="tail"
/>
Expand Down
55 changes: 54 additions & 1 deletion src/boot/TranslationProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ import type { GetText } from '../types';
import { useGlobalSelector } from '../react-redux';
import { getGlobalSettings } from '../selectors';
import messagesByLanguage from '../i18n/messagesByLanguage';
import { getZulipFeatureLevel, tryGetActiveAccountState } from '../account/accountsSelectors';
import { objectFromEntries } from '../jsBackport';
import { objectEntries } from '../flowPonyfill';
import {
streamChannelRenameFeatureLevel,
streamChannelRenamesMap,
} from './streamChannelRenamesMap';

// $FlowFixMe[incompatible-type] could put a well-typed mock value here, to help write tests
export const TranslationContext: React.Context<GetText> = React.createContext(undefined);
Expand Down Expand Up @@ -53,12 +60,58 @@ type Props = $ReadOnly<{|
children: React.Node,
|}>;

/**
* Like messagesByLanguage but with "channel" terminology instead of "stream".
*/
const messagesByLanguageRenamed = objectFromEntries(
objectEntries(messagesByLanguage).map(([language, messages]) => [
language,
objectFromEntries(
objectEntries(messages).map(([messageId, message]) => {
const renamedMessageId = streamChannelRenamesMap[messageId];
if (renamedMessageId == null) {
return [messageId, message];
}

const renamedMessage = messages[renamedMessageId];
if (renamedMessage === renamedMessageId && message !== messageId) {
// The newfangled "channel" string hasn't been translated yet, but
// the older "stream" string has. Consider falling back to that.
if (/^en($|-)/.test(language)) {
// The language is a variety of English. Prefer the newer
// terminology, even though awaiting translation. (Most of our
// strings don't change at all between one English and another.)
return [messageId, renamedMessage];
}
// Use the translation we have, even of the older terminology.
// (In many languages the translations have used an equivalent
// of "channel" all along anyway.)
return [messageId, message];
}
return [messageId, renamedMessage];
}),
),
]),
);

export default function TranslationProvider(props: Props): React.Node {
const { children } = props;
const language = useGlobalSelector(state => getGlobalSettings(state).language);

const activeAccountState = useGlobalSelector(tryGetActiveAccountState);

const effectiveMessagesByLanguage =
activeAccountState == null
|| getZulipFeatureLevel(activeAccountState) > streamChannelRenameFeatureLevel
? messagesByLanguageRenamed
: messagesByLanguage;

return (
<IntlProvider locale={language} textComponent={Text} messages={messagesByLanguage[language]}>
<IntlProvider
locale={language}
textComponent={Text}
messages={effectiveMessagesByLanguage[language]}
>
<TranslationContextTranslator>{children}</TranslationContextTranslator>
</IntlProvider>
);
Expand Down
88 changes: 88 additions & 0 deletions src/boot/streamChannelRenamesMap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/* @flow strict-local */

/**
* The feature level at which we want to say "channel" instead of "stream".
*
* Outside a per-account context, check the feature level of the active
* account, if there is one. If there isn't an active account, just choose
* "channel" terminology unconditionally.
*/
export const streamChannelRenameFeatureLevel = 1; // TODO

/**
* A messageId: messageId map, from "stream" terminology to "channel".
*
* When appropriate (see streamChannelRenameFeatureLevel), use this to patch
* UI-string data for all languages, so that the UI says "channel" instead
* of "stream". See https://github.com/zulip/zulip-mobile/issues/5827 .
*
* For example, use this to make a copy of messages_en that has
*
* "Notify stream": "Notify channel",
*
* instead of
*
* "Notify stream": "Notify stream",
* "Notify channel": "Notify channel",
*
* and likewise for all the other languages.
*/
export const streamChannelRenamesMap: {| [string]: string |} = {
stream: 'channel',
'Notify stream': 'Notify channel',
'Who can access the stream?': 'Who can access the channel?',
'Only organization administrators and owners can edit streams.':
'Only organization administrators and owners can edit channels.',
'{realmName} only allows organization administrators or owners to make public streams.':
'{realmName} only allows organization administrators or owners to make public channels.',
'{realmName} only allows organization moderators, administrators, or owners to make public streams.':
'{realmName} only allows organization moderators, administrators, or owners to make public channels.',
'{realmName} only allows full organization members, moderators, administrators, or owners to make public streams.':
'{realmName} only allows full organization members, moderators, administrators, or owners to make public channels.',
'{realmName} only allows organization members, moderators, administrators, or owners to make public streams.':
'{realmName} only allows organization members, moderators, administrators, or owners to make public channels.',
'{realmName} only allows organization administrators or owners to make private streams.':
'{realmName} only allows organization administrators or owners to make private channels.',
'{realmName} only allows organization moderators, administrators, or owners to make private streams.':
'{realmName} only allows organization moderators, administrators, or owners to make private channels.',
'{realmName} only allows full organization members, moderators, administrators, or owners to make private streams.':
'{realmName} only allows full organization members, moderators, administrators, or owners to make private channels.',
'{realmName} only allows organization members, moderators, administrators, or owners to make private streams.':
'{realmName} only allows organization members, moderators, administrators, or owners to make private channels.',
'{realmName} does not allow anybody to make web-public streams.':
'{realmName} does not allow anybody to make web-public channels.',
'{realmName} only allows organization owners to make web-public streams.':
'{realmName} only allows organization owners to make web-public channels.',
'{realmName} only allows organization administrators or owners to make web-public streams.':
'{realmName} only allows organization administrators or owners to make web-public channels.',
'{realmName} only allows organization moderators, administrators, or owners to make web-public streams.':
'{realmName} only allows organization moderators, administrators, or owners to make web-public channels.',
'Cannot subscribe to stream': 'Cannot subscribe to channel',
'Stream #{name} is private.': 'Channel #{name} is private.',
'Please specify a stream.': 'Please specify a channel.',
'Please specify a valid stream.': 'Please specify a valid channel.',
'No messages in stream': 'No messages in channel',
'All streams': 'All channels',
// 'No messages in topic: {streamAndTopic}': 'No messages in topic: {channelAndTopic}',
'Mute stream': 'Mute channel',
'Unmute stream': 'Unmute channel',
'{username} will not be notified unless you subscribe them to this stream.':
'{username} will not be notified unless you subscribe them to this channel.',
'Stream notifications': 'Channel notifications',
'No streams found': 'No channels found',
'Mark stream as read': 'Mark channel as read',
'Failed to mute stream': 'Failed to mute channel',
'Failed to unmute stream': 'Failed to unmute channel',
'Stream settings': 'Channel settings',
'Failed to show stream settings': 'Failed to show channel settings',
'You are not subscribed to this stream': 'You are not subscribed to this channel',
'Create new stream': 'Create new channel',
Stream: 'Channel',
'Edit stream': 'Edit channel',
'Only organization admins are allowed to post to this stream.':
'Only organization admins are allowed to post to this channel.',
'Copy link to stream': 'Copy link to channel',
'Failed to copy stream link': 'Failed to copy channel link',
'A stream with this name already exists.': 'A channel with this name already exists.',
Streams: 'Channels',
};
Loading

0 comments on commit 1dfc3e7

Please sign in to comment.