mirror of
https://github.com/morten-olsen/refocus.dev.git
synced 2026-02-08 00:46:25 +01:00
fix: improved Slack widget
This commit is contained in:
@@ -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"
|
||||
value={branch}
|
||||
onChange={(e) => setBranch(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
placeholder="Path"
|
||||
value={path}
|
||||
onChange={(e) => setPath(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
placeholder="Highlights"
|
||||
value={highlight}
|
||||
onChange={(e) => setHighlight(e.target.value)}
|
||||
/>
|
||||
<button onClick={handleSave}>Save</button>
|
||||
</div>
|
||||
<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)}
|
||||
/>
|
||||
</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)}
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Buttons>
|
||||
<Button onClick={handleSave} title="Save" />
|
||||
</Form.Buttons>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
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
|
||||
placeholder="PR"
|
||||
value={pr}
|
||||
onChange={(e) => setPr(e.target.value)}
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Buttons>
|
||||
<Button onClick={handleSave} title="Save" />
|
||||
</Form.Buttons>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
31
packages/widgets/src/linear/issue-with-comments/edit.tsx
Normal file
31
packages/widgets/src/linear/issue-with-comments/edit.tsx
Normal 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 };
|
||||
@@ -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;
|
||||
|
||||
31
packages/widgets/src/linear/issue/edit.tsx
Normal file
31
packages/widgets/src/linear/issue/edit.tsx
Normal 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 };
|
||||
@@ -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;
|
||||
|
||||
22
packages/widgets/src/linear/my-issues/edit.tsx
Normal file
22
packages/widgets/src/linear/my-issues/edit.tsx
Normal 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 };
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
46
packages/widgets/src/linear/my-issues/view.tsx
Normal file
46
packages/widgets/src/linear/my-issues/view.tsx
Normal 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 };
|
||||
54
packages/widgets/src/slack/block/elements/user-avatar.tsx
Normal file
54
packages/widgets/src/slack/block/elements/user-avatar.tsx
Normal 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 };
|
||||
52
packages/widgets/src/slack/block/elements/user.tsx
Normal file
52
packages/widgets/src/slack/block/elements/user.tsx
Normal 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 };
|
||||
69
packages/widgets/src/slack/block/render.tsx
Normal file
69
packages/widgets/src/slack/block/render.tsx
Normal 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 };
|
||||
47
packages/widgets/src/slack/block/types.ts
Normal file
47
packages/widgets/src/slack/block/types.ts
Normal 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,
|
||||
};
|
||||
31
packages/widgets/src/slack/conversation/edit.tsx
Normal file
31
packages/widgets/src/slack/conversation/edit.tsx
Normal 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 };
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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) => {
|
||||
const response = await client.send('conversations.history', {
|
||||
channel: props.conversationId,
|
||||
limit: 5,
|
||||
});
|
||||
return response.messages!;
|
||||
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! 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) => (
|
||||
<Message
|
||||
key={message.ts}
|
||||
text={message.text || '[No text|'}
|
||||
userId={message.user}
|
||||
/>
|
||||
))}
|
||||
{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}
|
||||
{...message}
|
||||
conversationId={conversationId}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</MessageList>
|
||||
<Chat.Compose
|
||||
value={message}
|
||||
|
||||
Reference in New Issue
Block a user