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:
@@ -5,16 +5,18 @@ type AvatarProps = {
|
|||||||
url?: string;
|
url?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
decal?: React.ReactNode;
|
decal?: React.ReactNode;
|
||||||
size?: 'sm' | 'md' | 'lg';
|
size?: keyof typeof sizes;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizes = {
|
const sizes = {
|
||||||
|
xs: 20,
|
||||||
sm: 28,
|
sm: 28,
|
||||||
md: 50,
|
md: 50,
|
||||||
lg: 75,
|
lg: 75,
|
||||||
};
|
};
|
||||||
|
|
||||||
const fontSizes = {
|
const fontSizes = {
|
||||||
|
xs: 8,
|
||||||
sm: 10,
|
sm: 10,
|
||||||
md: 24,
|
md: 24,
|
||||||
lg: 32,
|
lg: 32,
|
||||||
|
|||||||
29
packages/ui/src/base/form/index.stories.tsx
Normal file
29
packages/ui/src/base/form/index.stories.tsx
Normal 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 };
|
||||||
62
packages/ui/src/base/form/index.tsx
Normal file
62
packages/ui/src/base/form/index.tsx
Normal 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 };
|
||||||
@@ -10,3 +10,4 @@ export * from './button';
|
|||||||
export * from './masonry';
|
export * from './masonry';
|
||||||
export * from './code-editor';
|
export * from './code-editor';
|
||||||
export * from './popover';
|
export * from './popover';
|
||||||
|
export * from './form';
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { Issue as IssueType } from '@linear/sdk';
|
import type { Issue as IssueType } from '@linear/sdk';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { IssueSearchResult } from '@linear/sdk/dist/_generated_documents';
|
import { IssueSearchResult } from '@linear/sdk/dist/_generated_documents';
|
||||||
|
|
||||||
type IssueProps = {
|
type IssueProps = {
|
||||||
@@ -8,9 +7,7 @@ type IssueProps = {
|
|||||||
const Issue: React.FC<IssueProps> = ({ issue }) => {
|
const Issue: React.FC<IssueProps> = ({ issue }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Link to={`/linear/issue?id=${issue.id}`}>
|
<h3 className="text-lg font-bold">{issue.title}</h3>
|
||||||
<h3 className="text-lg font-bold">{issue.title}</h3>
|
|
||||||
</Link>
|
|
||||||
{issue.description}
|
{issue.description}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ const GlobalStyle = createGlobalStyle`
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
${styles.body}
|
${styles.body}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: ${({ theme }) => theme.colors.bg.highlight};
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const UIProvider: React.FC<UIProviderProps> = ({ children }) => {
|
const UIProvider: React.FC<UIProviderProps> = ({ children }) => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { Props } from './schema';
|
import { Props } from './schema';
|
||||||
|
import { Button, Form } from '@refocus/ui';
|
||||||
|
|
||||||
type EditorProps = {
|
type EditorProps = {
|
||||||
value?: Props;
|
value?: Props;
|
||||||
@@ -24,34 +25,32 @@ const Edit: React.FC<EditorProps> = ({ value, save }) => {
|
|||||||
}, [owner, repo, branch, path, highlight, save]);
|
}, [owner, repo, branch, path, highlight, save]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Form $fc>
|
||||||
<input
|
<Form.Field label="Owner">
|
||||||
placeholder="Owner"
|
<Form.Input value={owner} onChange={(e) => setOwner(e.target.value)} />
|
||||||
value={owner}
|
</Form.Field>
|
||||||
onChange={(e) => setOwner(e.target.value)}
|
<Form.Field label="Repo">
|
||||||
/>
|
<Form.Input value={repo} onChange={(e) => setRepo(e.target.value)} />
|
||||||
<input
|
</Form.Field>
|
||||||
placeholder="Repo"
|
<Form.Field label="Branch">
|
||||||
value={repo}
|
<Form.Input
|
||||||
onChange={(e) => setRepo(e.target.value)}
|
value={branch}
|
||||||
/>
|
onChange={(e) => setBranch(e.target.value)}
|
||||||
<input
|
/>
|
||||||
placeholder="Branch"
|
</Form.Field>
|
||||||
value={branch}
|
<Form.Field label="Path">
|
||||||
onChange={(e) => setBranch(e.target.value)}
|
<Form.Input value={path} onChange={(e) => setPath(e.target.value)} />
|
||||||
/>
|
</Form.Field>
|
||||||
<input
|
<Form.Field label="Highlights">
|
||||||
placeholder="Path"
|
<Form.Input
|
||||||
value={path}
|
value={highlight}
|
||||||
onChange={(e) => setPath(e.target.value)}
|
onChange={(e) => setHighlight(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<input
|
</Form.Field>
|
||||||
placeholder="Highlights"
|
<Form.Buttons>
|
||||||
value={highlight}
|
<Button onClick={handleSave} title="Save" />
|
||||||
onChange={(e) => setHighlight(e.target.value)}
|
</Form.Buttons>
|
||||||
/>
|
</Form>
|
||||||
<button onClick={handleSave}>Save</button>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { Props } from './schema';
|
import { Props } from './schema';
|
||||||
|
import { Button, Form } from '@refocus/ui';
|
||||||
|
|
||||||
type EditorProps = {
|
type EditorProps = {
|
||||||
value?: Props;
|
value?: Props;
|
||||||
@@ -20,24 +21,24 @@ const Edit: React.FC<EditorProps> = ({ value, save }) => {
|
|||||||
}, [owner, repo, pr, save]);
|
}, [owner, repo, pr, save]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Form>
|
||||||
<input
|
<Form.Field label="Owner">
|
||||||
placeholder="Owner"
|
<Form.Input value={owner} onChange={(e) => setOwner(e.target.value)} />
|
||||||
value={owner}
|
</Form.Field>
|
||||||
onChange={(e) => setOwner(e.target.value)}
|
<Form.Field label="Repo">
|
||||||
/>
|
<Form.Input value={repo} onChange={(e) => setRepo(e.target.value)} />
|
||||||
<input
|
</Form.Field>
|
||||||
placeholder="Repo"
|
<Form.Field label="PR">
|
||||||
value={repo}
|
<Form.Input
|
||||||
onChange={(e) => setRepo(e.target.value)}
|
placeholder="PR"
|
||||||
/>
|
value={pr}
|
||||||
<input
|
onChange={(e) => setPr(e.target.value)}
|
||||||
placeholder="PR"
|
/>
|
||||||
value={pr}
|
</Form.Field>
|
||||||
onChange={(e) => setPr(e.target.value)}
|
<Form.Buttons>
|
||||||
/>
|
<Button onClick={handleSave} title="Save" />
|
||||||
<button onClick={handleSave}>Save</button>
|
</Form.Buttons>
|
||||||
</div>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { Props } from './schema';
|
import { Props } from './schema';
|
||||||
|
import { Button, Form } from '@refocus/ui';
|
||||||
|
|
||||||
type EditorProps = {
|
type EditorProps = {
|
||||||
value?: Props;
|
value?: Props;
|
||||||
@@ -20,24 +21,20 @@ const Edit: React.FC<EditorProps> = ({ value, save }) => {
|
|||||||
}, [owner, repo, pr, save]);
|
}, [owner, repo, pr, save]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Form>
|
||||||
<input
|
<Form.Field label="Owner">
|
||||||
placeholder="Owner"
|
<Form.Input value={owner} onChange={(e) => setOwner(e.target.value)} />
|
||||||
value={owner}
|
</Form.Field>
|
||||||
onChange={(e) => setOwner(e.target.value)}
|
<Form.Field label="Repo">
|
||||||
/>
|
<Form.Input value={repo} onChange={(e) => setRepo(e.target.value)} />
|
||||||
<input
|
</Form.Field>
|
||||||
placeholder="Repo"
|
<Form.Field label="PR">
|
||||||
value={repo}
|
<Form.Input value={pr} onChange={(e) => setPr(e.target.value)} />
|
||||||
onChange={(e) => setRepo(e.target.value)}
|
</Form.Field>
|
||||||
/>
|
<Form.Buttons>
|
||||||
<input
|
<Button onClick={handleSave} title="Save" />
|
||||||
placeholder="PR"
|
</Form.Buttons>
|
||||||
value={pr}
|
</Form>
|
||||||
onChange={(e) => setPr(e.target.value)}
|
|
||||||
/>
|
|
||||||
<button onClick={handleSave}>Save</button>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { Props } from './schema';
|
import { Props } from './schema';
|
||||||
|
import { Button, Form } from '@refocus/ui';
|
||||||
|
|
||||||
type EditorProps = {
|
type EditorProps = {
|
||||||
value?: Props;
|
value?: Props;
|
||||||
@@ -20,24 +21,20 @@ const Edit: React.FC<EditorProps> = ({ value, save }) => {
|
|||||||
}, [owner, repo, id, save]);
|
}, [owner, repo, id, save]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Form>
|
||||||
<input
|
<Form.Field label="Owner">
|
||||||
placeholder="Owner"
|
<Form.Input value={owner} onChange={(e) => setOwner(e.target.value)} />
|
||||||
value={owner}
|
</Form.Field>
|
||||||
onChange={(e) => setOwner(e.target.value)}
|
<Form.Field label="Repo">
|
||||||
/>
|
<Form.Input value={repo} onChange={(e) => setRepo(e.target.value)} />
|
||||||
<input
|
</Form.Field>
|
||||||
placeholder="Repo"
|
<Form.Field label="PR">
|
||||||
value={repo}
|
<Form.Input value={id} onChange={(e) => setId(e.target.value)} />
|
||||||
onChange={(e) => setRepo(e.target.value)}
|
</Form.Field>
|
||||||
/>
|
<Form.Buttons>
|
||||||
<input
|
<Button onClick={handleSave} title="Save" />
|
||||||
placeholder="PR"
|
</Form.Buttons>
|
||||||
value={id}
|
</Form>
|
||||||
onChange={(e) => setId(e.target.value)}
|
|
||||||
/>
|
|
||||||
<button onClick={handleSave}>Save</button>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { Props } from './schema';
|
import { Props } from './schema';
|
||||||
|
import { Button, Form } from '@refocus/ui';
|
||||||
|
|
||||||
type EditorProps = {
|
type EditorProps = {
|
||||||
value?: Props;
|
value?: Props;
|
||||||
@@ -18,19 +19,17 @@ const Edit: React.FC<EditorProps> = ({ value, save }) => {
|
|||||||
}, [owner, repo, save]);
|
}, [owner, repo, save]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Form>
|
||||||
<input
|
<Form.Field label="Owner">
|
||||||
placeholder="Owner"
|
<Form.Input value={owner} onChange={(e) => setOwner(e.target.value)} />
|
||||||
value={owner}
|
</Form.Field>
|
||||||
onChange={(e) => setOwner(e.target.value)}
|
<Form.Field label="Repo">
|
||||||
/>
|
<Form.Input value={repo} onChange={(e) => setRepo(e.target.value)} />
|
||||||
<input
|
</Form.Field>
|
||||||
placeholder="Repo"
|
<Form.Buttons>
|
||||||
value={repo}
|
<Button onClick={handleSave} title="Save" />
|
||||||
onChange={(e) => setRepo(e.target.value)}
|
</Form.Buttons>
|
||||||
/>
|
</Form>
|
||||||
<button onClick={handleSave}>Save</button>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ const WidgetView = withGithub<Props>(({ owner, repo }) => {
|
|||||||
setName(`${params.owner}/${params.repo} workflow runs`);
|
setName(`${params.owner}/${params.repo} workflow runs`);
|
||||||
return response.data.workflow_runs.slice(0, 5);
|
return response.data.workflow_runs.slice(0, 5);
|
||||||
});
|
});
|
||||||
console.log(data);
|
|
||||||
|
|
||||||
useAutoUpdate(
|
useAutoUpdate(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Widget } from '@refocus/sdk';
|
import { Widget } from '@refocus/sdk';
|
||||||
import linearMyIssuesWidget from './my-issues/index.widget';
|
import linearMyIssuesWidget from './my-issues/index';
|
||||||
import linearIssue from './issue';
|
import linearIssue from './issue';
|
||||||
import linearIssueWithComments from './issue-with-comments';
|
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 { SiLinear } from 'react-icons/si';
|
||||||
import { schema } from './schema';
|
import { schema } from './schema';
|
||||||
import { WidgetView } from './view';
|
import { WidgetView } from './view';
|
||||||
|
import { Edit } from './edit';
|
||||||
|
|
||||||
// https://linear.app/zeronorth/issue/VOY-93/save-a-new-cp-definition
|
// https://linear.app/zeronorth/issue/VOY-93/save-a-new-cp-definition
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ const widget: Widget<typeof schema> = {
|
|||||||
},
|
},
|
||||||
schema,
|
schema,
|
||||||
component: WidgetView,
|
component: WidgetView,
|
||||||
|
edit: Edit,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default widget;
|
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 { SiLinear } from 'react-icons/si';
|
||||||
import { schema } from './schema';
|
import { schema } from './schema';
|
||||||
import { WidgetView } from './view';
|
import { WidgetView } from './view';
|
||||||
|
import { Edit } from './edit';
|
||||||
|
|
||||||
// https://linear.app/zeronorth/issue/VOY-93/save-a-new-cp-definition
|
// https://linear.app/zeronorth/issue/VOY-93/save-a-new-cp-definition
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ const widget: Widget<typeof schema> = {
|
|||||||
},
|
},
|
||||||
schema,
|
schema,
|
||||||
component: WidgetView,
|
component: WidgetView,
|
||||||
|
edit: Edit,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default widget;
|
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 { Type } from '@sinclair/typebox';
|
||||||
import { Panel, Linear } from '@refocus/ui';
|
import { Widget } from '@refocus/sdk';
|
||||||
import { useEffect } from 'react';
|
import { SiLinear } from 'react-icons/si';
|
||||||
|
import { View } from './view';
|
||||||
|
import { Edit } from './edit';
|
||||||
|
|
||||||
const LinearMyIssues = withLinear(() => {
|
const schema = Type.Object({});
|
||||||
const issues = useLinearQuery(async (client) => {
|
|
||||||
const me = await client.viewer;
|
|
||||||
const issues = await me.assignedIssues({
|
|
||||||
filter: {
|
|
||||||
completedAt: {
|
|
||||||
null: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return issues.nodes;
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
const linearMyIssuesWidget: Widget<typeof schema> = {
|
||||||
issues.fetch();
|
name: 'Linear My Issues',
|
||||||
}, []);
|
id: 'linear.my-issues',
|
||||||
|
icon: <SiLinear />,
|
||||||
|
schema,
|
||||||
|
component: View,
|
||||||
|
edit: Edit,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
export default linearMyIssuesWidget;
|
||||||
<Panel title="My issue">
|
|
||||||
<ul>
|
|
||||||
{issues.data?.map((issue) => (
|
|
||||||
<Linear.Issue key={issue.id} issue={issue} />
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</Panel>
|
|
||||||
);
|
|
||||||
}, Linear.NotLoggedIn);
|
|
||||||
|
|
||||||
export { LinearMyIssues };
|
|
||||||
|
|||||||
@@ -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 { schema } from './schema';
|
||||||
import { WidgetView } from './view';
|
import { WidgetView } from './view';
|
||||||
import { SiSlack } from 'react-icons/si';
|
import { SiSlack } from 'react-icons/si';
|
||||||
|
import { Edit } from './edit';
|
||||||
|
|
||||||
// https://refocus.slack.com/archives/D05C97E7GB1I
|
// https://refocus.slack.com/archives/D05C97E7GB1I
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ const widget: Widget<typeof schema> = {
|
|||||||
},
|
},
|
||||||
schema,
|
schema,
|
||||||
component: WidgetView,
|
component: WidgetView,
|
||||||
|
edit: Edit,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default widget;
|
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 { 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 }) => {
|
type Message = Exclude<
|
||||||
const profile = useProfile(userId);
|
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 (
|
return (
|
||||||
<Chat.Message
|
<Chat.Message
|
||||||
message={{
|
message={{
|
||||||
sender: {
|
sender: {
|
||||||
name: profile?.real_name || profile?.name || 'Unknown',
|
name: profile?.real_name || profile?.name || 'Unknown',
|
||||||
|
avatar: profile?.profile?.image_192,
|
||||||
},
|
},
|
||||||
text,
|
text: (
|
||||||
timestamp: new Date(),
|
<>
|
||||||
|
{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';
|
} from '@refocus/sdk';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Props } from './schema';
|
import { Props } from './schema';
|
||||||
import { Message } from './message/view';
|
import { ConversationsHistoryResponse } from '@slack/web-api';
|
||||||
import { useState } from 'react';
|
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 = {
|
type PostMessageOptions = {
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type MessageType = Exclude<
|
||||||
|
ConversationsHistoryResponse['messages'],
|
||||||
|
undefined
|
||||||
|
>[0];
|
||||||
|
|
||||||
const MessageList = styled(View)`
|
const MessageList = styled(View)`
|
||||||
transform: scaleY(-1);
|
transform: scaleY(-1);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -31,16 +38,24 @@ const Wrapper = styled(View)`
|
|||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
`;
|
`;
|
||||||
const WidgetView = withSlack<Props>(({ conversationId }) => {
|
const WidgetView = withSlack<Props>(({ conversationId, ts }) => {
|
||||||
const [, setName] = useName();
|
const [, setName] = useName();
|
||||||
const addNotification = useAddWidgetNotification();
|
const addNotification = useAddWidgetNotification();
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
const { fetch, data } = useSlackQuery(async (client, props: Props) => {
|
const { fetch, data } = useSlackQuery(async (client, props: Props) => {
|
||||||
const response = await client.send('conversations.history', {
|
if (props.ts) {
|
||||||
channel: props.conversationId,
|
const response = await client.send('conversations.replies', {
|
||||||
limit: 5,
|
channel: props.conversationId,
|
||||||
});
|
ts: props.ts,
|
||||||
return response.messages!;
|
});
|
||||||
|
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 info = useSlackQuery(async (client, props: Props) => {
|
||||||
const response = await client.send('conversations.info', {
|
const response = await client.send('conversations.info', {
|
||||||
@@ -62,9 +77,10 @@ const WidgetView = withSlack<Props>(({ conversationId }) => {
|
|||||||
useAutoUpdate(
|
useAutoUpdate(
|
||||||
{
|
{
|
||||||
action: async () => {
|
action: async () => {
|
||||||
await info.fetch({ conversationId });
|
await info.fetch({ conversationId, ts });
|
||||||
return fetch({
|
return fetch({
|
||||||
conversationId,
|
conversationId,
|
||||||
|
ts,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
interval: 1000 * 60,
|
interval: 1000 * 60,
|
||||||
@@ -84,19 +100,28 @@ const WidgetView = withSlack<Props>(({ conversationId }) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[conversationId],
|
[conversationId, ts],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper $p="sm" $fc $gap="sm">
|
<Wrapper $p="sm" $fc $gap="sm">
|
||||||
<MessageList $gap="md" $fc>
|
<MessageList $gap="md" $fc>
|
||||||
{data?.map((message) => (
|
{data?.map((message) => {
|
||||||
<Message
|
if ('subtype' in message && message.subtype === 'channel_join') {
|
||||||
key={message.ts}
|
return (
|
||||||
text={message.text || '[No text|'}
|
<Typography key={message.ts}>
|
||||||
userId={message.user}
|
<User id={message.user!} /> joined the channel
|
||||||
/>
|
</Typography>
|
||||||
))}
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Message
|
||||||
|
key={message.ts}
|
||||||
|
{...message}
|
||||||
|
conversationId={conversationId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</MessageList>
|
</MessageList>
|
||||||
<Chat.Compose
|
<Chat.Compose
|
||||||
value={message}
|
value={message}
|
||||||
|
|||||||
Reference in New Issue
Block a user