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

native: use channel.meta to store custom channel config on API #4371

Draft
wants to merge 27 commits into
base: release-channels-updates
Choose a base branch
from
Draft
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1c5a177
Merge remote-tracking branch 'origin/release-channels-updates' into d…
davidisaaclee Jan 21, 2025
b85f972
Add configuration dict to ContentRenderer
davidisaaclee Jan 21, 2025
9f0aad1
Use ChannelMetadata instead of specific ChannelMetadataSchemaV1
davidisaaclee Jan 21, 2025
6147499
Remove content config from createChannel
davidisaaclee Jan 21, 2025
814aaa8
Set content config based on legacy channel types on creation
davidisaaclee Jan 21, 2025
39a22b3
Remove unused import
davidisaaclee Jan 21, 2025
c0a4792
Send channel meta updates over API
davidisaaclee Jan 21, 2025
4288810
Remove content config from group DMs
davidisaaclee Jan 21, 2025
79d2686
Include channelId in client metadata update event
davidisaaclee Jan 22, 2025
e55c93f
Update local channel cfg on event
davidisaaclee Jan 22, 2025
e462038
Disable receiving content configuration over groups APIs
davidisaaclee Jan 23, 2025
8a82f25
Prevent %groups overwriting contentConfiguration with missing field
davidisaaclee Jan 23, 2025
32e8240
channels: rename wrong %channel-action-2 to %channel-action-1
mikolajpp Jan 24, 2025
aae9859
channels: replace wrong %channel-action-2 mark. groups-ui: fix /v5/in…
mikolajpp Jan 24, 2025
771c1a2
Fix mistyped parameter name showAuthor -> showAuthors
davidisaaclee Jan 24, 2025
faa4c57
Point updateChannelMeta to correct mark
davidisaaclee Jan 24, 2025
f6fc3ba
Remove unused StructuredChannelDescription type
davidisaaclee Jan 24, 2025
a2ef550
Use groups-ui /v5/init
davidisaaclee Jan 24, 2025
1563478
channels: add missing fields
arthyn Jan 25, 2025
d3817b9
Make Channel.meta nullable (legacy channels do not have a meta)
davidisaaclee Jan 27, 2025
39faec7
Use correct ChannelFromServer type in GroupsInit response
davidisaaclee Jan 27, 2025
957d379
Add meta to ChannelInit payload, and use to set channel metas on init
davidisaaclee Jan 27, 2025
38f42eb
Avoid forced unwrap (after seeing it cause a crash)
davidisaaclee Jan 27, 2025
dbd627d
Make custom channels flag visible to dev clients
davidisaaclee Jan 27, 2025
5549799
Merge remote-tracking branch 'origin/release-channels-updates' into d…
davidisaaclee Jan 29, 2025
3be36f2
Merge remote-tracking branch 'origin/develop' into dil/integrate-cust…
davidisaaclee Jan 29, 2025
a203235
Merge branch 'release-channels-updates' into dil/integrate-custom-meta
arthyn Mar 4, 2025
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
2 changes: 1 addition & 1 deletion apps/tlon-mobile/ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1905,4 +1905,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: 0cb7a78e5777e69c86c1bf4bb5135fd660376dbe

COCOAPODS: 1.16.2
COCOAPODS: 1.15.2
2 changes: 1 addition & 1 deletion packages/app/features/settings/FeatureFlagScreen.tsx
Original file line number Diff line number Diff line change
@@ -29,7 +29,7 @@ export function FeatureFlagScreen({ navigation }: Props) {
Object.entries(featureFlags.featureMeta)
.filter(([name]) => {
if (name === 'customChannelCreation') {
return isTlonEmployee;
return isTlonEmployee || __DEV__;
}
return true;
})
14 changes: 11 additions & 3 deletions packages/app/ui/components/ContactNameV2.tsx
Original file line number Diff line number Diff line change
@@ -50,12 +50,20 @@ export const useContactNameProps = ({
]);
};

export const useContactName = (options: string | ContactNameOptions) => {
export function useContactName(options: string | ContactNameOptions): string;
export function useContactName(
options: string | ContactNameOptions | null
): string | null;
export function useContactName(
options: string | ContactNameOptions | null
): string | null {
const resolvedOptions = useMemo(() => {
return typeof options === 'string' ? { contactId: options } : options;
return typeof options === 'string' || options == null
? { contactId: options ?? '' }
: options;
}, [options]);
return useContactNameProps(resolvedOptions).children;
};
}

const BaseContactName = RawText.styleable<{
contactId: string;
2 changes: 1 addition & 1 deletion packages/app/ui/components/postCollectionViews/shared.tsx
Original file line number Diff line number Diff line change
@@ -29,7 +29,7 @@ export function ConnectedPostView({
return null;
}
return {
showAuthor: JSONValue.asBoolean(cfg.showAuthor, false),
showAuthor: JSONValue.asBoolean(cfg.showAuthors, false),
showReplies: JSONValue.asBoolean(cfg.showReplies, false),
};
}, [ctx.collectionConfiguration]);
2 changes: 1 addition & 1 deletion packages/app/ui/contexts/componentsKits.tsx
Original file line number Diff line number Diff line change
@@ -105,7 +105,7 @@ const BUILTIN_CONTENT_RENDERERS: { [id: string]: RenderItemType } = {
[PostContentRendererId.audio]: AudioPost,
[PostContentRendererId.color]: ColorPost,
[PostContentRendererId.raw]: ({ post, contentRendererConfiguration }) => {
const contactName = useContactName(post.author!.id);
const contactName = useContactName(post.author?.id ?? null);
return (
<Text
style={{
98 changes: 37 additions & 61 deletions packages/shared/src/api/channelContentConfig.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { JSONValue } from '../types/JSONValue';
import { ChannelMetadata } from '../urbit';
import { ValuesOf } from '../utils';

interface BaseParameterSpec {
@@ -284,72 +285,47 @@ export namespace ChannelContentConfiguration {
): ParameterizedId<CollectionRendererId> {
return ParameterizedId.coerce(configuration.defaultPostCollectionRenderer);
}
}

/**
* We use a channel's `description` field to store structured data. This
* module provides helpers for managing that data.
*/
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace StructuredChannelDescriptionPayload {
type Encoded = string | null | undefined;
interface Decoded {
channelContentConfiguration?: ChannelContentConfiguration;
description?: string;
}
export function toApiMeta(cfg: ChannelContentConfiguration): ChannelMetadata {
return {
version: 1,

export function encode(payload: Decoded): Encoded {
return JSON.stringify(payload);
}
postInput: ((x) => ({
postType: x.id,

/**
* Attempts to decode a `description` string into a structured payload.
*
* - If `description` is null/undefined, returns a payload with no
* description nor configuration.
* - If `description` is not valid JSON, returns a payload with the
* description as the input string.
* - If `description` validates as the expected
* `StructuredChannelDescriptionPayload` JSON, returns the decoded payload.
*/
export function decode(encoded: Encoded): Decoded {
// TODO: This should be validated - we'll be deserializing untrusted data
if (encoded == null) {
return {};
}
try {
const out = JSON.parse(encoded);
if ('channelContentConfiguration' in out) {
if (typeof out.channelContentConfiguration !== 'object') {
throw new Error('Invalid configuration');
}
// add a little robustness - if the configuration is missing a field,
// just add a default in to avoid crashing
out.channelContentConfiguration = ((raw) => {
const cfg = {
draftInput: DraftInputId.chat,
defaultPostContentRenderer: PostContentRendererId.chat,
defaultPostCollectionRenderer: CollectionRendererId.chat,
...raw,
} as ChannelContentConfiguration;
type: x.id,
configuration: x.configuration,
}))(ChannelContentConfiguration.draftInput(cfg)),

// add defaults to some standard params
const collCfgWithDefaults = ParameterizedId.coerce(
cfg.defaultPostCollectionRenderer
);
collCfgWithDefaults.configuration = {
showAuthors: true,
showReplies: true,
...collCfgWithDefaults.configuration,
};
postCollectionRenderer: ((x) => ({
id: x.id,
configuration: x.configuration,
}))(ChannelContentConfiguration.defaultPostCollectionRenderer(cfg)),

return cfg;
})(out.channelContentConfiguration);
}
return out;
} catch (_err) {
return { description: encoded.length === 0 ? undefined : encoded };
}
defaultContentRenderer: ((x) => ({
rendererId: x.id,
configuration: x.configuration,
}))(ChannelContentConfiguration.defaultPostContentRenderer(cfg)),
};
}

export function fromApiMeta(
meta: ChannelMetadata
): ChannelContentConfiguration {
return {
draftInput: {
id: meta.postInput.type as DraftInputId,
configuration: meta.postInput.configuration,
},
defaultPostContentRenderer: {
id: meta.defaultContentRenderer.rendererId as PostContentRendererId,
configuration: meta.defaultContentRenderer.configuration,
},
defaultPostCollectionRenderer: {
id: meta.postCollectionRenderer.id as CollectionRendererId,
configuration: meta.postCollectionRenderer.configuration,
},
};
}
}

48 changes: 28 additions & 20 deletions packages/shared/src/api/channelsApi.ts
Original file line number Diff line number Diff line change
@@ -91,7 +91,8 @@ export type MarkChannelReadUpdate = {

export type MetaUpdate = {
type: 'channelMetaUpdate';
meta: Stringified<ub.ChannelMetadataSchemaV1> | null;
meta: Stringified<ub.ChannelMetadata> | null;
channelId: string;
};

export type ChannelsUpdate =
@@ -129,14 +130,30 @@ export const createChannel = async ({
);
};

export const setupChannelFromTemplate = async (
exampleChannelId: string,
targetChannelId: string
) => {
return client.thread<string>({
desk: 'groups',
inputMark: 'hook-setup-template-args',
outputMark: 'json',
threadName: 'channel-setup-from-template',
body: {
example: exampleChannelId,
target: targetChannelId,
},
});
};

export async function updateChannelMeta(
channelId: string,
metaPayload: Stringified<ub.ChannelMetadataSchemaV1> | null
metaPayload: Stringified<ub.ChannelMetadata> | null
) {
return trackedPoke<ub.ChannelsResponse>(
{
app: 'channels',
mark: 'channel-action',
mark: 'channel-action-1',
json: {
channel: {
nest: channelId,
@@ -153,22 +170,6 @@ export async function updateChannelMeta(
);
}

export const setupChannelFromTemplate = async (
exampleChannelId: string,
targetChannelId: string
) => {
return client.thread<string>({
desk: 'groups',
inputMark: 'hook-setup-template-args',
outputMark: 'json',
threadName: 'channel-setup-from-template',
body: {
example: exampleChannelId,
target: targetChannelId,
},
});
};

export const subscribeToChannelsUpdates = async (
eventHandler: (update: ChannelsUpdate) => void
) => {
@@ -193,14 +194,20 @@ export type ChannelInit = {
channelId: string;
writers: string[];
readers: string[];
meta: ub.ChannelMetadata | null;
};

export function toClientChannelInit(
id: string,
channel: ub.Channel,
readers: string[]
): ChannelInit {
return { channelId: id, writers: channel.perms.writers ?? [], readers };
return {
channelId: id,
writers: channel.perms.writers ?? [],
readers,
meta: channel.meta,
};
}

export const toChannelsUpdate = (
@@ -240,6 +247,7 @@ export const toChannelsUpdate = (
return {
type: 'channelMetaUpdate',
meta: channelEvent.response.meta,
channelId,
};
}

14 changes: 1 addition & 13 deletions packages/shared/src/api/chatApi.ts
Original file line number Diff line number Diff line change
@@ -8,10 +8,6 @@ import {
getCanonicalPostId,
toClientMeta,
} from './apiUtils';
import {
ChannelContentConfiguration,
StructuredChannelDescriptionPayload,
} from './channelContentConfig';
import { toPostData, toPostReplyData, toReplyMeta } from './postsApi';
import { getCurrentUserId, poke, scry, subscribe, trackedPoke } from './urbit';

@@ -339,21 +335,13 @@ export const toClientGroupDms = (groupDms: ub.Clubs): GetDmsResponse => {

const metaFields = toClientMeta(club.meta);

// Channel meta is different from other metas, since we can overload the
// `description` to fit other channel-specific data.
// Attempt to decode that extra info here.
const decodedDesc = StructuredChannelDescriptionPayload.decode(
metaFields.description
);

return {
id,
type: 'groupDm',
...metaFields,
isDmInvite: !isJoined && isInvited,
members: [...joinedMembers, ...invitedMembers],
contentConfiguration: decodedDesc.channelContentConfiguration,
description: decodedDesc.description,
description: metaFields.description,
};
})
.filter(Boolean) as db.Channel[];
13 changes: 2 additions & 11 deletions packages/shared/src/api/groupsApi.ts
Original file line number Diff line number Diff line change
@@ -13,7 +13,6 @@ import {
getJoinStatusFromGang,
} from '../urbit';
import { parseGroupChannelId, parseGroupId, toClientMeta } from './apiUtils';
import { StructuredChannelDescriptionPayload } from './channelContentConfig';
import {
getCurrentUserId,
poke,
@@ -1640,9 +1639,6 @@ function toClientChannel({
channel: ub.GroupChannel;
groupId: string;
}): db.Channel {
const { description, channelContentConfiguration } =
StructuredChannelDescriptionPayload.decode(channel.meta.description);

const readerRoles = (channel.readers ?? []).map((roleId) => ({
channelId: id,
roleId,
@@ -1663,8 +1659,7 @@ function toClientChannel({
iconImage: omitEmpty(channel.meta.image),
title: omitEmpty(channel.meta.title),
coverImage: omitEmpty(channel.meta.cover),
description,
contentConfiguration: channelContentConfiguration,
description: omitEmpty(channel.meta.description),
currentUserIsHost: hostUserId === currentUserId,
readerRoles,
writerRoles,
@@ -1680,18 +1675,14 @@ function toClientChannelFromPreview({
channel: ub.ChannelPreview;
groupId: string;
}): db.Channel {
const { description, channelContentConfiguration } =
StructuredChannelDescriptionPayload.decode(channel.meta.description);

return {
id,
groupId,
type: getChannelType(id),
iconImage: omitEmpty(channel.meta.image),
title: omitEmpty(channel.meta.title),
coverImage: omitEmpty(channel.meta.cover),
description,
contentConfiguration: channelContentConfiguration,
description: omitEmpty(channel.meta.description),
};
}

Loading