fix: improved Slack widget

This commit is contained in:
Morten Olsen
2023-06-20 08:31:18 +02:00
parent 673df20514
commit fec30cc430
30 changed files with 718 additions and 180 deletions

View File

@@ -5,16 +5,18 @@ type AvatarProps = {
url?: string;
name?: string;
decal?: React.ReactNode;
size?: 'sm' | 'md' | 'lg';
size?: keyof typeof sizes;
};
const sizes = {
xs: 20,
sm: 28,
md: 50,
lg: 75,
};
const fontSizes = {
xs: 8,
sm: 10,
md: 24,
lg: 32,

View File

@@ -0,0 +1,29 @@
import { StoryObj, Meta } from '@storybook/react';
import { Form } from '.';
import { Button } from '../button';
type Story = StoryObj<typeof Form>;
const meta = {
title: 'Components/Form',
component: Form,
} satisfies Meta<typeof Form>;
const docs: Story = {
render: () => (
<Form>
<Form.Field label="Owner">
<Form.Input />
</Form.Field>
<Form.Field label="Repo">
<Form.Input />
</Form.Field>
<Form.Buttons>
<Button title="Save" />
</Form.Buttons>
</Form>
),
};
export default meta;
export { docs };

View File

@@ -0,0 +1,62 @@
import styled from 'styled-components';
import { View } from '../view';
import { Typography } from '../../typography';
type RootProps = React.ComponentProps<typeof View> & {
children: React.ReactNode;
};
const Root: React.FC<RootProps> = ({ children, ...props }) => (
<View $fc $gap="sm" {...props}>
{children}
</View>
);
type FieldProps = React.ComponentProps<typeof View> & {
label: string;
children: React.ReactNode;
};
const Label = styled(Typography)`
font-weight: bold;
`;
const FieldWrapper = styled(View)``;
const Field: React.FC<FieldProps> = ({ label, children, ...props }) => (
<FieldWrapper {...props} $fc>
<Label as="label" variant="overline">
{label}
</Label>
{children}
</FieldWrapper>
);
const Input = styled.input`
all: unset;
display: block;
border-radius: 4px;
padding: 8px 12px;
box-shadow: 0 0 0 1px ${({ theme }) => theme.colors.bg.highlight100};
&:focus {
box-shadow: 0 0 0 2px ${({ theme }) => theme.colors.bg.highlight};
}
`;
const Buttons = styled(View)`
display: flex;
justify-content: flex-end;
border-radius: 4px;
overflow: hidden;
padding: 8px 12px;
background-color: ${({ theme }) => theme.colors.bg.highlight100};
`;
const Form = Object.assign(Root, {
Field,
Input,
Buttons,
});
export { Form };

View File

@@ -10,3 +10,4 @@ export * from './button';
export * from './masonry';
export * from './code-editor';
export * from './popover';
export * from './form';

View File

@@ -1,5 +1,4 @@
import type { Issue as IssueType } from '@linear/sdk';
import { Link } from 'react-router-dom';
import { IssueSearchResult } from '@linear/sdk/dist/_generated_documents';
type IssueProps = {
@@ -8,9 +7,7 @@ type IssueProps = {
const Issue: React.FC<IssueProps> = ({ issue }) => {
return (
<div>
<Link to={`/linear/issue?id=${issue.id}`}>
<h3 className="text-lg font-bold">{issue.title}</h3>
</Link>
{issue.description}
</div>
);

View File

@@ -29,6 +29,11 @@ const GlobalStyle = createGlobalStyle`
padding: 0;
${styles.body}
}
a {
color: ${({ theme }) => theme.colors.bg.highlight};
text-decoration: none;
}
`;
const UIProvider: React.FC<UIProviderProps> = ({ children }) => {

View File

@@ -1,5 +1,6 @@
import { useCallback, useState } from 'react';
import { Props } from './schema';
import { Button, Form } from '@refocus/ui';
type EditorProps = {
value?: Props;
@@ -24,34 +25,32 @@ const Edit: React.FC<EditorProps> = ({ value, save }) => {
}, [owner, repo, branch, path, highlight, save]);
return (
<div>
<input
placeholder="Owner"
value={owner}
onChange={(e) => setOwner(e.target.value)}
/>
<input
placeholder="Repo"
value={repo}
onChange={(e) => setRepo(e.target.value)}
/>
<input
placeholder="Branch"
<Form $fc>
<Form.Field label="Owner">
<Form.Input value={owner} onChange={(e) => setOwner(e.target.value)} />
</Form.Field>
<Form.Field label="Repo">
<Form.Input value={repo} onChange={(e) => setRepo(e.target.value)} />
</Form.Field>
<Form.Field label="Branch">
<Form.Input
value={branch}
onChange={(e) => setBranch(e.target.value)}
/>
<input
placeholder="Path"
value={path}
onChange={(e) => setPath(e.target.value)}
/>
<input
placeholder="Highlights"
</Form.Field>
<Form.Field label="Path">
<Form.Input value={path} onChange={(e) => setPath(e.target.value)} />
</Form.Field>
<Form.Field label="Highlights">
<Form.Input
value={highlight}
onChange={(e) => setHighlight(e.target.value)}
/>
<button onClick={handleSave}>Save</button>
</div>
</Form.Field>
<Form.Buttons>
<Button onClick={handleSave} title="Save" />
</Form.Buttons>
</Form>
);
};

View File

@@ -1,5 +1,6 @@
import { useCallback, useState } from 'react';
import { Props } from './schema';
import { Button, Form } from '@refocus/ui';
type EditorProps = {
value?: Props;
@@ -20,24 +21,24 @@ const Edit: React.FC<EditorProps> = ({ value, save }) => {
}, [owner, repo, pr, save]);
return (
<div>
<input
placeholder="Owner"
value={owner}
onChange={(e) => setOwner(e.target.value)}
/>
<input
placeholder="Repo"
value={repo}
onChange={(e) => setRepo(e.target.value)}
/>
<input
<Form>
<Form.Field label="Owner">
<Form.Input value={owner} onChange={(e) => setOwner(e.target.value)} />
</Form.Field>
<Form.Field label="Repo">
<Form.Input value={repo} onChange={(e) => setRepo(e.target.value)} />
</Form.Field>
<Form.Field label="PR">
<Form.Input
placeholder="PR"
value={pr}
onChange={(e) => setPr(e.target.value)}
/>
<button onClick={handleSave}>Save</button>
</div>
</Form.Field>
<Form.Buttons>
<Button onClick={handleSave} title="Save" />
</Form.Buttons>
</Form>
);
};

View File

@@ -1,5 +1,6 @@
import { useCallback, useState } from 'react';
import { Props } from './schema';
import { Button, Form } from '@refocus/ui';
type EditorProps = {
value?: Props;
@@ -20,24 +21,20 @@ const Edit: React.FC<EditorProps> = ({ value, save }) => {
}, [owner, repo, pr, save]);
return (
<div>
<input
placeholder="Owner"
value={owner}
onChange={(e) => setOwner(e.target.value)}
/>
<input
placeholder="Repo"
value={repo}
onChange={(e) => setRepo(e.target.value)}
/>
<input
placeholder="PR"
value={pr}
onChange={(e) => setPr(e.target.value)}
/>
<button onClick={handleSave}>Save</button>
</div>
<Form>
<Form.Field label="Owner">
<Form.Input value={owner} onChange={(e) => setOwner(e.target.value)} />
</Form.Field>
<Form.Field label="Repo">
<Form.Input value={repo} onChange={(e) => setRepo(e.target.value)} />
</Form.Field>
<Form.Field label="PR">
<Form.Input value={pr} onChange={(e) => setPr(e.target.value)} />
</Form.Field>
<Form.Buttons>
<Button onClick={handleSave} title="Save" />
</Form.Buttons>
</Form>
);
};

View File

@@ -1,5 +1,6 @@
import { useCallback, useState } from 'react';
import { Props } from './schema';
import { Button, Form } from '@refocus/ui';
type EditorProps = {
value?: Props;
@@ -20,24 +21,20 @@ const Edit: React.FC<EditorProps> = ({ value, save }) => {
}, [owner, repo, id, save]);
return (
<div>
<input
placeholder="Owner"
value={owner}
onChange={(e) => setOwner(e.target.value)}
/>
<input
placeholder="Repo"
value={repo}
onChange={(e) => setRepo(e.target.value)}
/>
<input
placeholder="PR"
value={id}
onChange={(e) => setId(e.target.value)}
/>
<button onClick={handleSave}>Save</button>
</div>
<Form>
<Form.Field label="Owner">
<Form.Input value={owner} onChange={(e) => setOwner(e.target.value)} />
</Form.Field>
<Form.Field label="Repo">
<Form.Input value={repo} onChange={(e) => setRepo(e.target.value)} />
</Form.Field>
<Form.Field label="PR">
<Form.Input value={id} onChange={(e) => setId(e.target.value)} />
</Form.Field>
<Form.Buttons>
<Button onClick={handleSave} title="Save" />
</Form.Buttons>
</Form>
);
};

View File

@@ -1,5 +1,6 @@
import { useCallback, useState } from 'react';
import { Props } from './schema';
import { Button, Form } from '@refocus/ui';
type EditorProps = {
value?: Props;
@@ -18,19 +19,17 @@ const Edit: React.FC<EditorProps> = ({ value, save }) => {
}, [owner, repo, save]);
return (
<div>
<input
placeholder="Owner"
value={owner}
onChange={(e) => setOwner(e.target.value)}
/>
<input
placeholder="Repo"
value={repo}
onChange={(e) => setRepo(e.target.value)}
/>
<button onClick={handleSave}>Save</button>
</div>
<Form>
<Form.Field label="Owner">
<Form.Input value={owner} onChange={(e) => setOwner(e.target.value)} />
</Form.Field>
<Form.Field label="Repo">
<Form.Input value={repo} onChange={(e) => setRepo(e.target.value)} />
</Form.Field>
<Form.Buttons>
<Button onClick={handleSave} title="Save" />
</Form.Buttons>
</Form>
);
};

View File

@@ -22,7 +22,6 @@ const WidgetView = withGithub<Props>(({ owner, repo }) => {
setName(`${params.owner}/${params.repo} workflow runs`);
return response.data.workflow_runs.slice(0, 5);
});
console.log(data);
useAutoUpdate(
{

View File

@@ -1,5 +1,5 @@
import { Widget } from '@refocus/sdk';
import linearMyIssuesWidget from './my-issues/index.widget';
import linearMyIssuesWidget from './my-issues/index';
import linearIssue from './issue';
import linearIssueWithComments from './issue-with-comments';

View File

@@ -0,0 +1,31 @@
import { useCallback, useState } from 'react';
import { Props } from './schema';
import { Button, Form } from '@refocus/ui';
type EditorProps = {
value?: Props;
save: (data: Props) => void;
};
const Edit: React.FC<EditorProps> = ({ value, save }) => {
const [id, setId] = useState(value?.id || '');
const handleSave = useCallback(() => {
save({
id,
});
}, [id, save]);
return (
<Form>
<Form.Field label="Id">
<Form.Input value={id} onChange={(e) => setId(e.target.value)} />
</Form.Field>
<Form.Buttons>
<Button onClick={handleSave} title="Save" />
</Form.Buttons>
</Form>
);
};
export { Edit };

View File

@@ -2,6 +2,7 @@ import { Widget } from '@refocus/sdk';
import { SiLinear } from 'react-icons/si';
import { schema } from './schema';
import { WidgetView } from './view';
import { Edit } from './edit';
// https://linear.app/zeronorth/issue/VOY-93/save-a-new-cp-definition
@@ -23,6 +24,7 @@ const widget: Widget<typeof schema> = {
},
schema,
component: WidgetView,
edit: Edit,
};
export default widget;

View File

@@ -0,0 +1,31 @@
import { useCallback, useState } from 'react';
import { Props } from './schema';
import { Button, Form } from '@refocus/ui';
type EditorProps = {
value?: Props;
save: (data: Props) => void;
};
const Edit: React.FC<EditorProps> = ({ value, save }) => {
const [id, setId] = useState(value?.id || '');
const handleSave = useCallback(() => {
save({
id,
});
}, [id, save]);
return (
<Form>
<Form.Field label="Id">
<Form.Input value={id} onChange={(e) => setId(e.target.value)} />
</Form.Field>
<Form.Buttons>
<Button onClick={handleSave} title="Save" />
</Form.Buttons>
</Form>
);
};
export { Edit };

View File

@@ -2,6 +2,7 @@ import { Widget } from '@refocus/sdk';
import { SiLinear } from 'react-icons/si';
import { schema } from './schema';
import { WidgetView } from './view';
import { Edit } from './edit';
// https://linear.app/zeronorth/issue/VOY-93/save-a-new-cp-definition
@@ -23,6 +24,7 @@ const widget: Widget<typeof schema> = {
},
schema,
component: WidgetView,
edit: Edit,
};
export default widget;

View File

@@ -0,0 +1,22 @@
import { useCallback } from 'react';
import { Button, Form } from '@refocus/ui';
type EditorProps = {
save: (data: {}) => void;
};
const Edit: React.FC<EditorProps> = ({ save }) => {
const handleSave = useCallback(() => {
save({});
}, [save]);
return (
<Form>
<Form.Buttons>
<Button onClick={handleSave} title="Save" />
</Form.Buttons>
</Form>
);
};
export { Edit };

View File

@@ -1,33 +1,18 @@
import { useLinearQuery, withLinear } from '@refocus/sdk';
import { Panel, Linear } from '@refocus/ui';
import { useEffect } from 'react';
import { Type } from '@sinclair/typebox';
import { Widget } from '@refocus/sdk';
import { SiLinear } from 'react-icons/si';
import { View } from './view';
import { Edit } from './edit';
const LinearMyIssues = withLinear(() => {
const issues = useLinearQuery(async (client) => {
const me = await client.viewer;
const issues = await me.assignedIssues({
filter: {
completedAt: {
null: true,
},
},
});
return issues.nodes;
});
const schema = Type.Object({});
useEffect(() => {
issues.fetch();
}, []);
const linearMyIssuesWidget: Widget<typeof schema> = {
name: 'Linear My Issues',
id: 'linear.my-issues',
icon: <SiLinear />,
schema,
component: View,
edit: Edit,
};
return (
<Panel title="My issue">
<ul>
{issues.data?.map((issue) => (
<Linear.Issue key={issue.id} issue={issue} />
))}
</ul>
</Panel>
);
}, Linear.NotLoggedIn);
export { LinearMyIssues };
export default linearMyIssuesWidget;

View File

@@ -1,14 +0,0 @@
import { Type } from '@sinclair/typebox';
import { LinearMyIssues } from '.';
import { Widget } from '@refocus/sdk';
const schema = Type.Object({});
const linearMyIssuesWidget: Widget<typeof schema> = {
name: 'Linear My Issues',
id: 'linear.my-issues',
schema,
component: LinearMyIssues,
};
export default linearMyIssuesWidget;

View File

@@ -0,0 +1,46 @@
import {
useAutoUpdate,
useLinearQuery,
useName,
withLinear,
} from '@refocus/sdk';
import { Panel, Linear } from '@refocus/ui';
import { useEffect } from 'react';
const WidgetView = withLinear(() => {
const [, setName] = useName();
const { data, fetch } = useLinearQuery(async (client) => {
const me = await client.viewer;
const issues = await me.assignedIssues({
filter: {
completedAt: {
null: true,
},
},
});
return issues.nodes;
});
useEffect(() => {
setName('My issues');
}, [setName]);
useAutoUpdate(
{
action: fetch,
interval: 1000 * 60 * 5,
},
[],
);
return (
<Panel title="My issue">
<ul>
{data?.map((issue) => (
<Linear.Issue key={issue.id} issue={issue} />
))}
</ul>
</Panel>
);
}, Linear.NotLoggedIn);
export { WidgetView as View };

View File

@@ -0,0 +1,54 @@
import { Avatar, Popover, Typography, View } from '@refocus/ui';
import { useProfile } from '../../hooks';
import { styled } from 'styled-components';
import { useState } from 'react';
const Wrapper = styled(View)`
align-items: center;
gap: 0.5rem;
`;
const Trigger = styled(Popover.Trigger)`
display: inline-block;
color: ${({ theme }) => theme.colors.bg.highlight};
`;
const UserAvatar: React.FC<{ id: string }> = ({ id }) => {
const profile = useProfile(id);
const [open, setOpen] = useState(false);
return (
<>
<Popover open={open} onOpenChange={setOpen}>
<Trigger>
<Avatar
size="sm"
url={profile?.profile?.image_192}
name={profile?.real_name || profile?.name}
/>
</Trigger>
<Popover.Portal>
<>
<Popover.Overlay />
<Popover.Content>
<Wrapper $fr $gap="sm">
<Avatar
url={profile?.profile?.image_192}
name={profile?.real_name || profile?.name}
/>
<View $fc>
<Typography>{profile?.real_name || profile?.name}</Typography>
<Typography variant="tiny">
{profile?.profile?.status_text}
</Typography>
</View>
</Wrapper>
</Popover.Content>
</>
</Popover.Portal>
</Popover>
</>
);
};
export { UserAvatar };

View File

@@ -0,0 +1,52 @@
import { Avatar, Popover, Typography, View } from '@refocus/ui';
import { useProfile } from '../../hooks';
import { styled } from 'styled-components';
import { useState } from 'react';
const Wrapper = styled(View)`
align-items: center;
gap: 0.5rem;
`;
const Trigger = styled(Popover.Trigger)`
display: inline-block;
color: ${({ theme }) => theme.colors.bg.highlight};
`;
const User: React.FC<{ id: string }> = ({ id }) => {
const profile = useProfile(id);
const [open, setOpen] = useState(false);
return (
<>
<Popover open={open} onOpenChange={setOpen}>
<Trigger>
<Typography onClick={() => setOpen(true)}>
{profile?.real_name || profile?.name}
</Typography>
</Trigger>
<Popover.Portal>
<>
<Popover.Overlay />
<Popover.Content>
<Wrapper $fr $gap="sm">
<Avatar
url={profile?.profile?.image_192}
name={profile?.real_name || profile?.name}
/>
<View $fc>
<Typography>{profile?.real_name || profile?.name}</Typography>
<Typography variant="tiny">
{profile?.profile?.status_text}
</Typography>
</View>
</Wrapper>
</Popover.Content>
</>
</Popover.Portal>
</Popover>
</>
);
};
export { User };

View File

@@ -0,0 +1,69 @@
import { Fragment } from 'react';
import { User } from './elements/user';
import { Block, Renderable } from './types';
const unicodeToEmoji = (unicode: string) => {
const codePoints = unicode.split('-').map((u) => parseInt(u, 16));
return String.fromCodePoint(...codePoints);
};
const renderElement = (item: Renderable) => {
switch (item.type) {
case 'text':
return item.text.split('\n').flatMap((value, index, array) => {
const jsx = <Fragment key={index}>{value}</Fragment>;
if (index === array.length - 1) {
return [jsx];
}
return [jsx, <br key={'br' + index} />];
});
case 'link':
return (
<a href={item.url} target="_blank">
{item.text || item.url}
</a>
);
case 'user':
return <User id={item.user_id} />;
case 'emoji':
return unicodeToEmoji(item.unicode);
case 'rich_text_list':
return (
<ul>
{item.elements.map((elm, i) => (
<li key={i}>{renderElement(elm)}</li>
))}
</ul>
);
case 'rich_text':
case 'rich_text_section':
return (
<>
{item.elements.map((elm, i) => (
<Fragment key={i}>{renderElement(elm)}</Fragment>
))}
</>
);
default: {
console.log('Unknown element type', item);
return (
<>
Unknown element type: <pre>{JSON.stringify(item, null, 2)}</pre>
</>
);
}
}
};
const render = (blocks: Block[]) => {
return (
<>
{blocks.map((block, i) => (
<Fragment key={i}>{renderElement(block)}</Fragment>
))}
</>
);
};
export { render };

View File

@@ -0,0 +1,47 @@
type TextElement = {
type: 'text';
text: string;
};
type LinkElement = {
type: 'link';
url: string;
text: string;
};
type UserElement = {
type: 'user';
user_id: string;
};
type EmojiElement = {
type: 'emoji';
name: string;
unicode: string;
};
type RichTextElement = TextElement | LinkElement | UserElement | EmojiElement;
type RichTextSection = {
type: 'rich_text_section' | 'rich_text_list';
elements: RichTextElement[];
};
type RichText = {
type: 'rich_text';
elements: RichTextSection[];
};
type Renderable = RichTextElement | RichText | RichTextSection;
type Block = RichText;
export type {
TextElement,
LinkElement,
RichTextElement,
RichText,
UserElement,
EmojiElement,
Block,
Renderable,
};

View File

@@ -0,0 +1,31 @@
import { useCallback, useState } from 'react';
import { Props } from './schema';
import { Button, Form } from '@refocus/ui';
type EditorProps = {
value?: Props;
save: (data: Props) => void;
};
const Edit: React.FC<EditorProps> = ({ value, save }) => {
const [id, setId] = useState(value?.conversationId || '');
const handleSave = useCallback(() => {
save({
conversationId: id,
});
}, [id, save]);
return (
<Form>
<Form.Field label="Id">
<Form.Input value={id} onChange={(e) => setId(e.target.value)} />
</Form.Field>
<Form.Buttons>
<Button onClick={handleSave} title="Save" />
</Form.Buttons>
</Form>
);
};
export { Edit };

View File

@@ -2,6 +2,7 @@ import { Widget } from '@refocus/sdk';
import { schema } from './schema';
import { WidgetView } from './view';
import { SiSlack } from 'react-icons/si';
import { Edit } from './edit';
// https://refocus.slack.com/archives/D05C97E7GB1I
@@ -23,6 +24,7 @@ const widget: Widget<typeof schema> = {
},
schema,
component: WidgetView,
edit: Edit,
};
export default widget;

View File

@@ -1,11 +0,0 @@
import { Type, Static } from '@sinclair/typebox';
const schema = Type.Object({
text: Type.String(),
userId: Type.Optional(Type.String()),
});
type Props = Static<typeof schema>;
export type { Props };
export { schema };

View File

@@ -1,18 +1,94 @@
import { Chat } from '@refocus/ui';
import { Chat, Dialog, Typography, View } from '@refocus/ui';
import { useProfile } from '../../hooks';
import { Props } from './schema';
import { ConversationsHistoryResponse } from '@slack/web-api';
import { render } from '../../block/render';
import { useMemo } from 'react';
import { WidgetProvider, WidgetView } from '@refocus/sdk';
import { styled } from 'styled-components';
import { User } from '../../block/elements/user';
import { UserAvatar } from '../../block/elements/user-avatar';
const Message: React.FC<Props> = ({ text, userId }) => {
const profile = useProfile(userId);
type Message = Exclude<
ConversationsHistoryResponse['messages'],
undefined
>[0] & {
conversationId: string;
};
const LinkText = styled(Typography)`
color: ${({ theme }) => theme.colors.bg.highlight};
cursor: pointer;
`;
const Message: React.FC<Message> = ({
text,
blocks,
user,
reactions,
reply_count,
conversationId,
thread_ts,
ts,
}) => {
const profile = useProfile(user);
const threadData = useMemo(
() => ({ conversationId, ts: thread_ts }),
[conversationId, thread_ts],
);
const sendTime = new Date(parseInt(ts || '0', 10) * 1000);
return (
<Chat.Message
message={{
sender: {
name: profile?.real_name || profile?.name || 'Unknown',
avatar: profile?.profile?.image_192,
},
text,
timestamp: new Date(),
text: (
<>
{blocks ? render(blocks as any) : text}
{(reactions || reply_count) && (
<View $bg="highlight100" $br $p="sm">
{reactions && (
<View $gap="sm" $fr>
{reactions.map((reaction) => (
<Typography variant="tiny" key={reaction.name} $gap="sm">
{reaction.name}
<View $fr>
{reaction.users?.map((user) => (
<UserAvatar id={user} />
))}
</View>
</Typography>
))}
</View>
)}
{reply_count && (
<Dialog>
<Dialog.Trigger>
<LinkText variant="tiny" $gap="sm">
{reply_count} replies
</LinkText>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<WidgetProvider
id="slack.conversation"
data={threadData}
>
<WidgetView />
</WidgetProvider>
</Dialog.Content>
</Dialog.Portal>
</Dialog>
)}
</View>
)}
</>
),
timestamp: sendTime,
}}
/>
);

View File

@@ -7,14 +7,21 @@ import {
} from '@refocus/sdk';
import styled from 'styled-components';
import { Props } from './schema';
import { Message } from './message/view';
import { ConversationsHistoryResponse } from '@slack/web-api';
import { useState } from 'react';
import { Chat, List, Slack, Typography, View } from '@refocus/ui';
import { Chat, Slack, Typography, View } from '@refocus/ui';
import { User } from '../block/elements/user';
import { Message } from './message/view';
type PostMessageOptions = {
message: string;
};
type MessageType = Exclude<
ConversationsHistoryResponse['messages'],
undefined
>[0];
const MessageList = styled(View)`
transform: scaleY(-1);
flex: 1;
@@ -31,16 +38,24 @@ const Wrapper = styled(View)`
max-height: 100%;
overflow: hidden;
`;
const WidgetView = withSlack<Props>(({ conversationId }) => {
const WidgetView = withSlack<Props>(({ conversationId, ts }) => {
const [, setName] = useName();
const addNotification = useAddWidgetNotification();
const [message, setMessage] = useState('');
const { fetch, data } = useSlackQuery(async (client, props: Props) => {
if (props.ts) {
const response = await client.send('conversations.replies', {
channel: props.conversationId,
ts: props.ts,
});
return response.messages! as MessageType[];
} else {
const response = await client.send('conversations.history', {
channel: props.conversationId,
limit: 5,
});
return response.messages!;
return response.messages! as MessageType[];
}
});
const info = useSlackQuery(async (client, props: Props) => {
const response = await client.send('conversations.info', {
@@ -62,9 +77,10 @@ const WidgetView = withSlack<Props>(({ conversationId }) => {
useAutoUpdate(
{
action: async () => {
await info.fetch({ conversationId });
await info.fetch({ conversationId, ts });
return fetch({
conversationId,
ts,
});
},
interval: 1000 * 60,
@@ -84,19 +100,28 @@ const WidgetView = withSlack<Props>(({ conversationId }) => {
}
},
},
[conversationId],
[conversationId, ts],
);
return (
<Wrapper $p="sm" $fc $gap="sm">
<MessageList $gap="md" $fc>
{data?.map((message) => (
{data?.map((message) => {
if ('subtype' in message && message.subtype === 'channel_join') {
return (
<Typography key={message.ts}>
<User id={message.user!} /> joined the channel
</Typography>
);
}
return (
<Message
key={message.ts}
text={message.text || '[No text|'}
userId={message.user}
{...message}
conversationId={conversationId}
/>
))}
);
})}
</MessageList>
<Chat.Compose
value={message}