Skip to content
3 changes: 3 additions & 0 deletions src/actionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ type EventAlertWordsAction = {|
|};

type EventRealmFiltersAction = {|
...ServerEvent,
type: typeof EVENT_REALM_FILTERS,
realm_filters: RealmFilter[],
|};
Expand Down Expand Up @@ -497,11 +498,13 @@ type EventUserGroupRemoveMembersAction = {|
|};

type EventRealmEmojiUpdateAction = {|
...ServerEvent,
type: typeof EVENT_REALM_EMOJI_UPDATE,
realm_emoji: RealmEmojiById,
|};

type EventUpdateDisplaySettingsAction = {|
...ServerEvent,
type: typeof EVENT_UPDATE_DISPLAY_SETTINGS,
setting_name: string,
/** In reality, this can be a variety of types. It's boolean for a
Expand Down
12 changes: 12 additions & 0 deletions src/api/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* @flow strict-local */

/**
* The topic servers understand to mean "there is no topic".
*
* This should match
* https://github.com/zulip/zulip/blob/5.0-dev/zerver/lib/actions.py#L2714
* or similar logic at the latest `main`.
*/
// This is hardcoded in the server, and therefore untranslated; that's
// zulip/zulip#3639.
export const NO_TOPIC_TOPIC: string = '(no topic)';
3 changes: 3 additions & 0 deletions src/boot/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,9 @@ const migrations: {| [string]: (GlobalState) => GlobalState |} = {
})),
}),

// Add mandatoryTopics to RealmState. No migration; handled automatically
// by merging with the new initial state.

// TIP: When adding a migration, consider just using `dropCache`.
};

Expand Down
28 changes: 24 additions & 4 deletions src/compose/ComposeBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { type EdgeInsets } from 'react-native-safe-area-context';
import { compose } from 'redux';
import invariant from 'invariant';

import * as apiConstants from '../api/constants';
import { withSafeAreaInsets } from '../react-native-safe-area-context';
import type { ThemeData } from '../styles';
import { ThemeContext } from '../styles';
Expand All @@ -29,7 +30,7 @@ import { connect } from '../react-redux';
import { withGetText } from '../boot/TranslationProvider';
import { draftUpdate, sendTypingStart, sendTypingStop } from '../actions';
import { FloatingActionButton, Input } from '../common';
import { showToast } from '../utils/info';
import { showToast, showErrorAlert } from '../utils/info';
import { IconDone, IconSend } from '../common/Icons';
import {
isConversationNarrow,
Expand All @@ -45,7 +46,13 @@ import getComposeInputPlaceholder from './getComposeInputPlaceholder';
import NotSubscribed from '../message/NotSubscribed';
import AnnouncementOnly from '../message/AnnouncementOnly';
import MentionWarnings from './MentionWarnings';
import { getAuth, getIsAdmin, getStreamInNarrow, getVideoChatProvider } from '../selectors';
import {
getAuth,
getIsAdmin,
getStreamInNarrow,
getVideoChatProvider,
getRealm,
} from '../selectors';
import {
getIsActiveStreamSubscribed,
getIsActiveStreamAnnouncementOnly,
Expand All @@ -63,6 +70,7 @@ type SelectorProps = {|
isAnnouncementOnly: boolean,
isSubscribed: boolean,
videoChatProvider: VideoChatProvider | null,
mandatoryTopics: boolean,
stream: Subscription | {| ...Stream, in_home_view: boolean |},
|};

Expand Down Expand Up @@ -399,17 +407,28 @@ class ComposeBoxInner extends PureComponent<Props, State> {
if (isStreamNarrow(narrow) || (isTopicNarrow(narrow) && isEditing)) {
const streamName = streamNameOfNarrow(narrow);
const topic = this.state.topic.trim();
return topicNarrow(streamName, topic || '(no topic)');
return topicNarrow(streamName, topic || apiConstants.NO_TOPIC_TOPIC);
}
invariant(isConversationNarrow(narrow), 'destination narrow must be conversation');
return narrow;
};

handleSend = () => {
const { dispatch } = this.props;
const { dispatch, mandatoryTopics, _ } = this.props;
const { message } = this.state;
const destinationNarrow = this.getDestinationNarrow();

if (
isTopicNarrow(destinationNarrow)
&& topicOfNarrow(destinationNarrow) === apiConstants.NO_TOPIC_TOPIC
&& mandatoryTopics
) {
// TODO how should this behave in the isEditing case? See
// https://github.com/zulip/zulip-mobile/pull/4798#discussion_r731341400.
showErrorAlert(_('Message not sent'), _('Please specify a topic.'));
return;
}

this.props.onSend(message, destinationNarrow);

this.setMessageInputValue('');
Expand Down Expand Up @@ -593,6 +612,7 @@ const ComposeBox: ComponentType<OuterProps> = compose(
isSubscribed: getIsActiveStreamSubscribed(state, props.narrow),
stream: getStreamInNarrow(state, props.narrow),
videoChatProvider: getVideoChatProvider(state),
mandatoryTopics: getRealm(state).mandatoryTopics,
})),
withSafeAreaInsets,
)(withGetText(ComposeBoxInner));
Expand Down
130 changes: 85 additions & 45 deletions src/realm/__tests__/realmReducer-test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* @flow strict-local */
import deepFreeze from 'deep-freeze';

import realmReducer from '../realmReducer';
Expand All @@ -7,29 +8,40 @@ import {
EVENT_UPDATE_DISPLAY_SETTINGS,
EVENT_REALM_FILTERS,
} from '../../actionConstants';
import { NULL_OBJECT } from '../../nullObjects';
import * as eg from '../../__tests__/lib/exampleData';

describe('realmReducer', () => {
describe('REALM_INIT', () => {
test('updates as appropriate on a boring but representative REALM_INIT', () => {
const action = eg.action.realm_init;
expect(realmReducer(eg.baseReduxState.realm, action)).toEqual({
crossRealmBots: action.data.cross_realm_bots,

nonActiveUsers: action.data.realm_non_active_users,
filters: action.data.realm_filters,
emoji: {}, // update as necessary if example data changes
videoChatProvider: null, // update as necessary if example data changes
mandatoryTopics: action.data.realm_mandatory_topics,

email: action.data.email,
user_id: action.data.user_id,
twentyFourHourTime: action.data.twenty_four_hour_time,
canCreateStreams: action.data.can_create_streams,
isAdmin: action.data.is_admin,
});
});
});

describe('ACCOUNT_SWITCH', () => {
test('resets state', () => {
const initialState = NULL_OBJECT;
const initialState = eg.plusReduxState.realm;

const action = deepFreeze({
type: ACCOUNT_SWITCH,
index: 1,
});

const expectedState = {
canCreateStreams: true,
crossRealmBots: [],
email: undefined,
user_id: undefined,
isAdmin: false,
twentyFourHourTime: false,
emoji: {},
filters: [],
videoChatProvider: null,
nonActiveUsers: [],
};
const expectedState = eg.baseReduxState.realm;

const actualState = realmReducer(initialState, action);

Expand All @@ -39,23 +51,31 @@ describe('realmReducer', () => {

describe('EVENT_UPDATE_DISPLAY_SETTINGS', () => {
test('change the display settings', () => {
const initialState = deepFreeze({
twentyFourHourTime: false,
emoji: { customEmoji1: {} },
});
const initialState = eg.reduxStatePlus({
realm: {
...eg.plusReduxState.realm,
twentyFourHourTime: false,
emoji: {
customEmoji1: {
code: 'customEmoji1',
deactivated: false,
name: 'Custom Emoji 1',
source_url: 'https://emoji.zulip.invalid/?id=custom1',
},
},
},
}).realm;

const action = deepFreeze({
type: EVENT_UPDATE_DISPLAY_SETTINGS,
eventId: 1,
id: 1,
setting: true,
setting_name: 'twenty_four_hour_time',
user: 'example@zulip.com',
});

const expectedState = {
...initialState,
twentyFourHourTime: true,
emoji: { customEmoji1: {} },
};

const actualState = realmReducer(initialState, action);
Expand All @@ -66,61 +86,81 @@ describe('realmReducer', () => {

describe('EVENT_REALM_EMOJI_UPDATE', () => {
test('update state to new realm_emoji', () => {
const prevState = deepFreeze({
twentyFourHourTime: false,
emoji: {},
filter: [],
});
const initialState = eg.reduxStatePlus({
realm: {
...eg.plusReduxState.realm,
twentyFourHourTime: false,
emoji: {},
filters: [],
},
}).realm;

const action = deepFreeze({
eventId: 4,
id: 4,
op: 'update',
realm_emoji: {
customEmoji1: {},
customEmoji2: {},
customEmoji1: {
code: 'customEmoji1',
deactivated: false,
name: 'Custom Emoji 1',
source_url: 'https://emoji.zulip.invalid/?id=custom1',
},
customEmoji2: {
code: 'customEmoji2',
deactivated: false,
name: 'Custom Emoji 2',
source_url: 'https://emoji.zulip.invalid/?id=custom2',
},
},
type: EVENT_REALM_EMOJI_UPDATE,
});

const expectedState = {
twentyFourHourTime: false,
...initialState,
emoji: {
customEmoji1: { code: 'customEmoji1' },
customEmoji2: { code: 'customEmoji2' },
customEmoji1: {
code: 'customEmoji1',
deactivated: false,
name: 'Custom Emoji 1',
source_url: 'https://emoji.zulip.invalid/?id=custom1',
},
customEmoji2: {
code: 'customEmoji2',
deactivated: false,
name: 'Custom Emoji 2',
source_url: 'https://emoji.zulip.invalid/?id=custom2',
},
},
filter: [],
};

const newState = realmReducer(prevState, action);
const newState = realmReducer(initialState, action);

expect(newState).toEqual(expectedState);
});
});

describe('EVENT_REALM_FILTERS', () => {
test('update state to new realm_filter', () => {
const prevState = deepFreeze({
twentyFourHourTime: false,
emoji: {},
filters: [],
});
const initialState = eg.reduxStatePlus({
realm: {
...eg.plusReduxState.realm,
twentyFourHourTime: false,
emoji: {},
filters: [],
},
}).realm;

const action = deepFreeze({
eventId: 4,
id: 4,
op: 'update',
realm_filters: [['#(?P<id>[0-9]+)', 'https://github.com/zulip/zulip/issues/%(id)s', 2]],
type: EVENT_REALM_FILTERS,
});

const expectedState = {
twentyFourHourTime: false,
emoji: {},
...initialState,
filters: [['#(?P<id>[0-9]+)', 'https://github.com/zulip/zulip/issues/%(id)s', 2]],
};

const newState = realmReducer(prevState, action);
const newState = realmReducer(initialState, action);

expect(newState).toEqual(expectedState);
});
Expand Down
2 changes: 2 additions & 0 deletions src/realm/realmReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const initialState = {
filters: [],
emoji: {},
videoChatProvider: null,
mandatoryTopics: false,

email: undefined,
user_id: undefined,
Expand Down Expand Up @@ -66,6 +67,7 @@ export default (state: RealmState = initialState, action: Action): RealmState =>
jitsiServerUrl: action.data.jitsi_server_url,
providerId: action.data.realm_video_chat_provider,
}),
mandatoryTopics: action.data.realm_mandatory_topics,

email: action.data.email,
user_id: action.data.user_id,
Expand Down
3 changes: 3 additions & 0 deletions src/reduxTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ export type VideoChatProvider = $ReadOnly<{| name: 'jitsi_meet', jitsiServerUrl:
* @prop videoChatProvider - The video chat provider configured by the
* server; null if none, or if the configured provider is one we don't
* support.
* @prop mandatoryTopics - Whether topics are required in stream messages
* (see https://zulip.com/help/require-topics)
*
* About the user:
* @prop email
Expand All @@ -260,6 +262,7 @@ export type RealmState = $ReadOnly<{|
filters: $ReadOnlyArray<RealmFilter>,
emoji: RealmEmojiById,
videoChatProvider: VideoChatProvider | null,
mandatoryTopics: boolean,

email: string | void,
user_id: UserId | void,
Expand Down
Loading