Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add post url when live started feature #160

Merged
merged 1 commit into from
Mar 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import {
} from '@chakra-ui/react';
import { FC, useCallback, useState } from 'react';
import { LivePrivate } from 'api-types/common/types';
import { Dialog } from './dialog';
import { LiveInfoModal } from './live-info-modal';
import { Dialog } from '../dialog';
import { LiveInfoModal } from '../live-info-modal';
import { useAuth } from '~/utils/hooks/use-auth';
import { client } from '~/utils/api/client';
import { useAPIError } from '~/utils/hooks/api/use-api-error';
import { StartModal } from '../start-modal';

type Props = {
live: LivePrivate;
Expand Down Expand Up @@ -40,42 +41,52 @@ export const GeneralSettings: FC<Props> = ({ live, notPushing }) => {
useAPIError(error);

const handlePublish = useCallback(
(isStart: boolean) => {
void (async () => {
if (!token) {
return;
}
async (isStart: boolean) => {
if (!token) {
return;
}

try {
await client.v1.streams._liveId(live.id).action.post({
body: {
command: isStart ? 'publish' : 'end'
},
headers: {
Authorization: `Bearer ${token}`
}
});
onCloseStop();
onCloseStart();
} catch (e) {
console.warn(e);
setError(e);
}
})();
try {
await client.v1.streams._liveId(live.id).action.post({
body: {
command: isStart ? 'publish' : 'end'
},
headers: {
Authorization: `Bearer ${token}`
}
});
} catch (e) {
console.warn(e);
setError(e);

throw e;
}
},
[live.id, token, onCloseStart, onCloseStop]
[live.id, token]
);

const handleStart = useCallback(async () => {
await handlePublish(true);
}, [handlePublish]);

const handleEnd = useCallback(() => {
void (async () => {
await handlePublish(false);
onCloseStop();
})();
}, [handlePublish, onCloseStop]);

return (
<Stack spacing={6}>
<Wrap spacing={6}>
<WrapItem>
<Dialog
<StartModal
isOpen={isOpenStart}
onClose={onCloseStart}
onSubmit={() => handlePublish(true)}
title="配信を開始しますか?"
submitText="配信を開始"
onPublish={handleStart}
url={live.publicUrl}
hashtag={live.hashtag}
isBroadcastViaBrowser={false}
/>

<Tooltip
Expand All @@ -97,7 +108,7 @@ export const GeneralSettings: FC<Props> = ({ live, notPushing }) => {
<Dialog
isOpen={isOpenStop}
onClose={onCloseStop}
onSubmit={() => handlePublish(false)}
onSubmit={handleEnd}
title="配信を終了しますか?"
submitText="配信を終了"
/>
Expand Down
251 changes: 251 additions & 0 deletions apps/web/organisms/live/admin/start-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import {
Button,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
Stack,
FormControl,
Checkbox,
FormLabel,
Textarea,
Collapse,
useToast,
Alert,
AlertIcon
} from '@chakra-ui/react';
import { FC, useCallback, useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
import {
MisskeyRequestReSignError,
postToExternal,
Visibility
} from '~/utils/api/post-to-external';
import {
MASTODON_DOMAIN_LS,
MISSKEY_DOMAIN_LS,
SignInType
} from '~/utils/contexts/auth';
import { useAPIError } from '~/utils/hooks/api/use-api-error';
import { useUsersMe } from '~/utils/hooks/api/use-users-me';
import { useAuth } from '~/utils/hooks/use-auth';

type Props = {
onPublish: () => Promise<void>;
url: string;
hashtag?: string;
isOpen: boolean;
onClose: () => void;
isBroadcastViaBrowser: boolean;
};

export const StartModal: FC<Props> = ({
onPublish,
url,
hashtag,
isOpen,
onClose,
isBroadcastViaBrowser
}) => {
const { mastodonToken, misskeyToken } = useAuth();
const toast = useToast();
const intl = useIntl();
const [me] = useUsersMe();
const [isLoading, setIsLoading] = useState(false);
const [publishPost, setPublishPost] = useState(true);
const [postText, setPostText] = useState('');
const [postError, setPostError] = useState<unknown>();
useAPIError(postError);

const applyPreset = useCallback(
(index: number) => {
const preset = presets[index];
if (!preset) {
return;
}

setPostText(
preset.text
.replace(/\{url\}/g, url)
.replace(/\{hashtag\}/g, hashtag ? `#${hashtag}` : '')
);
},
[url, hashtag]
);

const post = useCallback(async () => {
if (!publishPost || !postText) {
return;
}

try {
if (mastodonToken) {
const domain = localStorage.getItem(MASTODON_DOMAIN_LS);
if (!domain) {
throw new Error('domain is required');
}

await postToExternal(
{
type: SignInType.Mastodon,
domain,
token: mastodonToken
},
postText,
Visibility.Public
);
} else if (misskeyToken) {
const domain = localStorage.getItem(MISSKEY_DOMAIN_LS);
if (!domain) {
throw new Error('domain is required');
}

try {
await postToExternal(
{
type: SignInType.Misskey,
domain,
token: misskeyToken
},
postText,
Visibility.Public
);
} catch (e) {
if (e instanceof MisskeyRequestReSignError) {
toast({
title: intl.formatMessage({
id: 'toast.api-error.title'
}),
description: intl.formatMessage({
id: 'live.comment-post.permission-denied'
}),
status: 'error',
duration: 10000,
isClosable: true
});
}
}
}
} catch (e) {
setPostError(e);
// 投稿が失敗しても throw しない
}
}, [publishPost, postText, mastodonToken, misskeyToken, toast, intl]);

const handleClick = useCallback(() => {
void (async () => {
try {
setIsLoading(true);
await onPublish();
await post();
onClose();
} catch {
// noop
} finally {
setIsLoading(false);
}
})();
}, [onPublish, onClose, post]);

useEffect(() => {
applyPreset(0);
}, [applyPreset]);

return (
<Modal
isOpen={isOpen}
onClose={onClose}
isCentered
motionPreset="slideInBottom"
scrollBehavior="inside"
size="xl"
>
<ModalOverlay />
<ModalContent>
<ModalHeader>配信を開始</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Stack spacing={6}>
{isBroadcastViaBrowser && (
<Alert status="info">
<AlertIcon />
配信を開始すると、マイクがオンになります。(ブラウザの許可ダイアログが表示された場合は、許可してください)
</Alert>
)}

<FormControl>
<Checkbox
isChecked={publishPost}
onChange={e => setPublishPost(e.target.checked)}
>
配信を開始したら {me?.account} で公開投稿する
</Checkbox>
</FormControl>

<Collapse in={publishPost} animateOpacity>
<Stack spacing={6}>
<FormControl>
<FormLabel>投稿文</FormLabel>

<Textarea
value={postText}
onChange={e => setPostText(e.target.value)}
maxLength={500}
disabled={isLoading}
/>
</FormControl>

<FormControl>
<FormLabel>プリセットを使用</FormLabel>

<Stack spacing={2}>
{presets.map((preset, index) => (
<Button
key={preset.name}
size="sm"
onClick={() => applyPreset(index)}
disabled={isLoading}
>
{preset.name}
</Button>
))}
</Stack>
</FormControl>
</Stack>
</Collapse>
</Stack>
</ModalBody>

<ModalFooter>
<Button
colorScheme="blue"
width="100%"
onClick={handleClick}
isLoading={isLoading}
>
配信を開始{publishPost && '&投稿'}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
};

type Preset = {
name: string;
text: string;
};

const presets: Preset[] = [
{
name: 'デフォルト',
text: 'KnzkLive で配信中! {url} {hashtag}'
},
{
name: 'Knzk',
text: '{url}\n{url}\n{url}'
}
];
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import { useWakeLock } from '~/utils/hooks/use-wake-lock';
import { TimeCounter } from '~/atoms/time-counter';
import { useConvertLiveId } from '~/utils/hooks/api/use-convert-live-id';
import { useBeforeUnload } from 'react-use';
import { StartModal } from '~/organisms/live/admin/start-modal';

type Params = { slug: string; id: string };

Expand Down Expand Up @@ -215,13 +216,16 @@ const Page: NextPage<PageProps<Props, Params & PathProps>> = ({

<StartedNote />

<Dialog
isOpen={isOpenStart}
onClose={onCloseStart}
onSubmit={() => void connect()}
title="配信を開始しますか?マイクがオンになります。"
submitText="配信を開始"
/>
{live && (
<StartModal
isOpen={isOpenStart}
onClose={onCloseStart}
onPublish={connect}
url={live.publicUrl}
hashtag={live.hashtag}
isBroadcastViaBrowser={true}
/>
)}

<Dialog
isOpen={isOpenStop}
Expand Down
Loading