Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add follow/unfollow options to topic action sheet #5794

Merged
merged 14 commits into from
Nov 22, 2023
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
73 changes: 73 additions & 0 deletions patches/react-native+0.68.7.patch
Original file line number Diff line number Diff line change
@@ -1,3 +1,76 @@
diff --git a/node_modules/react-native/jest/setup.js b/node_modules/react-native/jest/setup.js
index 5bc654475..e61bbf49c 100644
--- a/node_modules/react-native/jest/setup.js
+++ b/node_modules/react-native/jest/setup.js
@@ -15,22 +15,52 @@ const mockComponent = jest.requireActual('./mockComponent');
jest.requireActual('@react-native/polyfills/Object.es8');
jest.requireActual('@react-native/polyfills/error-guard');

-global.__DEV__ = true;
-
-global.performance = {
- now: jest.fn(Date.now),
-};
-
-global.Promise = jest.requireActual('promise');
-global.regeneratorRuntime = jest.requireActual('regenerator-runtime/runtime');
-global.window = global;
-
-global.requestAnimationFrame = function (callback) {
- return setTimeout(callback, 0);
-};
-global.cancelAnimationFrame = function (id) {
- clearTimeout(id);
-};
+Object.defineProperties(global, {
+ __DEV__: {
+ configurable: true,
+ enumerable: true,
+ value: true,
+ writable: true,
+ },
+ Promise: {
+ configurable: true,
+ enumerable: true,
+ value: jest.requireActual('promise'),
+ writable: true,
+ },
+ cancelAnimationFrame: {
+ configurable: true,
+ enumerable: true,
+ value: id => clearTimeout(id),
+ writable: true,
+ },
+ performance: {
+ configurable: true,
+ enumerable: true,
+ value: {
+ now: jest.fn(Date.now),
+ },
+ writable: true,
+ },
+ regeneratorRuntime: {
+ configurable: true,
+ enumerable: true,
+ value: jest.requireActual('regenerator-runtime/runtime'),
+ writable: true,
+ },
+ requestAnimationFrame: {
+ configurable: true,
+ enumerable: true,
+ value: callback => setTimeout(callback, 0),
+ writable: true,
+ },
+ window: {
+ configurable: true,
+ enumerable: true,
+ value: global,
+ writable: true,
+ },
+});

// there's a __mock__ for it.
jest.setMock(
diff --git a/node_modules/react-native/scripts/react_native_pods.rb b/node_modules/react-native/scripts/react_native_pods.rb
index f2ceeda..c618f77 100644
--- a/node_modules/react-native/scripts/react_native_pods.rb
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/lib/exampleData.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,8 @@ export const userStatusEmojiRealm: UserStatus['status_emoji'] = deepFreeze({
export const realm: URL = new URL('https://zulip.example.org');

/** These may be raised but should not be lowered. */
export const recentZulipVersion: ZulipVersion = new ZulipVersion('6.0-dev-2191-gf56ce7a159');
export const recentZulipFeatureLevel = 153;
export const recentZulipVersion: ZulipVersion = new ZulipVersion('8.0-dev-2894-g86100cdb4e');
export const recentZulipFeatureLevel = 226;

export const makeAccount = (
args: {|
Expand Down
File renamed without changes.
15 changes: 15 additions & 0 deletions src/action-sheets/__tests__/action-sheet-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,21 @@ describe('constructTopicActionButtons', () => {
expect(titles({ ...eg.plusBackgroundData, mute })).toContain('Mute topic');
});

test('show followTopic on muted topic', () => {
const mute = makeMuteState([[eg.stream, topic]]);
expect(titles({ ...eg.plusBackgroundData, mute })).toContain('Follow topic');
});

test('show followTopic', () => {
const mute = makeMuteState([]);
expect(titles({ ...eg.plusBackgroundData, mute })).toContain('Follow topic');
});

test('show unfollowTopic', () => {
const mute = makeMuteState([[eg.stream, topic, UserTopicVisibilityPolicy.Followed]]);
expect(titles({ ...eg.plusBackgroundData, mute })).toContain('Unfollow topic');
});

test('show resolveTopic', () => {
expect(titles({ ...eg.plusBackgroundData })).toContain('Resolve topic');
});
Expand Down
48 changes: 46 additions & 2 deletions src/action-sheets/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,24 @@ const muteTopic = {
},
};

const followTopic = {
title: 'Follow topic',
errorMessage: 'Failed to follow topic',
action: async ({ auth, streamId, topic, zulipFeatureLevel }) => {
invariant(zulipFeatureLevel >= 219, 'Should only attempt to follow topic on FL 219+');
await api.updateUserTopic(auth, streamId, topic, UserTopicVisibilityPolicy.Followed);
},
};

const unfollowTopic = {
title: 'Unfollow topic',
errorMessage: 'Failed to unfollow topic',
action: async ({ auth, streamId, topic, zulipFeatureLevel }) => {
invariant(zulipFeatureLevel >= 219, 'Should only attempt to unfollow topic on FL 219+');
await api.updateUserTopic(auth, streamId, topic, UserTopicVisibilityPolicy.None);
},
};

const copyLinkToTopic = {
title: 'Copy link to topic',
errorMessage: 'Failed to copy topic link',
Expand Down Expand Up @@ -651,6 +669,11 @@ export const constructTopicActionButtons = (args: {|
const sub = subscriptions.get(streamId);
const streamMuted = !!sub && !sub.in_home_view;

// TODO(server-7.0): Simplify this condition away.
const supportsUnmutingTopics = zulipFeatureLevel >= 170;
// TODO(server-8.0): Simplify this condition away.
const supportsFollowingTopics = zulipFeatureLevel >= 219;

const buttons = [];
const unreadCount = getUnreadCountForTopic(unread, streamId, topic);
if (unreadCount > 0) {
Expand All @@ -661,25 +684,46 @@ export const constructTopicActionButtons = (args: {|
switch (getTopicVisibilityPolicy(mute, streamId, topic)) {
case UserTopicVisibilityPolicy.Muted:
buttons.push(unmuteTopic);
if (supportsFollowingTopics) {
buttons.push(followTopic);
}
break;
case UserTopicVisibilityPolicy.None:
case UserTopicVisibilityPolicy.Unmuted:
buttons.push(muteTopic);
if (supportsFollowingTopics) {
buttons.push(followTopic);
}
break;
case UserTopicVisibilityPolicy.Followed:
buttons.push(muteTopic);
if (supportsFollowingTopics) {
buttons.push(unfollowTopic);
}
break;
}
} else if (sub && streamMuted) {
// Muted stream.
// TODO(server-7.0): Simplify this condition away.
if (zulipFeatureLevel >= 170) {
if (supportsUnmutingTopics) {
switch (getTopicVisibilityPolicy(mute, streamId, topic)) {
case UserTopicVisibilityPolicy.None:
case UserTopicVisibilityPolicy.Muted:
buttons.push(unmuteTopicInMutedStream);
if (supportsFollowingTopics) {
buttons.push(followTopic);
}
break;
case UserTopicVisibilityPolicy.Unmuted:
buttons.push(muteTopic);
if (supportsFollowingTopics) {
buttons.push(followTopic);
}
break;
case UserTopicVisibilityPolicy.Followed:
buttons.push(muteTopic);
if (supportsFollowingTopics) {
buttons.push(unfollowTopic);
}
break;
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/common/Icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,5 +117,6 @@ export const IconAlertTriangle: SpecificIconType = makeIcon(Feather, 'alert-tria
// WildcardMentionItem depends on this being square.
export const IconWildcardMention: SpecificIconType = makeIcon(FontAwesome, 'bullhorn');

// eslint-disable-next-line react/function-component-definition
/* eslint-disable react/function-component-definition */
export const IconWebPublic: SpecificIconType = props => <ZulipIcon name="globe" {...props} />;
export const IconFollow: SpecificIconType = props => <ZulipIcon name="follow" {...props} />;
14 changes: 14 additions & 0 deletions src/mute/muteModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,20 @@ export function isTopicVisible(
}
}

/**
* Whether the user is following this topic.
*/
export function isTopicFollowed(streamId: number, topic: string, mute: MuteState): boolean {
switch (getTopicVisibilityPolicy(mute, streamId, topic)) {
case UserTopicVisibilityPolicy.None:
case UserTopicVisibilityPolicy.Muted:
case UserTopicVisibilityPolicy.Unmuted:
return false;
case UserTopicVisibilityPolicy.Followed:
return true;
}
}

//
//
// Reducer.
Expand Down
24 changes: 14 additions & 10 deletions src/pm-conversations/PmConversationList.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,20 @@
/* @flow strict-local */
import React, { useCallback } from 'react';
import React, { useCallback, useMemo } from 'react';
import type { Node } from 'react';
import { FlatList } from 'react-native';
import { useDispatch, useSelector } from '../react-redux';

import type { PmConversationData, UserOrBot } from '../types';
import { createStyleSheet } from '../styles';
import { type PmKeyUsers } from '../utils/recipient';
import { pm1to1NarrowFromUser, pmNarrowFromUsers } from '../utils/narrow';
import UserItem from '../users/UserItem';
import GroupPmConversationItem from './GroupPmConversationItem';
import { doNarrow } from '../actions';
import { getMutedUsers } from '../selectors';

const styles = createStyleSheet({
list: {
flex: 1,
flexDirection: 'column',
},
});

type Props = $ReadOnly<{|
conversations: $ReadOnlyArray<PmConversationData>,
extraPaddingEnd?: number,
|}>;

/**
Expand All @@ -44,9 +37,20 @@ export default function PmConversationList(props: Props): Node {
[dispatch],
);

const { conversations } = props;
const { conversations, extraPaddingEnd = 0 } = props;
const mutedUsers = useSelector(getMutedUsers);

const styles = useMemo(
() => ({
list: {
flex: 1,
flexDirection: 'column',
paddingRight: extraPaddingEnd,
},
}),
[extraPaddingEnd],
);

return (
<FlatList
style={styles.list}
Expand Down
10 changes: 6 additions & 4 deletions src/streams/StreamItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type Props = $ReadOnly<{|
unreadCount?: number,
iconSize: number,
offersSubscribeButton?: boolean,
extraPaddingEnd?: number,
// These stream names are here for a mix of good reasons and (#3918) bad ones.
// To audit all uses, change `name` to write-only (`-name:`), and run Flow.
onPress: ({ stream_id: number, name: string, ... }) => void,
Expand Down Expand Up @@ -87,6 +88,7 @@ export default function StreamItem(props: Props): Node {
iconSize,
offersSubscribeButton = false,
unreadCount,
extraPaddingEnd = 0,
onPress,
onSubscribeButtonPressed,
} = props;
Expand Down Expand Up @@ -125,9 +127,9 @@ export default function StreamItem(props: Props): Node {
wrapper: {
flexDirection: 'row',
alignItems: 'center',
...(handleExpandCollapse
? { paddingRight: 16 }
: { paddingVertical: 8, paddingHorizontal: 16 }),
paddingVertical: handleExpandCollapse ? 0 : 8,
paddingLeft: handleExpandCollapse ? 0 : 16,
paddingRight: extraPaddingEnd + 16,
backgroundColor,
opacity: isMuted ? 0.5 : 1,
},
Expand Down Expand Up @@ -156,7 +158,7 @@ export default function StreamItem(props: Props): Node {
fontSize: 12,
},
}),
[backgroundColor, handleExpandCollapse, isMuted, textColor],
[backgroundColor, extraPaddingEnd, handleExpandCollapse, isMuted, textColor],
);

const collapseButton = handleExpandCollapse && (
Expand Down
20 changes: 18 additions & 2 deletions src/streams/TopicItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { View } from 'react-native';
import { useActionSheet } from '@expo/react-native-action-sheet';

import styles, { BRAND_COLOR, createStyleSheet } from '../styles';
import { IconMention } from '../common/Icons';
import { IconMention, IconFollow } from '../common/Icons';
import ZulipText from '../common/ZulipText';
import Touchable from '../common/Touchable';
import UnreadCount from '../common/UnreadCount';
Expand All @@ -22,10 +22,11 @@ import {
getOwnUser,
getZulipFeatureLevel,
} from '../selectors';
import { getMute } from '../mute/muteModel';
import { getMute, isTopicFollowed } from '../mute/muteModel';
import { getUnread } from '../unread/unreadModel';
import { getOwnUserRole } from '../permissionSelectors';
import { useNavigation } from '../react-navigation';
import { ThemeContext } from '../styles/theme';

const componentStyles = createStyleSheet({
selectedRow: {
Expand All @@ -44,6 +45,11 @@ const componentStyles = createStyleSheet({
muted: {
opacity: 0.5,
},
followedIcon: {
paddingLeft: 4,
width: 20,
opacity: 0.2,
},
});

type Props = $ReadOnly<{|
Expand Down Expand Up @@ -84,6 +90,10 @@ export default function TopicItem(props: Props): Node {
zulipFeatureLevel: getZulipFeatureLevel(state),
}));

const theme = useContext(ThemeContext);
const iconColor = theme.themeName === 'dark' ? 'white' : 'black';
const isFollowed = useSelector(state => isTopicFollowed(streamId, name, getMute(state)));

return (
<Touchable
onPress={() => onPress(streamId, name)}
Expand Down Expand Up @@ -112,6 +122,12 @@ export default function TopicItem(props: Props): Node {
/>
{isMentioned && <IconMention size={14} style={componentStyles.mentionedLabel} />}
<UnreadCount count={unreadCount} inverse={isSelected} />
{isFollowed ? (
<IconFollow style={componentStyles.followedIcon} size={14} color={iconColor} />
) : (
// $FlowFixMe[incompatible-type]: complains about `color` but that's not present
<View style={componentStyles.followedIcon} />
)}
</View>
</Touchable>
);
Expand Down
Loading
Loading