Skip to content

Commit

Permalink
Merge pull request #4216 from tloncorp/ja/unjoined-channels
Browse files Browse the repository at this point in the history
GroupChannelsScreen: show unjoined channels, tap to join
  • Loading branch information
jamesacklin authored Nov 22, 2024
2 parents d334b9a + 5cf4974 commit fc02337
Show file tree
Hide file tree
Showing 12 changed files with 227 additions and 16 deletions.
19 changes: 19 additions & 0 deletions packages/app/features/top/GroupChannelsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ export function GroupChannelsScreenContent({
null
);
const { group } = useGroupContext({ groupId: id, isFocused });
const { data: unjoinedChannels } = store.useUnjoinedGroupChannels(
group?.id ?? ''
);

const pinnedItems = useMemo(() => {
return pins ?? [];
Expand All @@ -60,6 +63,20 @@ export function GroupChannelsScreenContent({

const [enableCustomChannels] = useFeatureFlag('customChannelCreation');

const handleJoinChannel = useCallback(
async (channel: db.Channel) => {
try {
await store.joinGroupChannel({
channelId: channel.id,
groupId: id,
});
} catch (error) {
console.error('Failed to join channel:', error);
}
},
[id]
);

return (
<ChatOptionsProvider
groupId={id}
Expand All @@ -73,7 +90,9 @@ export function GroupChannelsScreenContent({
<GroupChannelsScreenView
onChannelPressed={handleChannelSelected}
onBackPressed={handleGoBackPressed}
onJoinChannel={handleJoinChannel}
group={group}
unjoinedChannels={unjoinedChannels}
enableCustomChannels={enableCustomChannels}
/>
<InviteUsersSheet
Expand Down
21 changes: 21 additions & 0 deletions packages/shared/src/api/channelsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,3 +396,24 @@ export const leaveChannel = async (channelId: string) => {
}
);
};

export const joinChannel = async (channelId: string, groupId: string) => {
return trackedPoke<ub.ChannelsResponse>(
{
app: 'channels',
mark: 'channel-action',
json: {
channel: {
nest: channelId,
action: {
join: groupId,
},
},
},
},
{ app: 'channels', path: '/v1' },
(event) => {
return 'join' in event.response && event.nest === channelId;
}
);
};
78 changes: 72 additions & 6 deletions packages/shared/src/db/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,60 @@ export const getPendingChats = createReadQuery(
['groups', 'channels']
);

export const getUnjoinedGroupChannels = createReadQuery(
'getUnjoinedGroupChannels',
async (groupId: string, ctx: QueryCtx) => {
const currentUserId = getCurrentUserId();
const allUnjoined = await ctx.db.query.channels.findMany({
where: and(
eq($channels.groupId, groupId),
eq($channels.currentUserIsMember, false)
),
with: {
readerRoles: true,
group: {
with: {
roles: {
with: {
members: true,
},
},
},
},
},
});

const unjoinedIds = allUnjoined.map((c) => c.id);
const unjoinedIndex = new Map<string, Channel>();
for (const channel of allUnjoined) {
unjoinedIndex.set(channel.id, channel);
}

const joinableSubset = unjoinedIds.filter((id) => {
const channel = unjoinedIndex.get(id);
const isOpenChannel = channel?.readerRoles?.length === 0;

const userRolesForGroup =
channel?.group?.roles
?.filter((role) =>
role.members?.map((m) => m.contactId).includes(currentUserId)
)
.map((role) => role.id) ?? [];

const isClosedButCanRead = channel?.readerRoles
?.map((r) => r.roleId)
.some((r) => userRolesForGroup.includes(r));

return isOpenChannel || isClosedButCanRead;
});

return joinableSubset
.map((id) => unjoinedIndex.get(id))
.filter((c) => c !== undefined) as Channel[];
},
['channels']
);

export const getPins = createReadQuery(
'getPins',
async (ctx: QueryCtx): Promise<Pin[]> => {
Expand Down Expand Up @@ -1711,17 +1765,29 @@ export const addJoinedGroupChannel = createWriteQuery(
async ({ channelId }: { channelId: string }, ctx: QueryCtx) => {
logger.log('addJoinedGroupChannel', channelId);

await ctx.db.insert($groupNavSectionChannels).values({
channelId,
groupNavSectionId: 'default',
});

return await ctx.db
// First update the channel membership
await ctx.db
.update($channels)
.set({
currentUserIsMember: true,
})
.where(eq($channels.id, channelId));

// Then check if channel exists in any section
const existingInAnySection = await ctx.db
.select()
.from($groupNavSectionChannels)
.where(eq($groupNavSectionChannels.channelId, channelId));

// Only add to default if it's not
if (existingInAnySection.length === 0) {
await ctx.db.insert($groupNavSectionChannels).values({
channelId,
groupNavSectionId: 'default',
});
}

return;
},
['channels']
);
Expand Down
27 changes: 26 additions & 1 deletion packages/shared/src/store/channelActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,8 +323,33 @@ export async function leaveGroupChannel(channelId: string) {
try {
await api.leaveChannel(channelId);
} catch (e) {
console.error('Failed to leave chat channel', e);
console.error('Failed to leave channel', e);
// rollback optimistic update
await db.updateChannel({ id: channelId, currentUserIsMember: true });
}
}

export async function joinGroupChannel({
channelId,
groupId,
}: {
channelId: string;
groupId: string;
}) {
// optimistic update
await db.updateChannel({
id: channelId,
currentUserIsMember: true,
});

try {
await api.joinChannel(channelId, groupId);
} catch (e) {
// rollback on failure
logger.error('Failed to join group channel');
await db.updateChannel({
id: channelId,
currentUserIsMember: false,
});
}
}
14 changes: 14 additions & 0 deletions packages/shared/src/store/dbHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,20 @@ export const usePendingChats = (
});
};

export const useUnjoinedGroupChannels = (groupId: string) => {
const deps = useKeyFromQueryDeps(db.getUnjoinedGroupChannels);
return useQuery({
queryKey: [['unjoinedChannels', groupId], deps],
queryFn: async () => {
if (!groupId) {
return [];
}
const unjoined = await db.getUnjoinedGroupChannels(groupId);
return unjoined;
},
});
};

export const usePins = (
queryConfig?: CustomQueryConfig<db.Pin[]>
): UseQueryResult<db.Pin[] | null> => {
Expand Down
8 changes: 7 additions & 1 deletion packages/ui/src/components/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,15 +129,17 @@ export const GroupAvatar = React.memo(function GroupAvatarComponent({
export const ChannelAvatar = React.memo(function ChannelAvatarComponent({
model,
useTypeIcon,
dimmed,
...props
}: {
model: db.Channel;
useTypeIcon?: boolean;
dimmed?: boolean;
} & AvatarProps) {
const channelTitle = utils.useChannelTitle(model);

if (useTypeIcon) {
return <ChannelTypeAvatar channel={model} {...props} />;
return <ChannelTypeAvatar channel={model} dimmed={dimmed} {...props} />;
} else if (model.type === 'dm') {
return (
<ContactAvatar
Expand Down Expand Up @@ -168,13 +170,17 @@ export const ChannelAvatar = React.memo(function ChannelAvatarComponent({
export const ChannelTypeAvatar = React.memo(
function ChannelTypeAvatarComponent({
channel,
dimmed,
...props
}: {
channel: db.Channel;
dimmed?: boolean;
} & ComponentProps<typeof AvatarFrame>) {
return (
<SystemIconAvatar
{...props}
color={dimmed ? '$tertiaryText' : undefined}
backgroundColor={dimmed ? '$secondaryBackground' : undefined}
icon={getChannelTypeIcon(channel.type) || 'Channel'}
/>
);
Expand Down
6 changes: 4 additions & 2 deletions packages/ui/src/components/ChannelNavSection.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as db from '@tloncorp/shared/db';
import { useCallback, useMemo } from 'react';
import { SizableText, YStack } from 'tamagui';
import { SizableText, YStack, getVariableValue, useTheme } from 'tamagui';

import { ChannelListItem } from './ListItem';

Expand Down Expand Up @@ -32,13 +32,15 @@ export default function ChannelNavSection({
[channels]
);

const listSectionTitleColor = getVariableValue(useTheme().secondaryText);

return (
<YStack key={section.id}>
<SizableText
paddingHorizontal="$l"
paddingVertical="$xl"
fontSize="$s"
color="$secondaryText"
color={listSectionTitleColor}
>
{section.title}
</SizableText>
Expand Down
6 changes: 4 additions & 2 deletions packages/ui/src/components/ChannelNavSections.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as db from '@tloncorp/shared/db';
import { useMemo } from 'react';
import { SizableText, YStack } from 'tamagui';
import { SizableText, YStack, getVariableValue, useTheme } from 'tamagui';

import ChannelNavSection from './ChannelNavSection';
import { ChannelListItem } from './ListItem';
Expand Down Expand Up @@ -49,6 +49,8 @@ export default function ChannelNavSections({
[unGroupedChannels]
);

const listSectionTitleColor = getVariableValue(useTheme().secondaryText);

if (sortBy === 'recency') {
return (
<YStack paddingBottom={paddingBottom} alignSelf="stretch" gap="$s">
Expand Down Expand Up @@ -92,7 +94,7 @@ export default function ChannelNavSections({
paddingHorizontal="$l"
paddingVertical="$xl"
fontSize="$s"
color="$secondaryText"
color={listSectionTitleColor}
>
All Channels
</SizableText>
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/components/ChatOptionsSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,7 @@ export function ChannelOptions({
if (!isWeb) {
Alert.alert(
`Leave ${title}?`,
'This will be removed from the list',
'You will no longer receive updates from this channel.',
[
{
text: 'Cancel',
Expand Down
Loading

0 comments on commit fc02337

Please sign in to comment.