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,
|
GetTrendingTags,
|
||||||
} from './params/trends';
|
} from './params/trends';
|
||||||
import type { PaginatedResponse } from './responses';
|
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'];
|
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;
|
unsubscribe: (stream: string, params?: { list?: string; tag?: string }) => void;
|
||||||
close: () => 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 = {
|
public readonly notifications = {
|
||||||
/**
|
/**
|
||||||
* Get all notifications
|
* Get all notifications
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export * from './rule';
|
|||||||
export * from './scheduled-status';
|
export * from './scheduled-status';
|
||||||
export * from './scrobble';
|
export * from './scrobble';
|
||||||
export * from './search';
|
export * from './search';
|
||||||
|
export * from './shout-message';
|
||||||
export * from './status';
|
export * from './status';
|
||||||
export * from './status-edit';
|
export * from './status-edit';
|
||||||
export * from './status-source';
|
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,
|
fetchMeSuccess,
|
||||||
fetchMeFail,
|
fetchMeFail,
|
||||||
patchMeSuccess,
|
patchMeSuccess,
|
||||||
|
getMeToken,
|
||||||
|
getMeUrl,
|
||||||
type MeAction,
|
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>
|
</div>
|
||||||
{withFocalPoint && (
|
{withFocalPoint && (
|
||||||
<div
|
<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={{
|
style={{
|
||||||
top: `${y * 100}%`,
|
top: `${y * 100}%`,
|
||||||
left: `${x * 100}%`,
|
left: `${x * 100}%`,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import plfe from './pl-fe';
|
|||||||
import polls from './polls';
|
import polls from './polls';
|
||||||
import push_notifications from './push-notifications';
|
import push_notifications from './push-notifications';
|
||||||
import security from './security';
|
import security from './security';
|
||||||
|
import shoutbox from './shoutbox';
|
||||||
import status_lists from './status-lists';
|
import status_lists from './status-lists';
|
||||||
import statuses from './statuses';
|
import statuses from './statuses';
|
||||||
import timelines from './timelines';
|
import timelines from './timelines';
|
||||||
@@ -56,6 +57,7 @@ const reducers = {
|
|||||||
polls,
|
polls,
|
||||||
push_notifications,
|
push_notifications,
|
||||||
security,
|
security,
|
||||||
|
shoutbox,
|
||||||
status_lists,
|
status_lists,
|
||||||
statuses,
|
statuses,
|
||||||
timelines,
|
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