fix: improved UI

This commit is contained in:
Morten Olsen
2023-06-19 09:25:03 +02:00
parent 11299a31fa
commit 85b88822b4
28 changed files with 618 additions and 124 deletions

View File

@@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef } from 'react';
import { useUpdateEffect } from '../widgets';
type AutoUpdateOptions<TReturn> = {
interval: number;
@@ -36,6 +37,10 @@ const useAutoUpdate = <T>(
};
}, [interval, actionWithCallback, callbackWithCallback]);
useUpdateEffect(async () => {
await update();
}, [update]);
return update;
};

View File

@@ -1,4 +1,4 @@
import { useCallback, useContext, useMemo, useState } from 'react';
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { WidgetsContext } from './context';
import { WidgetContext } from './widget-context';
@@ -111,9 +111,58 @@ const useWidgetId = () => {
return context.id;
};
const useName = () => {
const context = useContext(WidgetContext);
if (!context) {
throw new Error('useName must be used within a WidgetProvider');
}
return [context.name, context.setName] as const;
};
const useHasUpdate = () => {
const context = useContext(WidgetContext);
if (!context) {
throw new Error('useHasUpdate must be used within a WidgetProvider');
}
return context.hasUpdater;
};
const useUpdateEffect = (fn: () => Promise<void>, deps: any[]) => {
const context = useContext(WidgetContext);
if (!context) {
throw new Error('useUpdateEffect must be used within a WidgetProvider');
}
useEffect(() => {
const unsubscribe = context.addUpdater(() => fn());
return () => {
unsubscribe();
};
}, [context.addUpdater, ...deps]);
};
const useReloadWidget = () => {
const context = useContext(WidgetContext);
if (!context) {
throw new Error('useUpdateWidget must be used within a WidgetProvider');
}
return context.updateWidget;
};
const useIsUpdating = () => {
const context = useContext(WidgetContext);
if (!context) {
throw new Error('useIsUpdating must be used within a WidgetProvider');
}
return context.updating;
};
export {
useWidget,
useWidgets,
useName,
useUpdateEffect,
useHasUpdate,
useReloadWidget,
useGetWidgetsFromUrl,
useWidgetNotifications,
useDismissWidgetNotification,
@@ -121,4 +170,5 @@ export {
useWidgetData,
useSetWidgetData,
useWidgetId,
useIsUpdating,
};

View File

@@ -10,6 +10,11 @@ export {
useWidgetId,
useWidgetData,
useSetWidgetData,
useName,
useUpdateEffect,
useReloadWidget,
useHasUpdate,
useIsUpdating,
} from './hooks';
export { WidgetProvider } from './widget-context';
export { WidgetView } from './view';

View File

@@ -1,4 +1,4 @@
import { createContext, useCallback, useMemo, useRef } from 'react';
import { createContext, useCallback, useMemo, useRef, useState } from 'react';
import {
Notification as BaseNotification,
useNotificationAdd,
@@ -14,8 +14,16 @@ type WidgetContextValue = {
dismissNotification: (id: string) => void;
notifications: Notification[];
setData?: (data: any) => void;
addUpdater: (updater: Updater) => () => void;
updating: boolean;
hasUpdater: boolean;
name: string;
setName: (name: string) => void;
updateWidget: () => Promise<void>;
};
type Updater = () => Promise<void> | void;
type WidgetProviderProps = {
id: string;
data?: any;
@@ -32,6 +40,9 @@ const WidgetProvider = ({
children,
}: WidgetProviderProps) => {
const ref = useRef(Symbol('WidgetRender'));
const [updating, setUpdating] = useState(false);
const [name, setName] = useState('');
const [updaters, setUpdaters] = useState<Updater[]>([]);
const globalNotifications = useNotifications();
const addGlobalNotification = useNotificationAdd();
const dissmissGlobalNotification = useNotificationDismiss();
@@ -46,6 +57,16 @@ const WidgetProvider = ({
[addGlobalNotification],
);
const addUpdater = useCallback(
(updater: Updater) => {
setUpdaters((prev) => [...prev, updater]);
return () => {
setUpdaters((prev) => prev.filter((u) => u !== updater));
};
},
[setUpdaters],
);
const dismissNotification = useCallback(
(dismissId: string) => {
dissmissGlobalNotification(dismissId);
@@ -53,6 +74,18 @@ const WidgetProvider = ({
[dissmissGlobalNotification],
);
const updateWidget = useCallback(async () => {
setUpdating(true);
for (const updater of updaters) {
try {
await updater();
} catch (e) {
console.error(e);
}
}
setUpdating(false);
}, [updaters]);
const value = useMemo(
() => ({
addNotification,
@@ -61,8 +94,27 @@ const WidgetProvider = ({
id,
data,
setData,
name,
setName,
addUpdater,
updateWidget,
updating,
hasUpdater: updaters.length > 0,
}),
[addNotification, notifications, id, data, setData, dismissNotification],
[
addNotification,
notifications,
id,
data,
setData,
updating,
dismissNotification,
name,
setName,
addUpdater,
updateWidget,
updaters.length,
],
);
return (

View File

@@ -11,6 +11,7 @@ const Overlay = styled(DialogPrimitives.Overlay)`
position: fixed;
inset: 0;
backdrop-filter: blur(5px);
background-color: rgba(0, 0, 0, 0.5);
`;
const Portal = styled(DialogPrimitives.Portal)``;
@@ -30,6 +31,7 @@ const Content = styled(DialogPrimitives.Content)`
max-width: 450px;
max-height: 85vh;
padding: 25px;
box-shadow: 0 0 0 1px ${({ theme }) => theme.colors.bg.highlight};
`;
const Title = styled(DialogPrimitives.Title)`

View File

@@ -1,4 +1,5 @@
import * as DropdownMenuPrimitives from '@radix-ui/react-dropdown-menu';
import { motion } from 'framer-motion';
import { styled, css } from 'styled-components';
const RightSlot = styled.div`
@@ -62,6 +63,17 @@ const Trigger = styled(DropdownMenuPrimitives.Trigger)`
const Portal = styled(DropdownMenuPrimitives.Portal)``;
const OverlayComponent = styled(motion.div)`
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(8px);
`;
const Overlay: React.FC = () => (
<OverlayComponent initial={{ opacity: 0 }} animate={{ opacity: 1 }} />
);
const Item = styled(DropdownMenuPrimitives.Item)`
${item}
`;
@@ -147,6 +159,7 @@ const DropdownMenu = Object.assign(Root, {
Item,
Sub,
SubTrigger,
Overlay,
SubContent,
Separator,
CheckboxItem,

View File

@@ -1,20 +0,0 @@
import type { StoryFn, Meta } from '@storybook/react';
import { Action } from './index';
import action from './data.json';
const meta = {
title: 'GitHub/Action',
component: Action,
} satisfies Meta<typeof Action>;
type Story = StoryFn<typeof Action>;
const Normal: Story = {
args: {
action: action,
onPress: () => {},
},
} as any;
export { Normal };
export default meta;

View File

@@ -1,56 +0,0 @@
import { useCallback } from 'react';
import { GithubTypes } from '@refocus/sdk';
import {
IoCheckmarkDoneCircleOutline,
IoCloseCircleOutline,
} from 'react-icons/io5';
import { RxTimer } from 'react-icons/rx';
import { FiPlayCircle } from 'react-icons/fi';
import { GoQuestion } from 'react-icons/go';
import { Avatar, Card, View } from '../../base';
import { Typography } from '../../typography';
type ActionProps = {
action: GithubTypes.WorkflowRun;
onPress?: (action: GithubTypes.WorkflowRun) => void;
};
const getIcon = (status: string | null) => {
switch (status) {
case 'success':
return <IoCheckmarkDoneCircleOutline size={48} color="green" />;
case 'failure':
return <IoCloseCircleOutline size={48} color="red" />;
case 'in_progress':
return <FiPlayCircle size={48} />;
case 'queued':
return <RxTimer size={48} />;
default:
return <GoQuestion size={48} />;
}
};
const Action: React.FC<ActionProps> = ({ action, onPress }) => {
const onPressHandler = useCallback(() => {
onPress?.(action);
}, [action, onPress]);
return (
<Card $fr $items="center" $p="md" $gap="md" onClick={onPressHandler}>
<Avatar
url={action.actor?.avatar_url}
name={action.actor?.name || action.actor?.login}
decal={`#${action.run_attempt}`}
/>
<View $fc $f={1}>
<Typography variant="overline">
{action.name} - {action.actor?.name || action.actor?.login}
</Typography>
<Typography variant="title">{action.display_title}</Typography>
<Typography variant="subtitle">{action.status}</Typography>
</View>
<View>{getIcon(action.status)}</View>
</Card>
);
};
export { Action };

View File

@@ -3,3 +3,4 @@ export * from './profile';
export * from './pull-request';
export * from './login';
export * from './not-logged-in';
export * from './workflow-run';

View File

@@ -0,0 +1,20 @@
import type { StoryFn, Meta } from '@storybook/react';
import { WorkflowRun } from './index';
import workflowRun from './data.json';
const meta = {
title: 'GitHub/Workflow Run',
component: WorkflowRun,
} satisfies Meta<typeof WorkflowRun>;
type Story = StoryFn<typeof WorkflowRun>;
const Normal: Story = {
args: {
workflowRun,
onPress: () => {},
},
} as any;
export { Normal };
export default meta;

View File

@@ -0,0 +1,72 @@
import { useCallback } from 'react';
import { GithubTypes } from '@refocus/sdk';
import {
IoCheckmarkDoneCircleOutline,
IoCloseCircleOutline,
} from 'react-icons/io5';
import { RxTimer } from 'react-icons/rx';
import { FiPlayCircle } from 'react-icons/fi';
import { GoQuestion } from 'react-icons/go';
import { Avatar, Card, View } from '../../base';
import { Typography } from '../../typography';
type WorkflowRunProps = {
workflowRun: GithubTypes.WorkflowRun;
onPress?: (action: GithubTypes.WorkflowRun) => void;
};
const getIcon = (workflowRun: GithubTypes.WorkflowRun) => {
const { status, conclusion } = workflowRun;
if (status === 'completed' && conclusion === 'success') {
return <IoCheckmarkDoneCircleOutline size={48} color="green" />;
} else if (status === 'completed' && conclusion === 'failure') {
return <IoCloseCircleOutline size={48} color="red" />;
} else if (status === 'completed' && conclusion === 'cancelled') {
return <IoCloseCircleOutline size={48} color="yellow" />;
} else if (status === 'completed' && conclusion === 'skipped') {
return <IoCloseCircleOutline size={48} color="gray" />;
} else if (status === 'completed' && conclusion === 'timed_out') {
return <IoCloseCircleOutline size={48} color="gray" />;
} else if (status === 'completed' && conclusion === 'action_required') {
return <IoCloseCircleOutline size={48} color="gray" />;
} else if (status === 'in_progress') {
return <FiPlayCircle size={48} />;
} else if (status === 'queued') {
return <RxTimer size={48} />;
} else {
return <GoQuestion size={48} />;
}
};
const WorkflowRun: React.FC<WorkflowRunProps> = ({ workflowRun, onPress }) => {
const onPressHandler = useCallback(() => {
onPress?.(workflowRun);
}, [workflowRun, onPress]);
return (
<Card
$fr
$items="center"
$p="md"
$gap="md"
onClick={onPressHandler}
$m="sm"
>
<Avatar
url={workflowRun.actor?.avatar_url}
name={workflowRun.actor?.name || workflowRun.actor?.login}
decal={`#${workflowRun.run_attempt}`}
/>
<View $fc $f={1}>
<Typography variant="overline">
{workflowRun.repository.full_name} -{' '}
{workflowRun.actor?.name || workflowRun.actor?.login}
</Typography>
<Typography variant="title">{workflowRun.display_title}</Typography>
<Typography variant="subtitle">{workflowRun.name}</Typography>
</View>
<View>{getIcon(workflowRun)}</View>
</Card>
);
};
export { WorkflowRun };

View File

@@ -16,10 +16,7 @@ type BoardProps = {
};
const ItemWrapper = styled(View)`
overflow-y: auto;
max-height: 500px;
max-width: 100%;
box-shadow: 0 0 4px 0px ${({ theme }) => theme.colors.bg.highlight};
border-radius: ${({ theme }) => theme.radii.md}px;
`;

View File

@@ -3,15 +3,20 @@ import {
WidgetEditor,
WidgetProvider,
WidgetView,
useHasUpdate,
useName,
useReloadWidget,
useWidget,
useIsUpdating,
} from '@refocus/sdk';
import { MdKeyboardArrowUp } from 'react-icons/md';
import { motion } from 'framer-motion';
import { VscTrash } from 'react-icons/vsc';
import { CgMoreO } from 'react-icons/cg';
import { CgMoreO, CgSync } from 'react-icons/cg';
import { Dialog, View } from '../../base';
import { DropdownMenu } from '../../base';
import { useCallback, useMemo, useState } from 'react';
import { Typography } from '../../typography';
type WidgetProps = {
id: string;
@@ -23,18 +28,55 @@ type WidgetProps = {
const Wrapper = styled(View)`
background: ${({ theme }) => theme.colors.bg.base};
box-shadow: 0 0 0 1px ${({ theme }) => theme.colors.bg.highlight};
margin: 5px;
border-radius: 5px;
`;
const WidgetWrapper = styled(View)`
flex-grow: 0;
overflow: hidden;
flex: 1;
max-height: 500px;
overflow-y: auto;
`;
const Spacer = styled(View)`
flex: 1;
`;
const SingleLine = styled(Typography)`
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
const Title: React.FC = () => {
const [name] = useName();
return <SingleLine variant="overline">{name}</SingleLine>;
};
const Update: React.FC = () => {
const hasUpdate = useHasUpdate();
const reload = useReloadWidget();
const updating = useIsUpdating();
if (!hasUpdate) {
return null;
}
return (
<motion.div
animate={{ rotate: updating ? 360 : 0 }}
transition={{ duration: 1, loop: Infinity }}
>
<View $p="sm" onClick={reload}>
<CgSync />
</View>
</motion.div>
);
};
const Widget: React.FC<WidgetProps> = ({
id,
data,
@@ -59,7 +101,7 @@ const Widget: React.FC<WidgetProps> = ({
);
return (
<WidgetProvider id={id} data={data} setData={setData}>
<View $fr>
<View $fr $items="center">
<motion.div animate={{ rotate: open ? 180 : 0 }}>
<View
$items="center"
@@ -71,7 +113,9 @@ const Widget: React.FC<WidgetProps> = ({
<MdKeyboardArrowUp size={22} />
</View>
</motion.div>
<Title />
<Spacer />
<Update />
{hasMenu && (
<DropdownMenu>
<DropdownMenu.Trigger>
@@ -80,6 +124,8 @@ const Widget: React.FC<WidgetProps> = ({
</View>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<>
<DropdownMenu.Overlay />
<DropdownMenu.Content alignOffset={50}>
{!!onRemove && (
<DropdownMenu.Item onClick={onRemove}>
@@ -96,11 +142,14 @@ const Widget: React.FC<WidgetProps> = ({
)}
<DropdownMenu.Arrow />
</DropdownMenu.Content>
</>
</DropdownMenu.Portal>
</DropdownMenu>
)}
</View>
<motion.div animate={{ height: open ? 'auto' : 0 }}>
<motion.div
animate={{ height: open ? 'auto' : 0, opacity: open ? 1 : 0 }}
>
<Wrapper className={className} $fr>
<WidgetWrapper $f={1}>
<WidgetView />
@@ -108,6 +157,7 @@ const Widget: React.FC<WidgetProps> = ({
</Wrapper>
</motion.div>
<Dialog open={showEdit} onOpenChange={setShowEdit}>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title>Edit Widget</Dialog.Title>
@@ -115,6 +165,7 @@ const Widget: React.FC<WidgetProps> = ({
<WidgetEditor onSave={onSave} />
</Dialog.Description>
</Dialog.Content>
</Dialog.Portal>
</Dialog>
</WidgetProvider>
);

View File

@@ -6,7 +6,6 @@ type UIProviderProps = {
};
// @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;600;700&display=swap');
const GlobalStyle = createGlobalStyle`
* {
box-sizing: border-box;
overflow-wrap: break-word;
@@ -24,6 +23,7 @@ const GlobalStyle = createGlobalStyle`
body {
background-color: ${({ theme }) => theme.colors.bg.base};
background-image: linear-gradient(to right, rgb(38, 39, 54), rgb(25, 26, 35));
color: ${({ theme }) => theme.colors.text.base};
margin: 0;
padding: 0;

View File

@@ -2,11 +2,15 @@ import { Widget } from '@refocus/sdk';
import githubProfileWidget from './profile/index.widget';
import pullRequest from './pull-request/index.widget';
import pullRequstComments from './pull-request-comments/index.widget';
import workflowRun from './workflow-run/index.widget';
import workflowRuns from './workflow-runs/index.widget';
const github = [
githubProfileWidget,
pullRequest,
pullRequstComments,
workflowRun,
workflowRuns,
] satisfies Widget<any>[];
export { github };

View File

@@ -3,6 +3,7 @@ import {
useAutoUpdate,
useGithubQuery,
withGithub,
useName,
} from '@refocus/sdk';
import { Chat, Github, List } from '@refocus/ui';
import { Props } from './schema';
@@ -16,12 +17,14 @@ type QueryData = {
const View = withGithub<Props>(({ owner, repo, pr }) => {
const addNotification = useAddWidgetNotification();
const [, setName] = useName();
const { data, fetch } = useGithubQuery(async (client, params: QueryData) => {
const response = await client.rest.pulls.listReviewComments({
owner: params.owner,
repo: params.repo,
pull_number: params.pr,
});
setName(`${params.owner}/${params.repo} #${params.pr}`);
return response.data.slice(0, 5);
});

View File

@@ -1,4 +1,9 @@
import { useAutoUpdate, useGithubQuery, withGithub } from '@refocus/sdk';
import {
useAutoUpdate,
useGithubQuery,
useName,
withGithub,
} from '@refocus/sdk';
import { Props } from './schema';
import { Github } from '@refocus/ui';
@@ -9,12 +14,14 @@ type QueryData = {
};
const View = withGithub<Props>(({ owner, repo, pr }) => {
const [, setName] = useName();
const { data, fetch } = useGithubQuery(async (client, params: QueryData) => {
const response = await client.rest.pulls.get({
owner: params.owner,
repo: params.repo,
pull_number: params.pr,
});
setName(`${params.owner}/${params.repo} #${params.pr}`);
return response.data;
});

View File

@@ -0,0 +1,44 @@
import { useCallback, useState } from 'react';
import { Props } from './schema';
type EditorProps = {
value?: Props;
save: (data: Props) => void;
};
const Edit: React.FC<EditorProps> = ({ value, save }) => {
const [owner, setOwner] = useState(value?.owner || '');
const [repo, setRepo] = useState(value?.repo || '');
const [id, setId] = useState(value?.id || '');
const handleSave = useCallback(() => {
save({
owner,
repo,
id: typeof id === 'string' ? parseInt(id, 10) : id,
});
}, [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>
);
};
export { Edit };

View File

@@ -0,0 +1,28 @@
import { Widget } from '@refocus/sdk';
import { SiGithub } from 'react-icons/si';
import { schema } from './schema';
import { Edit } from './edit';
import { View } from './view';
const widget: Widget<typeof schema> = {
name: 'Github Workflow Run',
description: 'Display information about a specific workflow run.',
icon: <SiGithub />,
id: 'github.workflow-run',
parseUrl: (url) => {
if (url.hostname !== 'github.com') {
return;
}
const pathParts = url.pathname.split('/').filter(Boolean);
const [owner, repo, type, subtype, id] = pathParts.slice(0);
if (type !== 'actions' || subtype !== 'runs' || !id) {
return;
}
return { owner, repo, id: parseInt(id, 10) };
},
schema,
component: View,
edit: Edit,
};
export default widget;

View File

@@ -0,0 +1,12 @@
import { Type, Static } from '@sinclair/typebox';
const schema = Type.Object({
owner: Type.String(),
repo: Type.String(),
id: Type.Number(),
});
type Props = Static<typeof schema>;
export type { Props };
export { schema };

View File

@@ -0,0 +1,53 @@
import {
useAutoUpdate,
useGithubQuery,
useName,
withGithub,
} from '@refocus/sdk';
import { Props } from './schema';
import { Github } from '@refocus/ui';
type QueryData = {
id: number;
owner: string;
repo: string;
};
const WidgetView = withGithub<Props>(({ owner, repo, id }) => {
const [, setName] = useName();
const { data, fetch } = useGithubQuery(async (client, params: QueryData) => {
const response = await client.rest.actions.getWorkflowRun({
owner: params.owner,
repo: params.repo,
run_id: params.id,
});
setName(`${response.data.repository.full_name} ${response.data.name}`);
return response.data;
});
useAutoUpdate(
{
interval: 1000 * 60 * 5,
action: async () => fetch({ owner, repo, id }),
},
[owner, repo, id],
);
if (!data) {
return null;
}
return (
<Github.WorkflowRun
workflowRun={data}
onPress={() =>
window.open(
`https://github.com/${owner}/${repo}/actions/runs/${id}`,
'_blank',
)
}
/>
);
}, Github.NotLoggedIn);
export { WidgetView as View };

View File

@@ -0,0 +1,37 @@
import { useCallback, useState } from 'react';
import { Props } from './schema';
type EditorProps = {
value?: Props;
save: (data: Props) => void;
};
const Edit: React.FC<EditorProps> = ({ value, save }) => {
const [owner, setOwner] = useState(value?.owner || '');
const [repo, setRepo] = useState(value?.repo || '');
const handleSave = useCallback(() => {
save({
owner,
repo,
});
}, [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>
);
};
export { Edit };

View File

@@ -0,0 +1,28 @@
import { Widget } from '@refocus/sdk';
import { SiGithub } from 'react-icons/si';
import { schema } from './schema';
import { Edit } from './edit';
import { View } from './view';
const widget: Widget<typeof schema> = {
name: 'Github Workflow Runs',
description: 'Display the last 5 workflow runs',
icon: <SiGithub />,
id: 'github.workflow-runs',
parseUrl: (url) => {
if (url.hostname !== 'github.com') {
return;
}
const pathParts = url.pathname.split('/').filter(Boolean);
const [owner, repo, type, subtype] = pathParts.slice(0);
if (type !== 'actions' || subtype) {
return;
}
return { owner, repo };
},
schema,
component: View,
edit: Edit,
};
export default widget;

View File

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

View File

@@ -0,0 +1,56 @@
import {
useAutoUpdate,
useGithubQuery,
useName,
withGithub,
} from '@refocus/sdk';
import { Props } from './schema';
import { Github, List } from '@refocus/ui';
type QueryData = {
owner: string;
repo: string;
};
const WidgetView = withGithub<Props>(({ owner, repo }) => {
const [, setName] = useName();
const { data, fetch } = useGithubQuery(async (client, params: QueryData) => {
const response = await client.rest.actions.listWorkflowRunsForRepo({
owner: params.owner,
repo: params.repo,
});
setName(`${params.owner}/${params.repo} workflow runs`);
return response.data.workflow_runs.slice(0, 5);
});
console.log(data);
useAutoUpdate(
{
interval: 1000 * 60 * 5,
action: async () => fetch({ owner, repo }),
},
[owner, repo],
);
if (!data) {
return null;
}
return (
<List>
{data?.map((run) => (
<Github.WorkflowRun
workflowRun={run}
onPress={() =>
window.open(
`https://github.com/${owner}/${repo}/actions/runs/${run.id}`,
'_blank',
)
}
/>
))}
</List>
);
}, Github.NotLoggedIn);
export { WidgetView as View };

View File

@@ -2,6 +2,7 @@ import {
useAddWidgetNotification,
useAutoUpdate,
useLinearQuery,
useName,
withLinear,
} from '@refocus/sdk';
import { Chat, Linear, List, View } from '@refocus/ui';
@@ -13,9 +14,11 @@ type LinearIssueProps = {
const WidgetView = withLinear<LinearIssueProps>(({ id }) => {
const addNotification = useAddWidgetNotification();
const [, setName] = useName();
const { data, fetch } = useLinearQuery(async (client) => {
const issue = await client.issue(id);
const comments = await issue.comments();
setName(id);
return comments.nodes;
});

View File

@@ -1,9 +1,11 @@
import {
useAddWidgetNotification,
useAutoUpdate,
useName,
useSlackQuery,
withSlack,
} from '@refocus/sdk';
import styled from 'styled-components';
import { Props } from './schema';
import { Message } from './message/view';
import { useState } from 'react';
@@ -13,7 +15,24 @@ type PostMessageOptions = {
message: string;
};
const MessageList = styled(View)`
transform: scaleY(-1);
flex: 1;
overflow-y: auto;
flex-grow: 1;
flex-shrink: 1;
& > * {
transform: scaleY(-1);
}
`;
const Wrapper = styled(View)`
max-height: 100%;
overflow: hidden;
`;
const WidgetView = withSlack<Props>(({ conversationId }) => {
const [, setName] = useName();
const addNotification = useAddWidgetNotification();
const [message, setMessage] = useState('');
const { fetch, data } = useSlackQuery(async (client, props: Props) => {
@@ -27,6 +46,7 @@ const WidgetView = withSlack<Props>(({ conversationId }) => {
const response = await client.send('conversations.info', {
channel: props.conversationId,
});
setName(response.channel!.name || 'Direct message');
return response.channel!;
});
@@ -39,7 +59,7 @@ const WidgetView = withSlack<Props>(({ conversationId }) => {
},
);
const update = useAutoUpdate(
useAutoUpdate(
{
action: async () => {
await info.fetch({ conversationId });
@@ -68,17 +88,8 @@ const WidgetView = withSlack<Props>(({ conversationId }) => {
);
return (
<View $p="md">
<button onClick={update}>Update</button>
<Typography variant="header">
{info.data?.name || 'Direct message'}
</Typography>
<Chat.Compose
value={message}
onValueChange={setMessage}
onSend={() => post({ message })}
/>
<List>
<Wrapper $p="sm" $fc $gap="sm">
<MessageList $gap="md" $fc>
{data?.map((message) => (
<Message
key={message.ts}
@@ -86,8 +97,13 @@ const WidgetView = withSlack<Props>(({ conversationId }) => {
userId={message.user}
/>
))}
</List>
</View>
</MessageList>
<Chat.Compose
value={message}
onValueChange={setMessage}
onSend={() => post({ message })}
/>
</Wrapper>
);
}, Slack.NotLoggedIn);