pl-api: basic support for pleroma shoutbox
Signed-off-by: Nicole Mikołajczyk <git@mkljczk.pl>
This commit is contained in:
@ -251,6 +251,7 @@ import type {
|
||||
GetTrendingTags,
|
||||
} from './params/trends';
|
||||
import type { PaginatedResponse } from './responses';
|
||||
import { ShoutMessage, shoutMessageSchema } from './entities/shout-message';
|
||||
|
||||
const GROUPED_TYPES = ['favourite', 'reblog', 'emoji_reaction', 'event_reminder', 'participation_accepted', 'participation_request'];
|
||||
|
||||
@ -285,6 +286,10 @@ class PlApiClient {
|
||||
unsubscribe: (stream: string, params?: { list?: string; tag?: string }) => void;
|
||||
close: () => void;
|
||||
};
|
||||
#shoutSocket?: {
|
||||
message: (text: string) => void;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
@ -2765,6 +2770,45 @@ class PlApiClient {
|
||||
},
|
||||
};
|
||||
|
||||
public readonly shoutbox = {
|
||||
connect: (token: string, { onMessage, onMessages }: {
|
||||
onMessages: (messages: Array<ShoutMessage>) => void;
|
||||
onMessage: (message: ShoutMessage) => void;
|
||||
}) => {
|
||||
if (this.#shoutSocket) return this.#shoutSocket;
|
||||
|
||||
const path = buildFullPath('/socket/websocket', this.baseURL, { token, vsn: '2.0.0' });
|
||||
|
||||
const ws = new WebSocket(path);
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
// @eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [_, __, ___, type, payload] = JSON.parse(event.data as string);
|
||||
if (type === 'new_msg') {
|
||||
const message = v.parse(payload, shoutMessageSchema);
|
||||
onMessage(message);
|
||||
} else if (type === 'messages') {
|
||||
const messages = v.parse(filteredArray(shoutMessageSchema), payload.messages);
|
||||
onMessages(messages);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onopen = () => {
|
||||
ws.send(JSON.stringify(['3', '3', 'chat:public', 'phx_join', {}]));
|
||||
};
|
||||
|
||||
this.#shoutSocket = {
|
||||
message: (text: string) => {
|
||||
ws.send(JSON.stringify({ type: 'message', text }));
|
||||
},
|
||||
close: () => {
|
||||
ws.close();
|
||||
this.#shoutSocket = undefined;
|
||||
},
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
public readonly notifications = {
|
||||
/**
|
||||
* Get all notifications
|
||||
|
||||
@ -67,6 +67,7 @@ export * from './rule';
|
||||
export * from './scheduled-status';
|
||||
export * from './scrobble';
|
||||
export * from './search';
|
||||
export * from './shout-message';
|
||||
export * from './status';
|
||||
export * from './status-edit';
|
||||
export * from './status-source';
|
||||
|
||||
19
packages/pl-api/lib/entities/shout-message.ts
Normal file
19
packages/pl-api/lib/entities/shout-message.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import * as v from 'valibot';
|
||||
|
||||
import { accountSchema } from './account';
|
||||
|
||||
/**
|
||||
* @category Schemas
|
||||
*/
|
||||
const shoutMessageSchema = v.object({
|
||||
id: v.number(),
|
||||
text: v.string(),
|
||||
author: accountSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* @category Entity types
|
||||
*/
|
||||
type ShoutMessage = v.InferOutput<typeof shoutMessageSchema>;
|
||||
|
||||
export { shoutMessageSchema, type ShoutMessage };
|
||||
@ -131,5 +131,7 @@ export {
|
||||
fetchMeSuccess,
|
||||
fetchMeFail,
|
||||
patchMeSuccess,
|
||||
getMeToken,
|
||||
getMeUrl,
|
||||
type MeAction,
|
||||
};
|
||||
|
||||
59
packages/pl-fe/src/actions/shoutbox.ts
Normal file
59
packages/pl-fe/src/actions/shoutbox.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { verifyCredentials } from './auth';
|
||||
import { getMeToken, getMeUrl } from './me';
|
||||
|
||||
import type { PlApiClient, ShoutMessage } from 'pl-api';
|
||||
import type { AppDispatch, RootState } from 'pl-fe/store';
|
||||
|
||||
const SHOUTBOX_MESSAGE_IMPORT = 'SHOUTBOX_MESSAGE_IMPORT' as const;
|
||||
const SHOUTBOX_MESSAGES_IMPORT = 'SHOUTBOX_MESSAGES_IMPORT' as const;
|
||||
const SHOUTBOX_CONNECT = 'SHOUTBOX_CONNECT' as const;
|
||||
|
||||
const importShoutboxMessages = (messages: ShoutMessage[]) => ({
|
||||
type: SHOUTBOX_MESSAGES_IMPORT,
|
||||
messages,
|
||||
});
|
||||
|
||||
const importShoutboxMessage = (message: ShoutMessage) => ({
|
||||
type: SHOUTBOX_MESSAGE_IMPORT,
|
||||
message,
|
||||
});
|
||||
|
||||
const connectShoutbox = (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const token = getMeToken(state);
|
||||
const accountUrl = getMeUrl(state);
|
||||
|
||||
if (!accountUrl) return;
|
||||
|
||||
return dispatch(verifyCredentials(token, accountUrl)).then((account) => {
|
||||
if (account.__meta.pleroma?.chat_token) {
|
||||
const socket = state.auth.client.shoutbox.connect(account.__meta.pleroma?.chat_token, {
|
||||
onMessage: (message) => dispatch(importShoutboxMessage(message)),
|
||||
onMessages: (messages) => dispatch(importShoutboxMessages(messages)),
|
||||
});
|
||||
|
||||
return dispatch({
|
||||
type: SHOUTBOX_CONNECT,
|
||||
socket,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
type ShoutboxAction =
|
||||
| {
|
||||
type: typeof SHOUTBOX_CONNECT;
|
||||
socket: ReturnType<(InstanceType<typeof PlApiClient>)['shoutbox']['connect']>;
|
||||
}
|
||||
| ReturnType<typeof importShoutboxMessages>
|
||||
| ReturnType<typeof importShoutboxMessage>;
|
||||
|
||||
export {
|
||||
SHOUTBOX_MESSAGES_IMPORT,
|
||||
SHOUTBOX_MESSAGE_IMPORT,
|
||||
SHOUTBOX_CONNECT,
|
||||
importShoutboxMessages,
|
||||
importShoutboxMessage,
|
||||
connectShoutbox,
|
||||
type ShoutboxAction,
|
||||
};
|
||||
@ -158,7 +158,7 @@ const Preview: React.FC<PreviewProps> = ({ media, position: [x, y], onPositionCh
|
||||
</div>
|
||||
{withFocalPoint && (
|
||||
<div
|
||||
className='pointer-events-none absolute h-24 w-24 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white'
|
||||
className='pointer-events-none absolute size-24 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white'
|
||||
style={{
|
||||
top: `${y * 100}%`,
|
||||
left: `${x * 100}%`,
|
||||
|
||||
@ -27,6 +27,7 @@ import plfe from './pl-fe';
|
||||
import polls from './polls';
|
||||
import push_notifications from './push-notifications';
|
||||
import security from './security';
|
||||
import shoutbox from './shoutbox';
|
||||
import status_lists from './status-lists';
|
||||
import statuses from './statuses';
|
||||
import timelines from './timelines';
|
||||
@ -56,6 +57,7 @@ const reducers = {
|
||||
polls,
|
||||
push_notifications,
|
||||
security,
|
||||
shoutbox,
|
||||
status_lists,
|
||||
statuses,
|
||||
timelines,
|
||||
|
||||
28
packages/pl-fe/src/reducers/shoutbox.ts
Normal file
28
packages/pl-fe/src/reducers/shoutbox.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { SHOUTBOX_CONNECT, SHOUTBOX_MESSAGES_IMPORT, SHOUTBOX_MESSAGE_IMPORT, type ShoutboxAction } from 'pl-fe/actions/shoutbox';
|
||||
|
||||
import type { PlApiClient, ShoutMessage } from 'pl-api';
|
||||
|
||||
interface State {
|
||||
socket: ReturnType<(InstanceType<typeof PlApiClient>)['shoutbox']['connect']> | null;
|
||||
messages: Array<ShoutMessage>;
|
||||
}
|
||||
|
||||
const initialState: State = {
|
||||
socket: null,
|
||||
messages: [],
|
||||
};
|
||||
|
||||
const shoutboxReducer = (state = initialState, action: ShoutboxAction) => {
|
||||
switch (action.type) {
|
||||
case SHOUTBOX_CONNECT:
|
||||
return { ...state, socket: action.socket };
|
||||
case SHOUTBOX_MESSAGES_IMPORT:
|
||||
return { ...state, messages: action.messages };
|
||||
case SHOUTBOX_MESSAGE_IMPORT:
|
||||
return { ...state, messages: [...state.messages, action.message] };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export { shoutboxReducer as default };
|
||||
Reference in New Issue
Block a user