refactor progress
This commit is contained in:
@@ -17,8 +17,8 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import AccessibilityStore, {type AccessibilitySettings} from '~/stores/AccessibilityStore';
|
||||
import AccessibilityStore, {type AccessibilitySettings} from '@app/stores/AccessibilityStore';
|
||||
|
||||
export const update = (settings: Partial<AccessibilitySettings>): void => {
|
||||
export function update(settings: Partial<AccessibilitySettings>): void {
|
||||
AccessibilityStore.updateSettings(settings);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,20 +17,20 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import type {AuthSession} from '~/records/AuthSessionRecord';
|
||||
import AuthSessionStore from '~/stores/AuthSessionStore';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import AuthSessionStore from '@app/stores/AuthSessionStore';
|
||||
import type {AuthSessionResponse} from '@fluxer/schema/src/domains/auth/AuthSchemas';
|
||||
|
||||
const logger = new Logger('AuthSessionsService');
|
||||
|
||||
export const fetch = async (): Promise<void> => {
|
||||
export async function fetch(): Promise<void> {
|
||||
logger.debug('Fetching authentication sessions');
|
||||
AuthSessionStore.fetchPending();
|
||||
|
||||
try {
|
||||
const response = await http.get<Array<AuthSession>>({url: Endpoints.AUTH_SESSIONS, retries: 2});
|
||||
const response = await http.get<Array<AuthSessionResponse>>({url: Endpoints.AUTH_SESSIONS, retries: 2});
|
||||
const sessions = response.body ?? [];
|
||||
logger.info(`Fetched ${sessions.length} authentication sessions`);
|
||||
AuthSessionStore.fetchSuccess(sessions);
|
||||
@@ -39,9 +39,9 @@ export const fetch = async (): Promise<void> => {
|
||||
AuthSessionStore.fetchError();
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const logout = async (sessionIdHashes: Array<string>): Promise<void> => {
|
||||
export async function logout(sessionIdHashes: Array<string>): Promise<void> {
|
||||
if (!sessionIdHashes.length) {
|
||||
logger.warn('Attempted to logout with empty session list');
|
||||
return;
|
||||
@@ -62,4 +62,4 @@ export const logout = async (sessionIdHashes: Array<string>): Promise<void> => {
|
||||
AuthSessionStore.logoutError();
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,17 +17,19 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import type {UserData} from '@app/lib/AccountStorage';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {HttpError} from '@app/lib/HttpError';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import AccountManager from '@app/stores/AccountManager';
|
||||
import AuthenticationStore from '@app/stores/AuthenticationStore';
|
||||
import GatewayConnectionStore from '@app/stores/gateway/GatewayConnectionStore';
|
||||
import {getApiErrorCode} from '@app/utils/ApiErrorUtils';
|
||||
import {isDesktop} from '@app/utils/NativeUtils';
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import type {ValueOf} from '@fluxer/constants/src/ValueOf';
|
||||
import type {AuthenticationResponseJSON, PublicKeyCredentialRequestOptionsJSON} from '@simplewebauthn/browser';
|
||||
import {APIErrorCodes} from '~/Constants';
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import type {UserData} from '~/lib/AccountStorage';
|
||||
import http from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import AccountManager from '~/stores/AccountManager';
|
||||
import AuthenticationStore from '~/stores/AuthenticationStore';
|
||||
import ConnectionStore from '~/stores/ConnectionStore';
|
||||
import RuntimeConfigStore from '~/stores/RuntimeConfigStore';
|
||||
import {isDesktop} from '~/utils/NativeUtils';
|
||||
|
||||
const logger = new Logger('AuthService');
|
||||
|
||||
@@ -43,14 +45,13 @@ export const VerificationResult = {
|
||||
RATE_LIMITED: 'RATE_LIMITED',
|
||||
SERVER_ERROR: 'SERVER_ERROR',
|
||||
} as const;
|
||||
export type VerificationResult = (typeof VerificationResult)[keyof typeof VerificationResult];
|
||||
export type VerificationResult = ValueOf<typeof VerificationResult>;
|
||||
|
||||
interface RegisterData {
|
||||
email?: string;
|
||||
global_name?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
beta_code: string;
|
||||
date_of_birth: string;
|
||||
consent: boolean;
|
||||
captchaToken?: string;
|
||||
@@ -63,7 +64,6 @@ interface StandardLoginResponse {
|
||||
user_id: string;
|
||||
token: string;
|
||||
theme?: string;
|
||||
pending_verification?: boolean;
|
||||
}
|
||||
|
||||
interface MfaLoginResponse {
|
||||
@@ -72,6 +72,8 @@ interface MfaLoginResponse {
|
||||
sms: boolean;
|
||||
totp: boolean;
|
||||
webauthn: boolean;
|
||||
allowed_methods?: Array<string>;
|
||||
sms_phone_hint?: string | null;
|
||||
}
|
||||
|
||||
type LoginResponse = StandardLoginResponse | MfaLoginResponse;
|
||||
@@ -83,19 +85,21 @@ export interface IpAuthorizationRequiredResponse {
|
||||
resend_available_in: number;
|
||||
}
|
||||
|
||||
export const isIpAuthorizationRequiredResponse = (
|
||||
export function isIpAuthorizationRequiredResponse(
|
||||
response: LoginResponse | IpAuthorizationRequiredResponse,
|
||||
): response is IpAuthorizationRequiredResponse => {
|
||||
): response is IpAuthorizationRequiredResponse {
|
||||
return (response as IpAuthorizationRequiredResponse).ip_authorization_required === true;
|
||||
};
|
||||
}
|
||||
|
||||
interface TokenResponse {
|
||||
user_id: string;
|
||||
token: string;
|
||||
theme?: string;
|
||||
pending_verification?: boolean;
|
||||
redirect_to?: string;
|
||||
}
|
||||
|
||||
export type ResetPasswordResponse = TokenResponse | MfaLoginResponse;
|
||||
|
||||
interface DesktopHandoffInitiateResponse {
|
||||
code: string;
|
||||
expires_at: string;
|
||||
@@ -107,26 +111,20 @@ interface DesktopHandoffStatusResponse {
|
||||
user_id?: string;
|
||||
}
|
||||
|
||||
export const login = async ({
|
||||
export async function login({
|
||||
email,
|
||||
password,
|
||||
captchaToken,
|
||||
inviteCode,
|
||||
captchaType,
|
||||
customApiEndpoint,
|
||||
}: {
|
||||
email: string;
|
||||
password: string;
|
||||
captchaToken?: string;
|
||||
inviteCode?: string;
|
||||
captchaType?: 'turnstile' | 'hcaptcha';
|
||||
customApiEndpoint?: string;
|
||||
}): Promise<LoginResponse | IpAuthorizationRequiredResponse> => {
|
||||
}): Promise<LoginResponse | IpAuthorizationRequiredResponse> {
|
||||
try {
|
||||
if (customApiEndpoint) {
|
||||
await RuntimeConfigStore.connectToEndpoint(customApiEndpoint);
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
if (captchaToken) {
|
||||
headers['X-Captcha-Token'] = captchaToken;
|
||||
@@ -148,22 +146,26 @@ export const login = async ({
|
||||
logger.debug('Login successful', {mfa: response.body?.mfa});
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
const httpError = error as {status?: number; body?: any};
|
||||
if (httpError.status === 403 && httpError.body?.code === APIErrorCodes.IP_AUTHORIZATION_REQUIRED) {
|
||||
if (
|
||||
error instanceof HttpError &&
|
||||
error.status === 403 &&
|
||||
getApiErrorCode(error) === APIErrorCodes.IP_AUTHORIZATION_REQUIRED
|
||||
) {
|
||||
logger.info('Login requires IP authorization', {email});
|
||||
const body = error.body as Record<string, unknown> | undefined;
|
||||
return {
|
||||
ip_authorization_required: true,
|
||||
ticket: httpError.body?.ticket,
|
||||
email: httpError.body?.email,
|
||||
resend_available_in: httpError.body?.resend_available_in ?? 30,
|
||||
ticket: body?.ticket as string,
|
||||
email: body?.email as string,
|
||||
resend_available_in: (body?.resend_available_in as number) ?? 30,
|
||||
};
|
||||
}
|
||||
logger.error('Login failed', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const loginMfaTotp = async (code: string, ticket: string, inviteCode?: string): Promise<TokenResponse> => {
|
||||
export async function loginMfaTotp(code: string, ticket: string, inviteCode?: string): Promise<TokenResponse> {
|
||||
try {
|
||||
const body: {
|
||||
code: string;
|
||||
@@ -185,9 +187,9 @@ export const loginMfaTotp = async (code: string, ticket: string, inviteCode?: st
|
||||
logger.error('MFA TOTP authentication failed', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const loginMfaSmsSend = async (ticket: string): Promise<void> => {
|
||||
export async function loginMfaSmsSend(ticket: string): Promise<void> {
|
||||
try {
|
||||
await http.post({
|
||||
url: Endpoints.AUTH_LOGIN_MFA_SMS_SEND,
|
||||
@@ -199,9 +201,9 @@ export const loginMfaSmsSend = async (ticket: string): Promise<void> => {
|
||||
logger.error('Failed to send SMS MFA code', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const loginMfaSms = async (code: string, ticket: string, inviteCode?: string): Promise<TokenResponse> => {
|
||||
export async function loginMfaSms(code: string, ticket: string, inviteCode?: string): Promise<TokenResponse> {
|
||||
try {
|
||||
const body: {
|
||||
code: string;
|
||||
@@ -223,14 +225,14 @@ export const loginMfaSms = async (code: string, ticket: string, inviteCode?: str
|
||||
logger.error('MFA SMS authentication failed', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const loginMfaWebAuthn = async (
|
||||
export async function loginMfaWebAuthn(
|
||||
response: AuthenticationResponseJSON,
|
||||
challenge: string,
|
||||
ticket: string,
|
||||
inviteCode?: string,
|
||||
): Promise<TokenResponse> => {
|
||||
): Promise<TokenResponse> {
|
||||
try {
|
||||
const body: {
|
||||
response: AuthenticationResponseJSON;
|
||||
@@ -253,9 +255,9 @@ export const loginMfaWebAuthn = async (
|
||||
logger.error('MFA WebAuthn authentication failed', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const getWebAuthnMfaOptions = async (ticket: string): Promise<PublicKeyCredentialRequestOptionsJSON> => {
|
||||
export async function getWebAuthnMfaOptions(ticket: string): Promise<PublicKeyCredentialRequestOptionsJSON> {
|
||||
try {
|
||||
const response = await http.post<PublicKeyCredentialRequestOptionsJSON>({
|
||||
url: Endpoints.AUTH_LOGIN_MFA_WEBAUTHN_OPTIONS,
|
||||
@@ -269,9 +271,9 @@ export const getWebAuthnMfaOptions = async (ticket: string): Promise<PublicKeyCr
|
||||
logger.error('Failed to get WebAuthn MFA options', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const getWebAuthnAuthenticationOptions = async (): Promise<PublicKeyCredentialRequestOptionsJSON> => {
|
||||
export async function getWebAuthnAuthenticationOptions(): Promise<PublicKeyCredentialRequestOptionsJSON> {
|
||||
try {
|
||||
const response = await http.post<PublicKeyCredentialRequestOptionsJSON>({
|
||||
url: Endpoints.AUTH_WEBAUTHN_OPTIONS,
|
||||
@@ -284,13 +286,13 @@ export const getWebAuthnAuthenticationOptions = async (): Promise<PublicKeyCrede
|
||||
logger.error('Failed to get WebAuthn authentication options', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const authenticateWithWebAuthn = async (
|
||||
export async function authenticateWithWebAuthn(
|
||||
response: AuthenticationResponseJSON,
|
||||
challenge: string,
|
||||
inviteCode?: string,
|
||||
): Promise<TokenResponse> => {
|
||||
): Promise<TokenResponse> {
|
||||
try {
|
||||
const body: {
|
||||
response: AuthenticationResponseJSON;
|
||||
@@ -312,9 +314,9 @@ export const authenticateWithWebAuthn = async (
|
||||
logger.error('WebAuthn authentication failed', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const register = async (data: RegisterData): Promise<TokenResponse> => {
|
||||
export async function register(data: RegisterData): Promise<TokenResponse> {
|
||||
try {
|
||||
const headers: Record<string, string> = {};
|
||||
if (data.captchaToken) {
|
||||
@@ -334,13 +336,13 @@ export const register = async (data: RegisterData): Promise<TokenResponse> => {
|
||||
logger.error('Registration failed', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
interface UsernameSuggestionsResponse {
|
||||
suggestions: Array<string>;
|
||||
}
|
||||
|
||||
export const getUsernameSuggestions = async (globalName: string): Promise<Array<string>> => {
|
||||
export async function getUsernameSuggestions(globalName: string): Promise<Array<string>> {
|
||||
try {
|
||||
const response = await http.post<UsernameSuggestionsResponse>({
|
||||
url: Endpoints.AUTH_USERNAME_SUGGESTIONS,
|
||||
@@ -354,13 +356,13 @@ export const getUsernameSuggestions = async (globalName: string): Promise<Array<
|
||||
logger.error('Failed to fetch username suggestions', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const forgotPassword = async (
|
||||
export async function forgotPassword(
|
||||
email: string,
|
||||
captchaToken?: string,
|
||||
captchaType?: 'turnstile' | 'hcaptcha',
|
||||
): Promise<void> => {
|
||||
): Promise<void> {
|
||||
try {
|
||||
const headers: Record<string, string> = {};
|
||||
if (captchaToken) {
|
||||
@@ -376,11 +378,11 @@ export const forgotPassword = async (
|
||||
} catch (error) {
|
||||
logger.warn('Password reset request failed, but returning success to user', error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const resetPassword = async (token: string, password: string): Promise<TokenResponse> => {
|
||||
export async function resetPassword(token: string, password: string): Promise<ResetPasswordResponse> {
|
||||
try {
|
||||
const response = await http.post<TokenResponse>({
|
||||
const response = await http.post<ResetPasswordResponse>({
|
||||
url: Endpoints.AUTH_RESET_PASSWORD,
|
||||
body: {token, password},
|
||||
headers: withPlatformHeader(),
|
||||
@@ -392,9 +394,9 @@ export const resetPassword = async (token: string, password: string): Promise<To
|
||||
logger.error('Password reset failed', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const revertEmailChange = async (token: string, password: string): Promise<TokenResponse> => {
|
||||
export async function revertEmailChange(token: string, password: string): Promise<TokenResponse> {
|
||||
try {
|
||||
const response = await http.post<TokenResponse>({
|
||||
url: Endpoints.AUTH_EMAIL_REVERT,
|
||||
@@ -408,9 +410,9 @@ export const revertEmailChange = async (token: string, password: string): Promis
|
||||
logger.error('Email revert failed', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const verifyEmail = async (token: string): Promise<VerificationResult> => {
|
||||
export async function verifyEmail(token: string): Promise<VerificationResult> {
|
||||
try {
|
||||
await http.post({
|
||||
url: Endpoints.AUTH_VERIFY_EMAIL,
|
||||
@@ -428,9 +430,9 @@ export const verifyEmail = async (token: string): Promise<VerificationResult> =>
|
||||
logger.error('Email verification failed - server error', error);
|
||||
return VerificationResult.SERVER_ERROR;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const resendVerificationEmail = async (): Promise<VerificationResult> => {
|
||||
export async function resendVerificationEmail(): Promise<VerificationResult> {
|
||||
try {
|
||||
await http.post({
|
||||
url: Endpoints.AUTH_RESEND_VERIFICATION,
|
||||
@@ -447,13 +449,13 @@ export const resendVerificationEmail = async (): Promise<VerificationResult> =>
|
||||
logger.error('Failed to resend verification email - server error', error);
|
||||
return VerificationResult.SERVER_ERROR;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const logout = async (): Promise<void> => {
|
||||
export async function logout(): Promise<void> {
|
||||
await AccountManager.logout();
|
||||
};
|
||||
}
|
||||
|
||||
export const authorizeIp = async (token: string): Promise<VerificationResult> => {
|
||||
export async function authorizeIp(token: string): Promise<VerificationResult> {
|
||||
try {
|
||||
await http.post({
|
||||
url: Endpoints.AUTH_AUTHORIZE_IP,
|
||||
@@ -471,45 +473,47 @@ export const authorizeIp = async (token: string): Promise<VerificationResult> =>
|
||||
logger.error('IP authorization failed - server error', error);
|
||||
return VerificationResult.SERVER_ERROR;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const resendIpAuthorization = async (ticket: string): Promise<void> => {
|
||||
export async function resendIpAuthorization(ticket: string): Promise<void> {
|
||||
await http.post({
|
||||
url: Endpoints.AUTH_IP_AUTHORIZATION_RESEND,
|
||||
body: {ticket},
|
||||
headers: withPlatformHeader(),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const subscribeToIpAuthorization = (ticket: string): EventSource => {
|
||||
const base = RuntimeConfigStore.apiEndpoint || '';
|
||||
const url = `${base}${Endpoints.AUTH_IP_AUTHORIZATION_STREAM(ticket)}`;
|
||||
return new EventSource(url);
|
||||
};
|
||||
export interface IpAuthorizationPollResult {
|
||||
completed: boolean;
|
||||
token?: string;
|
||||
user_id?: string;
|
||||
}
|
||||
|
||||
export const initiateDesktopHandoff = async (): Promise<DesktopHandoffInitiateResponse> => {
|
||||
export async function pollIpAuthorization(ticket: string): Promise<IpAuthorizationPollResult> {
|
||||
const response = await http.get<IpAuthorizationPollResult>({
|
||||
url: Endpoints.AUTH_IP_AUTHORIZATION_POLL(ticket),
|
||||
headers: withPlatformHeader(),
|
||||
});
|
||||
return response.body;
|
||||
}
|
||||
|
||||
export async function initiateDesktopHandoff(): Promise<DesktopHandoffInitiateResponse> {
|
||||
const response = await http.post<DesktopHandoffInitiateResponse>({
|
||||
url: Endpoints.AUTH_HANDOFF_INITIATE,
|
||||
skipAuth: true,
|
||||
});
|
||||
return response.body;
|
||||
};
|
||||
}
|
||||
|
||||
export const pollDesktopHandoffStatus = async (
|
||||
code: string,
|
||||
customApiEndpoint?: string,
|
||||
): Promise<DesktopHandoffStatusResponse> => {
|
||||
const url = customApiEndpoint
|
||||
? `${customApiEndpoint}${Endpoints.AUTH_HANDOFF_STATUS(code)}`
|
||||
: Endpoints.AUTH_HANDOFF_STATUS(code);
|
||||
export async function pollDesktopHandoffStatus(code: string): Promise<DesktopHandoffStatusResponse> {
|
||||
const response = await http.get<DesktopHandoffStatusResponse>({
|
||||
url,
|
||||
url: Endpoints.AUTH_HANDOFF_STATUS(code),
|
||||
skipAuth: true,
|
||||
});
|
||||
return response.body;
|
||||
};
|
||||
}
|
||||
|
||||
export const completeDesktopHandoff = async ({
|
||||
export async function completeDesktopHandoff({
|
||||
code,
|
||||
token,
|
||||
userId,
|
||||
@@ -517,15 +521,15 @@ export const completeDesktopHandoff = async ({
|
||||
code: string;
|
||||
token: string;
|
||||
userId: string;
|
||||
}): Promise<void> => {
|
||||
}): Promise<void> {
|
||||
await http.post({
|
||||
url: Endpoints.AUTH_HANDOFF_COMPLETE,
|
||||
body: {code, token, user_id: userId},
|
||||
skipAuth: true,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const startSession = (token: string, options: {startGateway?: boolean} = {}): void => {
|
||||
export function startSession(token: string, options: {startGateway?: boolean} = {}): void {
|
||||
const {startGateway = true} = options;
|
||||
|
||||
logger.info('Starting new session');
|
||||
@@ -535,12 +539,12 @@ export const startSession = (token: string, options: {startGateway?: boolean} =
|
||||
return;
|
||||
}
|
||||
|
||||
ConnectionStore.startSession(token);
|
||||
};
|
||||
GatewayConnectionStore.startSession(token);
|
||||
}
|
||||
|
||||
let sessionStartInProgress = false;
|
||||
|
||||
export const ensureSessionStarted = async (): Promise<void> => {
|
||||
export async function ensureSessionStarted(): Promise<void> {
|
||||
if (sessionStartInProgress) {
|
||||
return;
|
||||
}
|
||||
@@ -553,11 +557,11 @@ export const ensureSessionStarted = async (): Promise<void> => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ConnectionStore.isConnected || ConnectionStore.isConnecting) {
|
||||
if (GatewayConnectionStore.isConnected || GatewayConnectionStore.isConnecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ConnectionStore.socket) {
|
||||
if (GatewayConnectionStore.socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -568,16 +572,16 @@ export const ensureSessionStarted = async (): Promise<void> => {
|
||||
|
||||
const token = AuthenticationStore.authToken;
|
||||
if (token) {
|
||||
ConnectionStore.startSession(token);
|
||||
GatewayConnectionStore.startSession(token);
|
||||
}
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
sessionStartInProgress = false;
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const completeLogin = async ({
|
||||
export async function completeLogin({
|
||||
token,
|
||||
userId,
|
||||
userData,
|
||||
@@ -585,7 +589,7 @@ export const completeLogin = async ({
|
||||
token: string;
|
||||
userId: string;
|
||||
userData?: UserData;
|
||||
}): Promise<void> => {
|
||||
}): Promise<void> {
|
||||
logger.info('Completing login process');
|
||||
|
||||
if (userId && token) {
|
||||
@@ -593,7 +597,25 @@ export const completeLogin = async ({
|
||||
} else {
|
||||
startSession(token, {startGateway: true});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export async function startSso(redirectTo?: string): Promise<{authorization_url: string}> {
|
||||
const response = await http.post<{authorization_url: string}>({
|
||||
url: Endpoints.AUTH_SSO_START,
|
||||
body: {redirect_to: redirectTo},
|
||||
headers: withPlatformHeader(),
|
||||
});
|
||||
return response.body;
|
||||
}
|
||||
|
||||
export async function completeSso({code, state}: {code: string; state: string}): Promise<TokenResponse> {
|
||||
const response = await http.post<TokenResponse>({
|
||||
url: Endpoints.AUTH_SSO_COMPLETE,
|
||||
body: {code, state},
|
||||
headers: withPlatformHeader(),
|
||||
});
|
||||
return response.body;
|
||||
}
|
||||
|
||||
interface SetMfaTicketPayload {
|
||||
ticket: string;
|
||||
@@ -602,26 +624,12 @@ interface SetMfaTicketPayload {
|
||||
webauthn: boolean;
|
||||
}
|
||||
|
||||
export const setMfaTicket = ({ticket, sms, totp, webauthn}: SetMfaTicketPayload): void => {
|
||||
export function setMfaTicket({ticket, sms, totp, webauthn}: SetMfaTicketPayload): void {
|
||||
logger.debug('Setting MFA ticket');
|
||||
AuthenticationStore.handleMfaTicketSet({ticket, sms, totp, webauthn});
|
||||
};
|
||||
}
|
||||
|
||||
export const clearMfaTicket = (): void => {
|
||||
export function clearMfaTicket(): void {
|
||||
logger.debug('Clearing MFA ticket');
|
||||
AuthenticationStore.handleMfaTicketClear();
|
||||
};
|
||||
|
||||
export const redeemBetaCode = async (betaCode: string): Promise<void> => {
|
||||
try {
|
||||
await http.post({
|
||||
url: Endpoints.AUTH_REDEEM_BETA_CODE,
|
||||
body: {beta_code: betaCode},
|
||||
headers: withPlatformHeader(),
|
||||
});
|
||||
logger.info('Beta code redeemed successfully');
|
||||
} catch (error) {
|
||||
logger.error('Beta code redemption failed', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import {type BetaCode, BetaCodeRecord} from '~/records/BetaCodeRecord';
|
||||
import BetaCodeStore from '~/stores/BetaCodeStore';
|
||||
|
||||
const logger = new Logger('BetaCodes');
|
||||
|
||||
interface BetaCodesListResponse {
|
||||
beta_codes: Array<BetaCode>;
|
||||
allowance: number;
|
||||
next_reset_at: string | null;
|
||||
}
|
||||
|
||||
export const fetch = async () => {
|
||||
BetaCodeStore.fetchPending();
|
||||
try {
|
||||
const response = await http.get<BetaCodesListResponse>({url: Endpoints.USER_BETA_CODES, retries: 1});
|
||||
const data = response.body;
|
||||
BetaCodeStore.fetchSuccess(data.beta_codes, data.allowance, data.next_reset_at);
|
||||
return data;
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch beta codes:', error);
|
||||
BetaCodeStore.fetchError();
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const create = async () => {
|
||||
BetaCodeStore.createPending();
|
||||
try {
|
||||
const response = await http.post<BetaCode>(Endpoints.USER_BETA_CODES);
|
||||
const betaCode = new BetaCodeRecord(response.body);
|
||||
BetaCodeStore.createSuccess(betaCode);
|
||||
return betaCode;
|
||||
} catch (error) {
|
||||
logger.error('Failed to create beta code:', error);
|
||||
BetaCodeStore.createError();
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const remove = async (code: string) => {
|
||||
if (!code) {
|
||||
throw new Error('No beta code provided');
|
||||
}
|
||||
BetaCodeStore.deletePending();
|
||||
try {
|
||||
await http.delete({url: Endpoints.USER_BETA_CODE(code)});
|
||||
BetaCodeStore.deleteSuccess(code);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to delete beta code ${code}:`, error);
|
||||
BetaCodeStore.deleteError();
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -17,17 +17,19 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import HttpClient from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import CallInitiatorStore from '@app/stores/CallInitiatorStore';
|
||||
import CallStateStore from '@app/stores/CallStateStore';
|
||||
import ChannelStore from '@app/stores/ChannelStore';
|
||||
import GeoIPStore from '@app/stores/GeoIPStore';
|
||||
import RtcRegionsStore from '@app/stores/RtcRegionsStore';
|
||||
import SoundStore from '@app/stores/SoundStore';
|
||||
import UserStore from '@app/stores/UserStore';
|
||||
import MediaEngineStore from '@app/stores/voice/MediaEngineFacade';
|
||||
import type {RtcRegionResponse} from '@fluxer/schema/src/domains/channel/ChannelSchemas';
|
||||
import {reaction} from 'mobx';
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import HttpClient from '~/lib/HttpClient';
|
||||
import CallInitiatorStore from '~/stores/CallInitiatorStore';
|
||||
import CallStateStore from '~/stores/CallStateStore';
|
||||
import ChannelStore from '~/stores/ChannelStore';
|
||||
import GeoIPStore from '~/stores/GeoIPStore';
|
||||
import SoundStore from '~/stores/SoundStore';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
import MediaEngineStore from '~/stores/voice/MediaEngineFacade';
|
||||
import {SoundType} from '~/utils/SoundUtils';
|
||||
|
||||
interface PendingRing {
|
||||
channelId: string;
|
||||
@@ -35,6 +37,8 @@ interface PendingRing {
|
||||
dispose: () => void;
|
||||
}
|
||||
|
||||
const logger = new Logger('CallActionCreators');
|
||||
|
||||
let pendingRing: PendingRing | null = null;
|
||||
|
||||
export async function checkCallEligibility(channelId: string): Promise<{ringable: boolean}> {
|
||||
@@ -86,7 +90,7 @@ function setupPendingRing(channelId: string, recipients: Array<string>): void {
|
||||
({connected, currentChannelId}) => {
|
||||
if (connected && currentChannelId === channelId && pendingRing?.channelId === channelId) {
|
||||
void ringCallRecipients(channelId, pendingRing.recipients).catch((error) => {
|
||||
console.error('Failed to ring call recipients:', error);
|
||||
logger.error('Failed to ring call recipients:', error);
|
||||
});
|
||||
clearPendingRing();
|
||||
}
|
||||
@@ -107,7 +111,12 @@ export function startCall(channelId: string, silent = false): void {
|
||||
|
||||
CallInitiatorStore.markInitiated(channelId, recipients);
|
||||
|
||||
if (!silent) {
|
||||
if (silent) {
|
||||
clearPendingRing();
|
||||
void ringCallRecipients(channelId, []).catch((error) => {
|
||||
logger.error('Failed to start silent call:', error);
|
||||
});
|
||||
} else {
|
||||
setupPendingRing(channelId, recipients);
|
||||
}
|
||||
|
||||
@@ -121,7 +130,6 @@ export function joinCall(channelId: string): void {
|
||||
}
|
||||
CallStateStore.clearPendingRinging(channelId, [currentUser.id]);
|
||||
SoundStore.stopIncomingRing();
|
||||
SoundStore.playSound(SoundType.UserJoin);
|
||||
void MediaEngineStore.connectToVoiceChannel(null, channelId);
|
||||
}
|
||||
|
||||
@@ -147,7 +155,7 @@ export async function leaveCall(channelId: string): Promise<void> {
|
||||
try {
|
||||
await stopRingingCallRecipients(channelId, toStop);
|
||||
} catch (error) {
|
||||
console.error('Failed to stop ringing pending recipients:', error);
|
||||
logger.error('Failed to stop ringing pending recipients:', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +175,7 @@ export function rejectCall(channelId: string): void {
|
||||
void MediaEngineStore.disconnectFromVoiceChannel('user');
|
||||
}
|
||||
void stopRingingCallRecipients(channelId).catch((error) => {
|
||||
console.error('Failed to stop ringing:', error);
|
||||
logger.error('Failed to stop ringing:', error);
|
||||
});
|
||||
SoundStore.stopIncomingRing();
|
||||
CallInitiatorStore.clearChannel(channelId);
|
||||
@@ -180,7 +188,20 @@ export function ignoreCall(channelId: string): void {
|
||||
}
|
||||
CallStateStore.clearPendingRinging(channelId, [currentUser.id]);
|
||||
void stopRingingCallRecipients(channelId, [currentUser.id]).catch((error) => {
|
||||
console.error('Failed to stop ringing:', error);
|
||||
logger.error('Failed to stop ringing:', error);
|
||||
});
|
||||
SoundStore.stopIncomingRing();
|
||||
}
|
||||
|
||||
export async function fetchCallRegions(channelId: string): Promise<Array<RtcRegionResponse>> {
|
||||
const channel = ChannelStore.getChannel(channelId);
|
||||
if (channel?.isPrivate()) {
|
||||
return RtcRegionsStore.getRegions();
|
||||
}
|
||||
const response = await HttpClient.get<Array<RtcRegionResponse>>({url: Endpoints.CHANNEL_RTC_REGIONS(channelId)});
|
||||
return response.body ?? [];
|
||||
}
|
||||
|
||||
export async function updateCallRegion(channelId: string, region: string | null): Promise<void> {
|
||||
await HttpClient.patch(Endpoints.CHANNEL_CALL(channelId), {region});
|
||||
}
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {ChannelTypes} from '~/Constants';
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import type {Channel} from '~/records/ChannelRecord';
|
||||
import type {Invite} from '~/records/MessageRecord';
|
||||
import ChannelStore from '~/stores/ChannelStore';
|
||||
import InviteStore from '~/stores/InviteStore';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import ChannelStore from '@app/stores/ChannelStore';
|
||||
import InviteStore from '@app/stores/InviteStore';
|
||||
import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
|
||||
import type {Channel} from '@fluxer/schema/src/domains/channel/ChannelSchemas';
|
||||
import type {Invite} from '@fluxer/schema/src/domains/invite/InviteSchemas';
|
||||
|
||||
const logger = new Logger('Channels');
|
||||
|
||||
@@ -34,10 +34,10 @@ export interface ChannelRtcRegion {
|
||||
emoji: string;
|
||||
}
|
||||
|
||||
export const create = async (
|
||||
export async function create(
|
||||
guildId: string,
|
||||
params: Pick<Channel, 'name' | 'url' | 'type' | 'parent_id' | 'bitrate' | 'user_limit'>,
|
||||
) => {
|
||||
) {
|
||||
try {
|
||||
const response = await http.post<Channel>(Endpoints.GUILD_CHANNELS(guildId), params);
|
||||
return response.body;
|
||||
@@ -45,12 +45,12 @@ export const create = async (
|
||||
logger.error('Failed to create channel:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const update = async (
|
||||
export async function update(
|
||||
channelId: string,
|
||||
params: Partial<Pick<Channel, 'name' | 'topic' | 'url' | 'nsfw' | 'icon' | 'owner_id' | 'rtc_region'>>,
|
||||
) => {
|
||||
) {
|
||||
try {
|
||||
const response = await http.patch<Channel>(Endpoints.CHANNEL(channelId), params);
|
||||
return response.body;
|
||||
@@ -58,9 +58,9 @@ export const update = async (
|
||||
logger.error(`Failed to update channel ${channelId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const updateGroupDMNickname = async (channelId: string, userId: string, nickname: string | null) => {
|
||||
export async function updateGroupDMNickname(channelId: string, userId: string, nickname: string | null) {
|
||||
try {
|
||||
const response = await http.patch<Channel>({
|
||||
url: Endpoints.CHANNEL(channelId),
|
||||
@@ -75,13 +75,13 @@ export const updateGroupDMNickname = async (channelId: string, userId: string, n
|
||||
logger.error(`Failed to update nickname for user ${userId} in channel ${channelId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface RemoveChannelOptions {
|
||||
optimistic?: boolean;
|
||||
}
|
||||
|
||||
export const remove = async (channelId: string, silent?: boolean, options?: RemoveChannelOptions) => {
|
||||
export async function remove(channelId: string, silent?: boolean, options?: RemoveChannelOptions) {
|
||||
const channel = ChannelStore.getChannel(channelId);
|
||||
const isPrivateChannel =
|
||||
channel != null && !channel.guildId && (channel.type === ChannelTypes.DM || channel.type === ChannelTypes.GROUP_DM);
|
||||
@@ -92,8 +92,10 @@ export const remove = async (channelId: string, silent?: boolean, options?: Remo
|
||||
}
|
||||
|
||||
try {
|
||||
const url = silent ? `${Endpoints.CHANNEL(channelId)}?silent=true` : Endpoints.CHANNEL(channelId);
|
||||
await http.delete({url});
|
||||
await http.delete({
|
||||
url: Endpoints.CHANNEL(channelId),
|
||||
query: silent ? {silent: true} : undefined,
|
||||
});
|
||||
if (shouldOptimisticallyRemove) {
|
||||
ChannelStore.clearOptimisticallyRemovedChannel(channelId);
|
||||
}
|
||||
@@ -104,12 +106,12 @@ export const remove = async (channelId: string, silent?: boolean, options?: Remo
|
||||
logger.error(`Failed to delete channel ${channelId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const updatePermissionOverwrites = async (
|
||||
export async function updatePermissionOverwrites(
|
||||
channelId: string,
|
||||
permissionOverwrites: Array<{id: string; type: 0 | 1; allow: string; deny: string}>,
|
||||
) => {
|
||||
) {
|
||||
try {
|
||||
const response = await http.patch<Channel>({
|
||||
url: Endpoints.CHANNEL(channelId),
|
||||
@@ -120,9 +122,9 @@ export const updatePermissionOverwrites = async (
|
||||
logger.error(`Failed to update permission overwrites for channel ${channelId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const fetchChannelInvites = async (channelId: string): Promise<Array<Invite>> => {
|
||||
export async function fetchChannelInvites(channelId: string): Promise<Array<Invite>> {
|
||||
try {
|
||||
InviteStore.handleChannelInvitesFetchPending(channelId);
|
||||
const response = await http.get<Array<Invite>>({url: Endpoints.CHANNEL_INVITES(channelId)});
|
||||
@@ -134,9 +136,9 @@ export const fetchChannelInvites = async (channelId: string): Promise<Array<Invi
|
||||
InviteStore.handleChannelInvitesFetchError(channelId);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const fetchRtcRegions = async (channelId: string): Promise<Array<ChannelRtcRegion>> => {
|
||||
export async function fetchRtcRegions(channelId: string): Promise<Array<ChannelRtcRegion>> {
|
||||
try {
|
||||
const response = await http.get<Array<ChannelRtcRegion>>({url: Endpoints.CHANNEL_RTC_REGIONS(channelId)});
|
||||
return response.body ?? [];
|
||||
@@ -144,4 +146,4 @@ export const fetchRtcRegions = async (channelId: string): Promise<Array<ChannelR
|
||||
logger.error(`Failed to fetch RTC regions for channel ${channelId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,15 +17,18 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {modal} from '~/actions/ModalActionCreators';
|
||||
import {APIErrorCodes} from '~/Constants';
|
||||
import {PinFailedModal, type PinFailureReason} from '~/components/alerts/PinFailedModal';
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http, {type HttpError} from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import type {Message} from '~/records/MessageRecord';
|
||||
import ChannelPinsStore from '~/stores/ChannelPinsStore';
|
||||
import * as ModalActionCreators from '@app/actions/ModalActionCreators';
|
||||
import {modal} from '@app/actions/ModalActionCreators';
|
||||
import {PinFailedModal, type PinFailureReason} from '@app/components/alerts/PinFailedModal';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import type {HttpError} from '@app/lib/HttpError';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import ChannelPinsStore from '@app/stores/ChannelPinsStore';
|
||||
import ChannelStore from '@app/stores/ChannelStore';
|
||||
import GuildNSFWAgreeStore from '@app/stores/GuildNSFWAgreeStore';
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import type {Message} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
|
||||
|
||||
interface ApiErrorBody {
|
||||
code?: string;
|
||||
@@ -40,6 +43,14 @@ const getApiErrorCode = (error: HttpError): string | undefined => {
|
||||
const logger = new Logger('Pins');
|
||||
const PIN_PAGE_SIZE = 25;
|
||||
|
||||
const shouldBlockPinsFetch = (channelId: string): boolean => {
|
||||
const channel = ChannelStore.getChannel(channelId);
|
||||
if (!channel || channel.isPrivate()) {
|
||||
return false;
|
||||
}
|
||||
return GuildNSFWAgreeStore.shouldShowGate({channelId: channel.id, guildId: channel.guildId ?? null});
|
||||
};
|
||||
|
||||
interface ChannelPinResponse {
|
||||
message: Message;
|
||||
pinned_at: string;
|
||||
@@ -49,7 +60,12 @@ interface ChannelPinsPayload {
|
||||
has_more: boolean;
|
||||
}
|
||||
|
||||
export const fetch = async (channelId: string) => {
|
||||
export async function fetch(channelId: string) {
|
||||
if (shouldBlockPinsFetch(channelId)) {
|
||||
ChannelPinsStore.handleChannelPinsFetchSuccess(channelId, [], false);
|
||||
return [];
|
||||
}
|
||||
|
||||
ChannelPinsStore.handleFetchPending(channelId);
|
||||
try {
|
||||
const response = await http.get<ChannelPinsPayload>({
|
||||
@@ -64,9 +80,14 @@ export const fetch = async (channelId: string) => {
|
||||
ChannelPinsStore.handleChannelPinsFetchError(channelId);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadMore(channelId: string): Promise<Array<Message>> {
|
||||
if (shouldBlockPinsFetch(channelId)) {
|
||||
ChannelPinsStore.handleChannelPinsFetchSuccess(channelId, [], false);
|
||||
return [];
|
||||
}
|
||||
|
||||
export const loadMore = async (channelId: string): Promise<Array<Message>> => {
|
||||
if (!ChannelPinsStore.getHasMore(channelId) || ChannelPinsStore.getIsLoading(channelId)) {
|
||||
return [];
|
||||
}
|
||||
@@ -94,7 +115,7 @@ export const loadMore = async (channelId: string): Promise<Array<Message>> => {
|
||||
ChannelPinsStore.handleChannelPinsFetchError(channelId);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const getFailureReason = (error: HttpError): PinFailureReason => {
|
||||
const errorCode = getApiErrorCode(error);
|
||||
@@ -104,7 +125,7 @@ const getFailureReason = (error: HttpError): PinFailureReason => {
|
||||
return 'generic';
|
||||
};
|
||||
|
||||
export const pin = async (channelId: string, messageId: string): Promise<void> => {
|
||||
export async function pin(channelId: string, messageId: string): Promise<void> {
|
||||
try {
|
||||
await http.put({url: Endpoints.CHANNEL_PIN(channelId, messageId)});
|
||||
logger.debug(`Pinned message ${messageId} in channel ${channelId}`);
|
||||
@@ -113,9 +134,9 @@ export const pin = async (channelId: string, messageId: string): Promise<void> =
|
||||
const reason = getFailureReason(error as HttpError);
|
||||
ModalActionCreators.push(modal(() => <PinFailedModal reason={reason} />));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const unpin = async (channelId: string, messageId: string): Promise<void> => {
|
||||
export async function unpin(channelId: string, messageId: string): Promise<void> {
|
||||
try {
|
||||
await http.delete({url: Endpoints.CHANNEL_PIN(channelId, messageId)});
|
||||
logger.debug(`Unpinned message ${messageId} from channel ${channelId}`);
|
||||
@@ -124,4 +145,4 @@ export const unpin = async (channelId: string, messageId: string): Promise<void>
|
||||
const reason = getFailureReason(error as HttpError);
|
||||
ModalActionCreators.push(modal(() => <PinFailedModal isUnpin reason={reason} />));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {GuildStickerRecord} from '~/records/GuildStickerRecord';
|
||||
import ChannelStickerStore from '~/stores/ChannelStickerStore';
|
||||
import type {GuildStickerRecord} from '@app/records/GuildStickerRecord';
|
||||
import ChannelStickerStore from '@app/stores/ChannelStickerStore';
|
||||
|
||||
export function setPendingSticker(channelId: string, sticker: GuildStickerRecord): void {
|
||||
ChannelStickerStore.setPendingSticker(channelId, sticker);
|
||||
|
||||
196
fluxer_app/src/actions/ConnectionActionCreators.tsx
Normal file
196
fluxer_app/src/actions/ConnectionActionCreators.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as ToastActionCreators from '@app/actions/ToastActionCreators';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import UserConnectionStore from '@app/stores/UserConnectionStore';
|
||||
import * as ApiErrorUtils from '@app/utils/ApiErrorUtils';
|
||||
import type {ConnectionType} from '@fluxer/constants/src/ConnectionConstants';
|
||||
import type {
|
||||
ConnectionListResponse,
|
||||
ConnectionResponse,
|
||||
ConnectionVerificationResponse,
|
||||
CreateConnectionRequest,
|
||||
ReorderConnectionsRequest,
|
||||
UpdateConnectionRequest,
|
||||
VerifyAndCreateConnectionRequest,
|
||||
} from '@fluxer/schema/src/domains/connection/ConnectionSchemas';
|
||||
import type {I18n, MessageDescriptor} from '@lingui/core';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
|
||||
const logger = new Logger('Connections');
|
||||
|
||||
function showErrorToast(i18n: I18n, error: unknown, fallbackMessage: MessageDescriptor): void {
|
||||
const errorMessage = ApiErrorUtils.getApiErrorMessage(error);
|
||||
|
||||
ToastActionCreators.createToast({
|
||||
type: 'error',
|
||||
children: errorMessage ?? i18n._(fallbackMessage),
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchConnections(): Promise<void> {
|
||||
try {
|
||||
const response = await http.get<ConnectionListResponse>(Endpoints.CONNECTIONS);
|
||||
UserConnectionStore.setConnections(response.body);
|
||||
logger.debug('Successfully fetched connections');
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch connections:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function initiateConnection(
|
||||
i18n: I18n,
|
||||
type: ConnectionType,
|
||||
identifier: string,
|
||||
): Promise<ConnectionVerificationResponse> {
|
||||
try {
|
||||
const payload: CreateConnectionRequest = {
|
||||
type,
|
||||
identifier,
|
||||
};
|
||||
|
||||
const response = await http.post<ConnectionVerificationResponse>(Endpoints.CONNECTIONS, payload);
|
||||
logger.debug(`Successfully initiated connection: ${type}/${identifier}`);
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to initiate connection ${type}/${identifier}:`, error);
|
||||
showErrorToast(i18n, error, msg`Failed to initiate connection`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function authorizeBlueskyConnection(i18n: I18n, handle: string): Promise<void> {
|
||||
try {
|
||||
const response = await http.post<{authorize_url: string}>(Endpoints.BLUESKY_AUTHORIZE, {handle});
|
||||
window.open(response.body.authorize_url, '_blank');
|
||||
} catch (error) {
|
||||
logger.error(`Failed to start Bluesky OAuth flow for ${handle}:`, error);
|
||||
showErrorToast(i18n, error, msg`Failed to start Bluesky authorisation`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifyAndCreateConnection(
|
||||
i18n: I18n,
|
||||
initiationToken: string,
|
||||
visibilityFlags?: number,
|
||||
): Promise<ConnectionResponse> {
|
||||
try {
|
||||
const payload: VerifyAndCreateConnectionRequest = {
|
||||
initiation_token: initiationToken,
|
||||
visibility_flags: visibilityFlags,
|
||||
};
|
||||
|
||||
const response = await http.post<ConnectionResponse>(Endpoints.CONNECTIONS_VERIFY_AND_CREATE, payload);
|
||||
UserConnectionStore.addConnection(response.body);
|
||||
|
||||
ToastActionCreators.createToast({
|
||||
type: 'success',
|
||||
children: i18n._(msg`Connection verified`),
|
||||
});
|
||||
|
||||
logger.debug('Successfully verified and created connection');
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
logger.error('Failed to verify and create connection:', error);
|
||||
showErrorToast(i18n, error, msg`Failed to verify connection`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateConnection(
|
||||
i18n: I18n,
|
||||
type: string,
|
||||
connectionId: string,
|
||||
patch: UpdateConnectionRequest,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await http.patch(Endpoints.CONNECTION(type, connectionId), patch);
|
||||
UserConnectionStore.updateConnection(connectionId, patch);
|
||||
|
||||
ToastActionCreators.createToast({
|
||||
type: 'success',
|
||||
children: i18n._(msg`Connection updated`),
|
||||
});
|
||||
|
||||
logger.debug(`Successfully updated connection: ${type}/${connectionId}`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to update connection ${type}/${connectionId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteConnection(i18n: I18n, type: string, connectionId: string): Promise<void> {
|
||||
try {
|
||||
await http.delete({url: Endpoints.CONNECTION(type, connectionId)});
|
||||
UserConnectionStore.removeConnection(connectionId);
|
||||
|
||||
ToastActionCreators.createToast({
|
||||
type: 'success',
|
||||
children: i18n._(msg`Connection removed`),
|
||||
});
|
||||
|
||||
logger.debug(`Successfully deleted connection: ${type}/${connectionId}`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to delete connection ${type}/${connectionId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifyConnection(i18n: I18n, type: string, connectionId: string): Promise<void> {
|
||||
try {
|
||||
const response = await http.post<ConnectionResponse>(Endpoints.CONNECTION_VERIFY(type, connectionId), {});
|
||||
UserConnectionStore.updateConnection(connectionId, response.body);
|
||||
|
||||
ToastActionCreators.createToast({
|
||||
type: 'success',
|
||||
children: i18n._(msg`Connection verified`),
|
||||
});
|
||||
|
||||
logger.debug(`Successfully verified connection: ${type}/${connectionId}`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to verify connection ${type}/${connectionId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function reorderConnections(i18n: I18n, connectionIds: Array<string>): Promise<void> {
|
||||
try {
|
||||
const payload: ReorderConnectionsRequest = {
|
||||
connection_ids: connectionIds,
|
||||
};
|
||||
|
||||
await http.patch(Endpoints.CONNECTIONS_REORDER, payload);
|
||||
await fetchConnections();
|
||||
|
||||
ToastActionCreators.createToast({
|
||||
type: 'success',
|
||||
children: i18n._(msg`Connections reordered`),
|
||||
});
|
||||
|
||||
logger.debug('Successfully reordered connections');
|
||||
} catch (error) {
|
||||
logger.error('Failed to reorder connections:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,9 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {ContextMenu, ContextMenuConfig, ContextMenuTargetElement} from '@app/stores/ContextMenuStore';
|
||||
import ContextMenuStore from '@app/stores/ContextMenuStore';
|
||||
import type React from 'react';
|
||||
import type {ContextMenu, ContextMenuConfig, ContextMenuTargetElement} from '~/stores/ContextMenuStore';
|
||||
import ContextMenuStore from '~/stores/ContextMenuStore';
|
||||
|
||||
const nativeContextMenuTarget: ContextMenuTargetElement = {
|
||||
tagName: 'ReactNativeContextMenu',
|
||||
@@ -47,18 +47,18 @@ const toHTMLElement = (value: unknown): HTMLElement | null => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export const close = (): void => {
|
||||
export function close(): void {
|
||||
ContextMenuStore.close();
|
||||
};
|
||||
}
|
||||
|
||||
type RenderFn = (props: {onClose: () => void}) => React.ReactNode;
|
||||
|
||||
export const openAtPoint = (
|
||||
export function openAtPoint(
|
||||
point: {x: number; y: number},
|
||||
render: RenderFn,
|
||||
config?: ContextMenuConfig,
|
||||
target: ContextMenuTargetElement = nativeContextMenuTarget,
|
||||
): void => {
|
||||
): void {
|
||||
const contextMenu: ContextMenu = {
|
||||
id: makeId('context-menu'),
|
||||
target: {x: point.x, y: point.y, target},
|
||||
@@ -67,22 +67,22 @@ export const openAtPoint = (
|
||||
};
|
||||
|
||||
ContextMenuStore.open(contextMenu);
|
||||
};
|
||||
}
|
||||
|
||||
export const openForElement = (
|
||||
export function openForElement(
|
||||
element: HTMLElement,
|
||||
render: RenderFn,
|
||||
options?: {point?: {x: number; y: number}; config?: ContextMenuConfig},
|
||||
): void => {
|
||||
): void {
|
||||
const point = options?.point ?? getViewportCenterForElement(element);
|
||||
openAtPoint(point, render, options?.config, element);
|
||||
};
|
||||
}
|
||||
|
||||
export const openFromEvent = (
|
||||
export function openFromEvent(
|
||||
event: React.MouseEvent | MouseEvent,
|
||||
render: RenderFn,
|
||||
config?: ContextMenuConfig,
|
||||
): void => {
|
||||
): void {
|
||||
event.preventDefault?.();
|
||||
event.stopPropagation?.();
|
||||
|
||||
@@ -103,13 +103,13 @@ export const openFromEvent = (
|
||||
: {x: 0, y: 0};
|
||||
|
||||
openAtPoint(point, render, config, anchor ?? nativeContextMenuTarget);
|
||||
};
|
||||
}
|
||||
|
||||
export const openFromElementBottomRight = (
|
||||
export function openFromElementBottomRight(
|
||||
event: React.MouseEvent | MouseEvent,
|
||||
render: RenderFn,
|
||||
config?: ContextMenuConfig,
|
||||
): void => {
|
||||
): void {
|
||||
event.preventDefault?.();
|
||||
event.stopPropagation?.();
|
||||
|
||||
@@ -128,9 +128,34 @@ export const openFromElementBottomRight = (
|
||||
const point = {x: rect.right + scrollX, y: rect.bottom + scrollY + 4};
|
||||
|
||||
openAtPoint(point, render, {align: 'top-right', ...config}, anchor);
|
||||
};
|
||||
}
|
||||
|
||||
export const openNativeContextMenu = (render: RenderFn, config?: ContextMenu['config']): void => {
|
||||
export function openFromElementTopLeft(
|
||||
event: React.MouseEvent | MouseEvent,
|
||||
render: RenderFn,
|
||||
config?: ContextMenuConfig,
|
||||
): void {
|
||||
event.preventDefault?.();
|
||||
event.stopPropagation?.();
|
||||
|
||||
const currentTarget = 'currentTarget' in event ? toHTMLElement(event.currentTarget) : null;
|
||||
const target = 'target' in event ? toHTMLElement(event.target) : null;
|
||||
const anchor = currentTarget ?? target;
|
||||
|
||||
if (!anchor) {
|
||||
openFromEvent(event, render, config);
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = anchor.getBoundingClientRect();
|
||||
const scrollX = window.scrollX || window.pageXOffset || 0;
|
||||
const scrollY = window.scrollY || window.pageYOffset || 0;
|
||||
const point = {x: rect.left + scrollX, y: rect.top + scrollY};
|
||||
|
||||
openAtPoint(point, render, {align: 'bottom-left', ...config}, anchor);
|
||||
}
|
||||
|
||||
export function openNativeContextMenu(render: RenderFn, config?: ContextMenu['config']): void {
|
||||
const contextMenu: ContextMenu = {
|
||||
id: makeId('native-context-menu'),
|
||||
target: {
|
||||
@@ -146,4 +171,4 @@ export const openNativeContextMenu = (render: RenderFn, config?: ContextMenu['co
|
||||
};
|
||||
|
||||
ContextMenuStore.open(contextMenu);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,21 +17,24 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {ChannelTypes} from '~/Constants';
|
||||
import {ComponentDispatch} from '~/lib/ComponentDispatch';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import {type Channel, ChannelRecord} from '~/records/ChannelRecord';
|
||||
import {type UserPartial, UserRecord} from '~/records/UserRecord';
|
||||
import DeveloperOptionsStore, {type DeveloperOptionsState} from '~/stores/DeveloperOptionsStore';
|
||||
import MockIncomingCallStore from '~/stores/MockIncomingCallStore';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
import {ComponentDispatch} from '@app/lib/ComponentDispatch';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import {ChannelRecord} from '@app/records/ChannelRecord';
|
||||
import {UserRecord} from '@app/records/UserRecord';
|
||||
import DeveloperOptionsStore, {type DeveloperOptionsState} from '@app/stores/DeveloperOptionsStore';
|
||||
import MockIncomingCallStore from '@app/stores/MockIncomingCallStore';
|
||||
import UserStore from '@app/stores/UserStore';
|
||||
import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
|
||||
import type {Channel} from '@fluxer/schema/src/domains/channel/ChannelSchemas';
|
||||
import type {UserPartial} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
|
||||
import {generateSnowflake} from '@fluxer/snowflake/src/Snowflake';
|
||||
|
||||
const logger = new Logger('DeveloperOptions');
|
||||
|
||||
export const updateOption = <K extends keyof DeveloperOptionsState>(key: K, value: DeveloperOptionsState[K]): void => {
|
||||
export function updateOption<K extends keyof DeveloperOptionsState>(key: K, value: DeveloperOptionsState[K]): void {
|
||||
logger.debug(`Updating developer option: ${String(key)} = ${value}`);
|
||||
DeveloperOptionsStore.updateOption(key, value);
|
||||
};
|
||||
}
|
||||
|
||||
export function setAttachmentMock(
|
||||
attachmentId: string,
|
||||
@@ -59,15 +62,15 @@ export function triggerMockIncomingCall(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
const timestamp = Date.now() - 1420070400000;
|
||||
const random = Math.floor(Math.random() * 4096);
|
||||
const mockChannelId = ((timestamp << 22) | random).toString();
|
||||
const mockChannelId = generateSnowflake().toString();
|
||||
|
||||
const initiatorPartial: UserPartial = {
|
||||
id: currentUser.id,
|
||||
username: currentUser.username,
|
||||
discriminator: currentUser.discriminator,
|
||||
global_name: currentUser.globalName,
|
||||
avatar: currentUser.avatar ?? null,
|
||||
avatar_color: currentUser.avatarColor ?? null,
|
||||
flags: currentUser.flags ?? 0,
|
||||
};
|
||||
|
||||
|
||||
@@ -17,24 +17,22 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import DimensionStore from '~/stores/DimensionStore';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import DimensionStore from '@app/stores/DimensionStore';
|
||||
|
||||
const logger = new Logger('DimensionActions');
|
||||
|
||||
type GuildId = string;
|
||||
|
||||
export const updateChannelListScroll = (guildId: GuildId, scrollTop: number): void => {
|
||||
export function updateChannelListScroll(guildId: string, scrollTop: number): void {
|
||||
logger.debug(`Updating channel list scroll: guildId=${guildId}, scrollTop=${scrollTop}`);
|
||||
DimensionStore.updateGuildDimensions(guildId, scrollTop, undefined);
|
||||
};
|
||||
}
|
||||
|
||||
export const clearChannelListScrollTo = (guildId: GuildId): void => {
|
||||
export function clearChannelListScrollTo(guildId: string): void {
|
||||
logger.debug(`Clearing channel list scroll target: guildId=${guildId}`);
|
||||
DimensionStore.updateGuildDimensions(guildId, undefined, null);
|
||||
};
|
||||
}
|
||||
|
||||
export const updateGuildListScroll = (scrollTop: number): void => {
|
||||
export function updateGuildListScroll(scrollTop: number): void {
|
||||
logger.debug(`Updating guild list scroll: scrollTop=${scrollTop}`);
|
||||
DimensionStore.updateGuildListDimensions(scrollTop);
|
||||
};
|
||||
}
|
||||
|
||||
87
fluxer_app/src/actions/DiscoveryActionCreators.tsx
Normal file
87
fluxer_app/src/actions/DiscoveryActionCreators.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
|
||||
const logger = new Logger('Discovery');
|
||||
|
||||
export interface DiscoveryGuild {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string | null;
|
||||
description: string | null;
|
||||
category_id: number;
|
||||
member_count: number;
|
||||
online_count: number;
|
||||
features: Array<string>;
|
||||
verification_level: number;
|
||||
}
|
||||
|
||||
interface DiscoverySearchResponse {
|
||||
guilds: Array<DiscoveryGuild>;
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface DiscoveryCategory {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export async function searchGuilds(params: {
|
||||
query?: string;
|
||||
category?: number;
|
||||
sort_by?: string;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}): Promise<DiscoverySearchResponse> {
|
||||
const query: Record<string, string> = {
|
||||
limit: String(params.limit),
|
||||
offset: String(params.offset),
|
||||
};
|
||||
if (params.query) {
|
||||
query.query = params.query;
|
||||
}
|
||||
if (params.category !== undefined) {
|
||||
query.category = String(params.category);
|
||||
}
|
||||
if (params.sort_by) {
|
||||
query.sort_by = params.sort_by;
|
||||
}
|
||||
const response = await http.get<DiscoverySearchResponse>({
|
||||
url: Endpoints.DISCOVERY_GUILDS,
|
||||
query,
|
||||
});
|
||||
return response.body;
|
||||
}
|
||||
|
||||
export async function getCategories(): Promise<Array<DiscoveryCategory>> {
|
||||
const response = await http.get<Array<DiscoveryCategory>>({
|
||||
url: Endpoints.DISCOVERY_CATEGORIES,
|
||||
});
|
||||
return response.body;
|
||||
}
|
||||
|
||||
export async function joinGuild(guildId: string): Promise<void> {
|
||||
await http.post({
|
||||
url: Endpoints.DISCOVERY_JOIN(guildId),
|
||||
});
|
||||
logger.info('Joined guild via discovery', {guildId});
|
||||
}
|
||||
@@ -17,17 +17,17 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import DraftStore from '~/stores/DraftStore';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import DraftStore from '@app/stores/DraftStore';
|
||||
|
||||
const logger = new Logger('Draft');
|
||||
|
||||
export const createDraft = (channelId: string, content: string): void => {
|
||||
export function createDraft(channelId: string, content: string): void {
|
||||
logger.debug(`Creating draft for channel ${channelId}`);
|
||||
DraftStore.createDraft(channelId, content);
|
||||
};
|
||||
}
|
||||
|
||||
export const deleteDraft = (channelId: string): void => {
|
||||
export function deleteDraft(channelId: string): void {
|
||||
logger.debug(`Deleting draft for channel ${channelId}`);
|
||||
DraftStore.deleteDraft(channelId);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import EmojiStore from '~/stores/EmojiStore';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import EmojiStore from '@app/stores/EmojiStore';
|
||||
|
||||
const logger = new Logger('Emoji');
|
||||
|
||||
export const setSkinTone = (skinTone: string): void => {
|
||||
export function setSkinTone(skinTone: string): void {
|
||||
logger.debug(`Setting emoji skin tone: ${skinTone}`);
|
||||
EmojiStore.setSkinTone(skinTone);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,21 +17,21 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import EmojiPickerStore from '~/stores/EmojiPickerStore';
|
||||
import type {Emoji} from '~/stores/EmojiStore';
|
||||
import EmojiPickerStore from '@app/stores/EmojiPickerStore';
|
||||
import type {FlatEmoji} from '@app/types/EmojiTypes';
|
||||
|
||||
function getEmojiKey(emoji: Emoji): string {
|
||||
function getEmojiKey(emoji: FlatEmoji): string {
|
||||
if (emoji.id) {
|
||||
return `custom:${emoji.guildId}:${emoji.id}`;
|
||||
}
|
||||
return `unicode:${emoji.uniqueName}`;
|
||||
}
|
||||
|
||||
export function trackEmojiUsage(emoji: Emoji): void {
|
||||
export function trackEmojiUsage(emoji: FlatEmoji): void {
|
||||
EmojiPickerStore.trackEmojiUsage(getEmojiKey(emoji));
|
||||
}
|
||||
|
||||
export function toggleFavorite(emoji: Emoji): void {
|
||||
export function toggleFavorite(emoji: FlatEmoji): void {
|
||||
EmojiPickerStore.toggleFavorite(getEmojiKey(emoji));
|
||||
}
|
||||
|
||||
|
||||
@@ -17,28 +17,28 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {ExpressionPickerTabType} from '~/components/popouts/ExpressionPickerPopout';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import ExpressionPickerStore from '~/stores/ExpressionPickerStore';
|
||||
import type {ExpressionPickerTabType} from '@app/components/popouts/ExpressionPickerPopout';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import ExpressionPickerStore from '@app/stores/ExpressionPickerStore';
|
||||
|
||||
const logger = new Logger('ExpressionPicker');
|
||||
|
||||
export const open = (channelId: string, tab?: ExpressionPickerTabType): void => {
|
||||
export function open(channelId: string, tab?: ExpressionPickerTabType): void {
|
||||
logger.debug(`Opening expression picker for channel ${channelId}, tab: ${tab}`);
|
||||
ExpressionPickerStore.open(channelId, tab);
|
||||
};
|
||||
}
|
||||
|
||||
export const close = (): void => {
|
||||
export function close(): void {
|
||||
logger.debug('Closing expression picker');
|
||||
ExpressionPickerStore.close();
|
||||
};
|
||||
}
|
||||
|
||||
export const toggle = (channelId: string, tab: ExpressionPickerTabType): void => {
|
||||
export function toggle(channelId: string, tab: ExpressionPickerTabType): void {
|
||||
logger.debug(`Toggling expression picker for channel ${channelId}, tab: ${tab}`);
|
||||
ExpressionPickerStore.toggle(channelId, tab);
|
||||
};
|
||||
}
|
||||
|
||||
export const setTab = (tab: ExpressionPickerTabType): void => {
|
||||
export function setTab(tab: ExpressionPickerTabType): void {
|
||||
logger.debug(`Setting expression picker tab to: ${tab}`);
|
||||
ExpressionPickerStore.setTab(tab);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,30 +17,24 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as ModalActionCreators from '@app/actions/ModalActionCreators';
|
||||
import {modal} from '@app/actions/ModalActionCreators';
|
||||
import * as ToastActionCreators from '@app/actions/ToastActionCreators';
|
||||
import {MaxFavoriteMemesModal} from '@app/components/alerts/MaxFavoriteMemesModal';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import type {FavoriteMeme} from '@app/records/FavoriteMemeRecord';
|
||||
import FavoriteMemeStore from '@app/stores/FavoriteMemeStore';
|
||||
import {getApiErrorCode} from '@app/utils/ApiErrorUtils';
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {ME} from '@fluxer/constants/src/AppConstants';
|
||||
import type {I18n} from '@lingui/core';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {modal} from '~/actions/ModalActionCreators';
|
||||
import * as ToastActionCreators from '~/actions/ToastActionCreators';
|
||||
import {APIErrorCodes, ME} from '~/Constants';
|
||||
import {MaxFavoriteMemesModal} from '~/components/alerts/MaxFavoriteMemesModal';
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import type {FavoriteMeme} from '~/records/FavoriteMemeRecord';
|
||||
import FavoriteMemeStore from '~/stores/FavoriteMemeStore';
|
||||
|
||||
const logger = new Logger('FavoriteMemes');
|
||||
|
||||
const getApiErrorCode = (error: unknown): string | undefined => {
|
||||
if (typeof error === 'object' && error !== null && 'code' in error) {
|
||||
const {code} = error as {code?: unknown};
|
||||
return typeof code === 'string' ? code : undefined;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const createFavoriteMeme = async (
|
||||
export async function createFavoriteMeme(
|
||||
i18n: I18n,
|
||||
{
|
||||
channelId,
|
||||
@@ -59,7 +53,7 @@ export const createFavoriteMeme = async (
|
||||
altText?: string;
|
||||
tags?: Array<string>;
|
||||
},
|
||||
): Promise<void> => {
|
||||
): Promise<void> {
|
||||
try {
|
||||
await http.post<FavoriteMeme>(Endpoints.CHANNEL_MESSAGE_FAVORITE_MEMES(channelId, messageId), {
|
||||
attachment_id: attachmentId,
|
||||
@@ -84,31 +78,34 @@ export const createFavoriteMeme = async (
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const createFavoriteMemeFromUrl = async (
|
||||
export async function createFavoriteMemeFromUrl(
|
||||
i18n: I18n,
|
||||
{
|
||||
url,
|
||||
name,
|
||||
altText,
|
||||
tags,
|
||||
tenorId,
|
||||
klipySlug,
|
||||
tenorSlugId,
|
||||
}: {
|
||||
url: string;
|
||||
name: string;
|
||||
altText?: string;
|
||||
tags?: Array<string>;
|
||||
tenorId?: string;
|
||||
klipySlug?: string;
|
||||
tenorSlugId?: string;
|
||||
},
|
||||
): Promise<void> => {
|
||||
): Promise<void> {
|
||||
try {
|
||||
await http.post<FavoriteMeme>(Endpoints.USER_FAVORITE_MEMES(ME), {
|
||||
url,
|
||||
name,
|
||||
alt_text: altText,
|
||||
tags,
|
||||
tenor_id: tenorId,
|
||||
klipy_slug: klipySlug,
|
||||
tenor_slug_id: tenorSlugId,
|
||||
});
|
||||
|
||||
ToastActionCreators.createToast({
|
||||
@@ -126,9 +123,9 @@ export const createFavoriteMemeFromUrl = async (
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const updateFavoriteMeme = async (
|
||||
export async function updateFavoriteMeme(
|
||||
i18n: I18n,
|
||||
{
|
||||
memeId,
|
||||
@@ -141,7 +138,7 @@ export const updateFavoriteMeme = async (
|
||||
altText?: string | null;
|
||||
tags?: Array<string>;
|
||||
},
|
||||
): Promise<void> => {
|
||||
): Promise<void> {
|
||||
try {
|
||||
const response = await http.patch<FavoriteMeme>(Endpoints.USER_FAVORITE_MEME(ME, memeId), {
|
||||
name,
|
||||
@@ -160,9 +157,9 @@ export const updateFavoriteMeme = async (
|
||||
logger.error(`Failed to update favorite meme ${memeId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const deleteFavoriteMeme = async (i18n: I18n, memeId: string): Promise<void> => {
|
||||
export async function deleteFavoriteMeme(i18n: I18n, memeId: string): Promise<void> {
|
||||
try {
|
||||
await http.delete({url: Endpoints.USER_FAVORITE_MEME(ME, memeId)});
|
||||
|
||||
@@ -177,4 +174,4 @@ export const deleteFavoriteMeme = async (i18n: I18n, memeId: string): Promise<vo
|
||||
logger.error(`Failed to delete favorite meme ${memeId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,15 +17,15 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as AccessibilityActionCreators from '@app/actions/AccessibilityActionCreators';
|
||||
import * as ModalActionCreators from '@app/actions/ModalActionCreators';
|
||||
import {modal} from '@app/actions/ModalActionCreators';
|
||||
import {ConfirmModal} from '@app/components/modals/ConfirmModal';
|
||||
import type {I18n} from '@lingui/core';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import {Trans} from '@lingui/react/macro';
|
||||
import * as AccessibilityActionCreators from '~/actions/AccessibilityActionCreators';
|
||||
import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {modal} from '~/actions/ModalActionCreators';
|
||||
import {ConfirmModal} from '~/components/modals/ConfirmModal';
|
||||
|
||||
export const confirmHideFavorites = (onConfirm: (() => void) | undefined, i18n: I18n): void => {
|
||||
export function confirmHideFavorites(onConfirm: (() => void) | undefined, i18n: I18n): void {
|
||||
ModalActionCreators.push(
|
||||
modal(() => (
|
||||
<ConfirmModal
|
||||
@@ -48,4 +48,4 @@ export const confirmHideFavorites = (onConfirm: (() => void) | undefined, i18n:
|
||||
/>
|
||||
)),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
174
fluxer_app/src/actions/GifActionCreators.tsx
Normal file
174
fluxer_app/src/actions/GifActionCreators.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import RuntimeConfigStore, {type GifProvider} from '@app/stores/RuntimeConfigStore';
|
||||
import * as LocaleUtils from '@app/utils/LocaleUtils';
|
||||
|
||||
const logger = new Logger('GIF');
|
||||
|
||||
const getLocale = (): string => LocaleUtils.getCurrentLocale();
|
||||
|
||||
export interface Gif {
|
||||
id: string;
|
||||
title: string;
|
||||
url: string;
|
||||
src: string;
|
||||
proxy_src: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface GifCategory {
|
||||
name: string;
|
||||
src: string;
|
||||
proxy_src: string;
|
||||
}
|
||||
|
||||
export interface GifFeatured {
|
||||
categories: Array<GifCategory>;
|
||||
gifs: Array<Gif>;
|
||||
}
|
||||
|
||||
function getProvider(): GifProvider {
|
||||
return RuntimeConfigStore.gifProvider;
|
||||
}
|
||||
|
||||
function getProviderEndpoints(provider: GifProvider): {
|
||||
search: string;
|
||||
featured: string;
|
||||
trending: string;
|
||||
registerShare: string;
|
||||
suggest: string;
|
||||
} {
|
||||
if (provider === 'tenor') {
|
||||
return {
|
||||
search: Endpoints.TENOR_SEARCH,
|
||||
featured: Endpoints.TENOR_FEATURED,
|
||||
trending: Endpoints.TENOR_TRENDING_GIFS,
|
||||
registerShare: Endpoints.TENOR_REGISTER_SHARE,
|
||||
suggest: Endpoints.TENOR_SUGGEST,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
search: Endpoints.KLIPY_SEARCH,
|
||||
featured: Endpoints.KLIPY_FEATURED,
|
||||
trending: Endpoints.KLIPY_TRENDING_GIFS,
|
||||
registerShare: Endpoints.KLIPY_REGISTER_SHARE,
|
||||
suggest: Endpoints.KLIPY_SUGGEST,
|
||||
};
|
||||
}
|
||||
|
||||
let featuredCache: Partial<Record<GifProvider, GifFeatured>> = {};
|
||||
|
||||
export async function search(q: string): Promise<Array<Gif>> {
|
||||
const provider = getProvider();
|
||||
const endpoints = getProviderEndpoints(provider);
|
||||
|
||||
try {
|
||||
logger.debug({provider, q}, 'Searching for GIFs');
|
||||
const response = await http.get<Array<Gif>>({
|
||||
url: endpoints.search,
|
||||
query: {q, locale: getLocale()},
|
||||
});
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
logger.error({provider, q, error}, 'Failed to search for GIFs');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFeatured(): Promise<GifFeatured> {
|
||||
const provider = getProvider();
|
||||
const endpoints = getProviderEndpoints(provider);
|
||||
|
||||
const cached = featuredCache[provider];
|
||||
if (cached) {
|
||||
logger.debug({provider}, 'Returning cached featured GIF content');
|
||||
return cached;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.debug({provider}, 'Fetching featured GIF content');
|
||||
const response = await http.get<GifFeatured>({
|
||||
url: endpoints.featured,
|
||||
query: {locale: getLocale()},
|
||||
});
|
||||
const featured = response.body;
|
||||
featuredCache[provider] = featured;
|
||||
return featured;
|
||||
} catch (error) {
|
||||
logger.error({provider, error}, 'Failed to fetch featured GIF content');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTrending(): Promise<Array<Gif>> {
|
||||
const provider = getProvider();
|
||||
const endpoints = getProviderEndpoints(provider);
|
||||
|
||||
try {
|
||||
logger.debug({provider}, 'Fetching trending GIFs');
|
||||
const response = await http.get<Array<Gif>>({
|
||||
url: endpoints.trending,
|
||||
query: {locale: getLocale()},
|
||||
});
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
logger.error({provider, error}, 'Failed to fetch trending GIFs');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function registerShare(id: string, q: string): Promise<void> {
|
||||
const provider = getProvider();
|
||||
const endpoints = getProviderEndpoints(provider);
|
||||
|
||||
try {
|
||||
logger.debug({provider, id, q}, 'Registering GIF share');
|
||||
await http.post({url: endpoints.registerShare, body: {id, q, locale: getLocale()}});
|
||||
} catch (error) {
|
||||
// Share registration is best-effort; it should never block sending a GIF.
|
||||
logger.error({provider, id, error}, 'Failed to register GIF share');
|
||||
}
|
||||
}
|
||||
|
||||
export async function suggest(q: string): Promise<Array<string>> {
|
||||
const provider = getProvider();
|
||||
const endpoints = getProviderEndpoints(provider);
|
||||
|
||||
try {
|
||||
logger.debug({provider, q}, 'Getting GIF search suggestions');
|
||||
const response = await http.get<Array<string>>({
|
||||
url: endpoints.suggest,
|
||||
query: {q, locale: getLocale()},
|
||||
});
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
logger.error({provider, q, error}, 'Failed to get GIF search suggestions');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function resetFeaturedCache(): void {
|
||||
featuredCache = {};
|
||||
}
|
||||
@@ -17,21 +17,23 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as ModalActionCreators from '@app/actions/ModalActionCreators';
|
||||
import {modal} from '@app/actions/ModalActionCreators';
|
||||
import * as ToastActionCreators from '@app/actions/ToastActionCreators';
|
||||
import {GenericErrorModal} from '@app/components/alerts/GenericErrorModal';
|
||||
import {GiftAcceptModal} from '@app/components/modals/GiftAcceptModal';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {HttpError} from '@app/lib/HttpError';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import DeveloperOptionsStore from '@app/stores/DeveloperOptionsStore';
|
||||
import GiftStore from '@app/stores/GiftStore';
|
||||
import UserStore from '@app/stores/UserStore';
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {MS_PER_DAY} from '@fluxer/date_utils/src/DateConstants';
|
||||
import type {UserPartial} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
|
||||
import type {I18n} from '@lingui/core';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {modal} from '~/actions/ModalActionCreators';
|
||||
import * as ToastActionCreators from '~/actions/ToastActionCreators';
|
||||
import {APIErrorCodes} from '~/Constants';
|
||||
import {GenericErrorModal} from '~/components/alerts/GenericErrorModal';
|
||||
import {GiftAcceptModal} from '~/components/modals/GiftAcceptModal';
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http, {HttpError} from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import type {UserPartial} from '~/records/UserRecord';
|
||||
import DeveloperOptionsStore from '~/stores/DeveloperOptionsStore';
|
||||
import GiftStore from '~/stores/GiftStore';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
|
||||
interface ApiErrorResponse {
|
||||
code?: string;
|
||||
@@ -57,7 +59,7 @@ export interface GiftMetadata {
|
||||
redeemed_by: UserPartial | null;
|
||||
}
|
||||
|
||||
export const fetch = async (code: string): Promise<Gift> => {
|
||||
export async function fetch(code: string): Promise<Gift> {
|
||||
try {
|
||||
const response = await http.get<Gift>({url: Endpoints.GIFT(code)});
|
||||
const gift = response.body;
|
||||
@@ -72,21 +74,21 @@ export const fetch = async (code: string): Promise<Gift> => {
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const fetchWithCoalescing = async (code: string): Promise<Gift> => {
|
||||
export async function fetchWithCoalescing(code: string): Promise<Gift> {
|
||||
return GiftStore.fetchGift(code);
|
||||
};
|
||||
}
|
||||
|
||||
export const openAcceptModal = async (code: string): Promise<void> => {
|
||||
export async function openAcceptModal(code: string): Promise<void> {
|
||||
void fetchWithCoalescing(code).catch(() => {});
|
||||
ModalActionCreators.pushWithKey(
|
||||
modal(() => <GiftAcceptModal code={code} />),
|
||||
`gift-accept-${code}`,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const redeem = async (i18n: I18n, code: string): Promise<void> => {
|
||||
export async function redeem(i18n: I18n, code: string): Promise<void> {
|
||||
try {
|
||||
await http.post({url: Endpoints.GIFT_REDEEM(code)});
|
||||
logger.info('Gift redeemed', {code});
|
||||
@@ -167,9 +169,9 @@ export const redeem = async (i18n: I18n, code: string): Promise<void> => {
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const fetchUserGifts = async (): Promise<Array<GiftMetadata>> => {
|
||||
export async function fetchUserGifts(): Promise<Array<GiftMetadata>> {
|
||||
if (DeveloperOptionsStore.mockGiftInventory) {
|
||||
const currentUser = UserStore.getCurrentUser();
|
||||
const userPartial: UserPartial = currentUser
|
||||
@@ -177,20 +179,24 @@ export const fetchUserGifts = async (): Promise<Array<GiftMetadata>> => {
|
||||
id: currentUser.id,
|
||||
username: currentUser.username,
|
||||
discriminator: currentUser.discriminator,
|
||||
global_name: currentUser.globalName,
|
||||
avatar: currentUser.avatar,
|
||||
avatar_color: currentUser.avatarColor ?? null,
|
||||
flags: currentUser.flags,
|
||||
}
|
||||
: {
|
||||
id: '000000000000000000',
|
||||
username: 'MockUser',
|
||||
discriminator: '0000',
|
||||
global_name: null,
|
||||
avatar: null,
|
||||
avatar_color: null,
|
||||
flags: 0,
|
||||
};
|
||||
|
||||
const now = new Date();
|
||||
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000);
|
||||
const sevenDaysAgo = new Date(now.getTime() - 7 * MS_PER_DAY);
|
||||
const twoDaysAgo = new Date(now.getTime() - 2 * MS_PER_DAY);
|
||||
|
||||
const durationMonths = DeveloperOptionsStore.mockGiftDurationMonths ?? 12;
|
||||
const isRedeemed = DeveloperOptionsStore.mockGiftRedeemed ?? false;
|
||||
@@ -217,4 +223,4 @@ export const fetchUserGifts = async (): Promise<Array<GiftMetadata>> => {
|
||||
logger.error('User gifts fetch failed', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,39 +17,22 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {ChannelMoveOperation} from '~/components/layout/utils/channelMoveOperation';
|
||||
import type {AuditLogActionType} from '~/constants/AuditLogActionType';
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import type {Guild} from '~/records/GuildRecord';
|
||||
import type {GuildRole} from '~/records/GuildRoleRecord';
|
||||
import type {Invite} from '~/records/MessageRecord';
|
||||
import InviteStore from '~/stores/InviteStore';
|
||||
import type {ChannelMoveOperation} from '@app/components/layout/utils/ChannelMoveOperation';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import InviteStore from '@app/stores/InviteStore';
|
||||
import type {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType';
|
||||
import type {
|
||||
AuditLogWebhookResponse,
|
||||
GuildAuditLogEntryResponse,
|
||||
} from '@fluxer/schema/src/domains/guild/GuildAuditLogSchemas';
|
||||
import type {Guild} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas';
|
||||
import type {GuildRole} from '@fluxer/schema/src/domains/guild/GuildRoleSchemas';
|
||||
import type {Invite} from '@fluxer/schema/src/domains/invite/InviteSchemas';
|
||||
import type {UserPartial} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
|
||||
|
||||
const logger = new Logger('Guilds');
|
||||
|
||||
import type {UserPartial} from '~/records/UserRecord';
|
||||
|
||||
export interface AuditLogChangeEntry {
|
||||
key?: string;
|
||||
old_value?: unknown;
|
||||
new_value?: unknown;
|
||||
oldValue?: unknown;
|
||||
newValue?: unknown;
|
||||
}
|
||||
|
||||
export type GuildAuditLogChangePayload = Array<AuditLogChangeEntry> | null;
|
||||
|
||||
export interface GuildAuditLogEntry {
|
||||
id: string;
|
||||
action_type: number;
|
||||
user_id: string | null;
|
||||
target_id: string | null;
|
||||
reason?: string;
|
||||
options?: Record<string, unknown>;
|
||||
changes?: GuildAuditLogChangePayload;
|
||||
}
|
||||
const logger = new Logger('GuildActionCreators');
|
||||
|
||||
export interface GuildAuditLogFetchParams {
|
||||
userId?: string;
|
||||
@@ -60,9 +43,9 @@ export interface GuildAuditLogFetchParams {
|
||||
}
|
||||
|
||||
interface GuildAuditLogFetchResponse {
|
||||
audit_log_entries: Array<GuildAuditLogEntry>;
|
||||
audit_log_entries: Array<GuildAuditLogEntryResponse>;
|
||||
users: Array<UserPartial>;
|
||||
webhooks: Array<unknown>;
|
||||
webhooks: Array<AuditLogWebhookResponse>;
|
||||
}
|
||||
|
||||
export interface GuildBan {
|
||||
@@ -79,19 +62,19 @@ export interface GuildBan {
|
||||
expires_at: string | null;
|
||||
}
|
||||
|
||||
export const create = async (params: Pick<Guild, 'name'> & {icon?: string | null}): Promise<Guild> => {
|
||||
export async function create(params: Pick<Guild, 'name'> & {icon?: string | null}): Promise<Guild> {
|
||||
try {
|
||||
const response = await http.post<Guild>(Endpoints.GUILDS, params);
|
||||
const guild = response.body;
|
||||
logger.debug(`Created new guild: ${params.name}`);
|
||||
logger.debug(`Created new guild: ${params['name']}`);
|
||||
return guild;
|
||||
} catch (error) {
|
||||
logger.error('Failed to create guild:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const update = async (
|
||||
export async function update(
|
||||
guildId: string,
|
||||
params: Partial<
|
||||
Pick<
|
||||
@@ -108,12 +91,14 @@ export const update = async (
|
||||
| 'system_channel_flags'
|
||||
| 'features'
|
||||
| 'default_message_notifications'
|
||||
| 'message_history_cutoff'
|
||||
| 'verification_level'
|
||||
| 'mfa_level'
|
||||
| 'nsfw_level'
|
||||
| 'explicit_content_filter'
|
||||
>
|
||||
>,
|
||||
): Promise<Guild> => {
|
||||
): Promise<Guild> {
|
||||
try {
|
||||
const response = await http.patch<Guild>(Endpoints.GUILD(guildId), params);
|
||||
const guild = response.body;
|
||||
@@ -123,9 +108,9 @@ export const update = async (
|
||||
logger.error(`Failed to update guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const moveChannel = async (guildId: string, operation: ChannelMoveOperation): Promise<void> => {
|
||||
export async function moveChannel(guildId: string, operation: ChannelMoveOperation): Promise<void> {
|
||||
try {
|
||||
await http.patch({
|
||||
url: Endpoints.GUILD_CHANNELS(guildId),
|
||||
@@ -144,9 +129,9 @@ export const moveChannel = async (guildId: string, operation: ChannelMoveOperati
|
||||
logger.error(`Failed to move channel ${operation.channelId} in guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const getVanityURL = async (guildId: string): Promise<{code: string | null; uses: number}> => {
|
||||
export async function getVanityURL(guildId: string): Promise<{code: string | null; uses: number}> {
|
||||
try {
|
||||
const response = await http.get<{code: string | null; uses: number}>(Endpoints.GUILD_VANITY_URL(guildId));
|
||||
const result = response.body;
|
||||
@@ -156,9 +141,9 @@ export const getVanityURL = async (guildId: string): Promise<{code: string | nul
|
||||
logger.error(`Failed to fetch vanity URL for guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const updateVanityURL = async (guildId: string, code: string | null): Promise<string> => {
|
||||
export async function updateVanityURL(guildId: string, code: string | null): Promise<string> {
|
||||
try {
|
||||
const response = await http.patch<{code: string}>(Endpoints.GUILD_VANITY_URL(guildId), {code});
|
||||
logger.debug(`Updated vanity URL for guild ${guildId} to ${code || 'none'}`);
|
||||
@@ -167,9 +152,9 @@ export const updateVanityURL = async (guildId: string, code: string | null): Pro
|
||||
logger.error(`Failed to update vanity URL for guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const createRole = async (guildId: string, name: string): Promise<void> => {
|
||||
export async function createRole(guildId: string, name: string): Promise<void> {
|
||||
try {
|
||||
await http.post({url: Endpoints.GUILD_ROLES(guildId), body: {name}});
|
||||
logger.debug(`Created role "${name}" in guild ${guildId}`);
|
||||
@@ -177,9 +162,9 @@ export const createRole = async (guildId: string, name: string): Promise<void> =
|
||||
logger.error(`Failed to create role in guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const updateRole = async (guildId: string, roleId: string, patch: Partial<GuildRole>): Promise<void> => {
|
||||
export async function updateRole(guildId: string, roleId: string, patch: Partial<GuildRole>): Promise<void> {
|
||||
try {
|
||||
await http.patch({url: Endpoints.GUILD_ROLE(guildId, roleId), body: patch});
|
||||
logger.debug(`Updated role ${roleId} in guild ${guildId}`);
|
||||
@@ -187,9 +172,9 @@ export const updateRole = async (guildId: string, roleId: string, patch: Partial
|
||||
logger.error(`Failed to update role ${roleId} in guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const deleteRole = async (guildId: string, roleId: string): Promise<void> => {
|
||||
export async function deleteRole(guildId: string, roleId: string): Promise<void> {
|
||||
try {
|
||||
await http.delete({url: Endpoints.GUILD_ROLE(guildId, roleId)});
|
||||
logger.debug(`Deleted role ${roleId} from guild ${guildId}`);
|
||||
@@ -197,9 +182,9 @@ export const deleteRole = async (guildId: string, roleId: string): Promise<void>
|
||||
logger.error(`Failed to delete role ${roleId} from guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const setRoleOrder = async (guildId: string, orderedRoleIds: Array<string>): Promise<void> => {
|
||||
export async function setRoleOrder(guildId: string, orderedRoleIds: Array<string>): Promise<void> {
|
||||
try {
|
||||
const filteredIds = orderedRoleIds.filter((id) => id !== guildId);
|
||||
const payload = filteredIds.map((id, index) => ({id, position: filteredIds.length - index}));
|
||||
@@ -209,9 +194,9 @@ export const setRoleOrder = async (guildId: string, orderedRoleIds: Array<string
|
||||
logger.error(`Failed to update role ordering in guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const setRoleHoistOrder = async (guildId: string, orderedRoleIds: Array<string>): Promise<void> => {
|
||||
export async function setRoleHoistOrder(guildId: string, orderedRoleIds: Array<string>): Promise<void> {
|
||||
try {
|
||||
const filteredIds = orderedRoleIds.filter((id) => id !== guildId);
|
||||
const payload = filteredIds.map((id, index) => ({id, hoist_position: filteredIds.length - index}));
|
||||
@@ -221,9 +206,9 @@ export const setRoleHoistOrder = async (guildId: string, orderedRoleIds: Array<s
|
||||
logger.error(`Failed to update role hoist ordering in guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const resetRoleHoistOrder = async (guildId: string): Promise<void> => {
|
||||
export async function resetRoleHoistOrder(guildId: string): Promise<void> {
|
||||
try {
|
||||
await http.delete({url: Endpoints.GUILD_ROLE_HOIST_POSITIONS(guildId)});
|
||||
logger.debug(`Reset role hoist ordering in guild ${guildId}`);
|
||||
@@ -231,9 +216,9 @@ export const resetRoleHoistOrder = async (guildId: string): Promise<void> => {
|
||||
logger.error(`Failed to reset role hoist ordering in guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const remove = async (guildId: string): Promise<void> => {
|
||||
export async function remove(guildId: string): Promise<void> {
|
||||
try {
|
||||
await http.post({url: Endpoints.GUILD_DELETE(guildId), body: {}});
|
||||
logger.debug(`Deleted guild ${guildId}`);
|
||||
@@ -241,9 +226,9 @@ export const remove = async (guildId: string): Promise<void> => {
|
||||
logger.error(`Failed to delete guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const leave = async (guildId: string): Promise<void> => {
|
||||
export async function leave(guildId: string): Promise<void> {
|
||||
try {
|
||||
await http.delete({url: Endpoints.USER_GUILDS(guildId)});
|
||||
logger.debug(`Left guild ${guildId}`);
|
||||
@@ -251,9 +236,9 @@ export const leave = async (guildId: string): Promise<void> => {
|
||||
logger.error(`Failed to leave guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const fetchGuildInvites = async (guildId: string): Promise<Array<Invite>> => {
|
||||
export async function fetchGuildInvites(guildId: string): Promise<Array<Invite>> {
|
||||
try {
|
||||
InviteStore.handleGuildInvitesFetchPending(guildId);
|
||||
const response = await http.get<Array<Invite>>(Endpoints.GUILD_INVITES(guildId));
|
||||
@@ -265,9 +250,9 @@ export const fetchGuildInvites = async (guildId: string): Promise<Array<Invite>>
|
||||
InviteStore.handleGuildInvitesFetchError(guildId);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const toggleInvitesDisabled = async (guildId: string, disabled: boolean): Promise<Guild> => {
|
||||
export async function toggleInvitesDisabled(guildId: string, disabled: boolean): Promise<Guild> {
|
||||
try {
|
||||
const response = await http.patch<Guild>(Endpoints.GUILD(guildId), {
|
||||
features: disabled ? ['INVITES_DISABLED'] : [],
|
||||
@@ -279,9 +264,9 @@ export const toggleInvitesDisabled = async (guildId: string, disabled: boolean):
|
||||
logger.error(`Failed to ${disabled ? 'disable' : 'enable'} invites for guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const toggleTextChannelFlexibleNames = async (guildId: string, enabled: boolean): Promise<Guild> => {
|
||||
export async function toggleTextChannelFlexibleNames(guildId: string, enabled: boolean): Promise<Guild> {
|
||||
try {
|
||||
const response = await http.patch<Guild>(Endpoints.GUILD_TEXT_CHANNEL_FLEXIBLE_NAMES(guildId), {enabled});
|
||||
const guild = response.body;
|
||||
@@ -294,9 +279,9 @@ export const toggleTextChannelFlexibleNames = async (guildId: string, enabled: b
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const toggleDetachedBanner = async (guildId: string, enabled: boolean): Promise<Guild> => {
|
||||
export async function toggleDetachedBanner(guildId: string, enabled: boolean): Promise<Guild> {
|
||||
try {
|
||||
const response = await http.patch<Guild>(Endpoints.GUILD_DETACHED_BANNER(guildId), {enabled});
|
||||
const guild = response.body;
|
||||
@@ -306,24 +291,9 @@ export const toggleDetachedBanner = async (guildId: string, enabled: boolean): P
|
||||
logger.error(`Failed to ${enabled ? 'enable' : 'disable'} detached banner for guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const toggleDisallowUnclaimedAccounts = async (guildId: string, enabled: boolean): Promise<Guild> => {
|
||||
try {
|
||||
const response = await http.patch<Guild>(Endpoints.GUILD_DISALLOW_UNCLAIMED_ACCOUNTS(guildId), {enabled});
|
||||
const guild = response.body;
|
||||
logger.debug(`${enabled ? 'Enabled' : 'Disabled'} disallow unclaimed accounts for guild ${guildId}`);
|
||||
return guild;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to ${enabled ? 'enable' : 'disable'} disallow unclaimed accounts for guild ${guildId}:`,
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const transferOwnership = async (guildId: string, newOwnerId: string): Promise<Guild> => {
|
||||
export async function transferOwnership(guildId: string, newOwnerId: string): Promise<Guild> {
|
||||
try {
|
||||
const response = await http.post<Guild>(Endpoints.GUILD_TRANSFER_OWNERSHIP(guildId), {
|
||||
new_owner_id: newOwnerId,
|
||||
@@ -335,15 +305,15 @@ export const transferOwnership = async (guildId: string, newOwnerId: string): Pr
|
||||
logger.error(`Failed to transfer ownership of guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const banMember = async (
|
||||
export async function banMember(
|
||||
guildId: string,
|
||||
userId: string,
|
||||
deleteMessageDays?: number,
|
||||
reason?: string,
|
||||
banDurationSeconds?: number,
|
||||
): Promise<void> => {
|
||||
): Promise<void> {
|
||||
try {
|
||||
await http.put({
|
||||
url: Endpoints.GUILD_BAN(guildId, userId),
|
||||
@@ -358,9 +328,9 @@ export const banMember = async (
|
||||
logger.error(`Failed to ban user ${userId} from guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const unbanMember = async (guildId: string, userId: string): Promise<void> => {
|
||||
export async function unbanMember(guildId: string, userId: string): Promise<void> {
|
||||
try {
|
||||
await http.delete({url: Endpoints.GUILD_BAN(guildId, userId)});
|
||||
logger.debug(`Unbanned user ${userId} from guild ${guildId}`);
|
||||
@@ -368,9 +338,9 @@ export const unbanMember = async (guildId: string, userId: string): Promise<void
|
||||
logger.error(`Failed to unban user ${userId} from guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const fetchBans = async (guildId: string): Promise<Array<GuildBan>> => {
|
||||
export async function fetchBans(guildId: string): Promise<Array<GuildBan>> {
|
||||
try {
|
||||
const response = await http.get<Array<GuildBan>>(Endpoints.GUILD_BANS(guildId));
|
||||
const bans = response.body;
|
||||
@@ -380,12 +350,12 @@ export const fetchBans = async (guildId: string): Promise<Array<GuildBan>> => {
|
||||
logger.error(`Failed to fetch bans for guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const fetchGuildAuditLogs = async (
|
||||
export async function fetchGuildAuditLogs(
|
||||
guildId: string,
|
||||
params: GuildAuditLogFetchParams,
|
||||
): Promise<GuildAuditLogFetchResponse> => {
|
||||
): Promise<GuildAuditLogFetchResponse> {
|
||||
try {
|
||||
const query: Record<string, string | number> = {};
|
||||
if (params.limit !== undefined) query.limit = params.limit;
|
||||
@@ -406,4 +376,4 @@ export const fetchGuildAuditLogs = async (
|
||||
logger.error(`Failed to fetch audit logs for guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,23 +17,23 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import type {GuildEmojiWithUser} from '~/records/GuildEmojiRecord';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import type {GuildEmojiWithUser} from '@fluxer/schema/src/domains/guild/GuildEmojiSchemas';
|
||||
|
||||
const logger = new Logger('Emojis');
|
||||
|
||||
export const sanitizeEmojiName = (fileName: string): string => {
|
||||
export function sanitizeEmojiName(fileName: string): string {
|
||||
const name =
|
||||
fileName
|
||||
.split('.')
|
||||
.shift()
|
||||
?.replace(/[^a-zA-Z0-9_]/g, '') ?? '';
|
||||
return name.padEnd(2, '_').slice(0, 32);
|
||||
};
|
||||
}
|
||||
|
||||
export const list = async (guildId: string): Promise<ReadonlyArray<GuildEmojiWithUser>> => {
|
||||
export async function list(guildId: string): Promise<ReadonlyArray<GuildEmojiWithUser>> {
|
||||
try {
|
||||
const response = await http.get<ReadonlyArray<GuildEmojiWithUser>>({url: Endpoints.GUILD_EMOJIS(guildId)});
|
||||
const emojis = response.body;
|
||||
@@ -43,15 +43,18 @@ export const list = async (guildId: string): Promise<ReadonlyArray<GuildEmojiWit
|
||||
logger.error(`Failed to list emojis for guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const bulkUpload = async (
|
||||
export async function bulkUpload(
|
||||
guildId: string,
|
||||
emojis: Array<{name: string; image: string}>,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{success: Array<any>; failed: Array<{name: string; error: string}>}> => {
|
||||
): Promise<{success: Array<GuildEmojiWithUser>; failed: Array<{name: string; error: string}>}> {
|
||||
try {
|
||||
const response = await http.post<{success: Array<any>; failed: Array<{name: string; error: string}>}>({
|
||||
const response = await http.post<{
|
||||
success: Array<GuildEmojiWithUser>;
|
||||
failed: Array<{name: string; error: string}>;
|
||||
}>({
|
||||
url: `${Endpoints.GUILD_EMOJIS(guildId)}/bulk`,
|
||||
body: {emojis},
|
||||
signal,
|
||||
@@ -63,9 +66,9 @@ export const bulkUpload = async (
|
||||
logger.error(`Failed to bulk upload emojis to guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const update = async (guildId: string, emojiId: string, data: {name: string}): Promise<void> => {
|
||||
export async function update(guildId: string, emojiId: string, data: {name: string}): Promise<void> {
|
||||
try {
|
||||
await http.patch({url: Endpoints.GUILD_EMOJI(guildId, emojiId), body: data});
|
||||
logger.debug(`Updated emoji ${emojiId} in guild ${guildId}`);
|
||||
@@ -73,15 +76,17 @@ export const update = async (guildId: string, emojiId: string, data: {name: stri
|
||||
logger.error(`Failed to update emoji ${emojiId} in guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const remove = async (guildId: string, emojiId: string, purge = false): Promise<void> => {
|
||||
export async function remove(guildId: string, emojiId: string, purge = false): Promise<void> {
|
||||
try {
|
||||
const query = purge ? '?purge=true' : '';
|
||||
await http.delete({url: `${Endpoints.GUILD_EMOJI(guildId, emojiId)}${query}`});
|
||||
await http.delete({
|
||||
url: Endpoints.GUILD_EMOJI(guildId, emojiId),
|
||||
query: purge ? {purge: true} : undefined,
|
||||
});
|
||||
logger.debug(`Removed emoji ${emojiId} from guild ${guildId}`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to remove emoji ${emojiId} from guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,28 +17,28 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import type {GuildMember} from '~/records/GuildMemberRecord';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import type {GuildMemberData} from '@fluxer/schema/src/domains/guild/GuildMemberSchemas';
|
||||
|
||||
const logger = new Logger('GuildMembers');
|
||||
|
||||
export const update = async (
|
||||
export async function update(
|
||||
guildId: string,
|
||||
userId: string,
|
||||
params: Partial<GuildMember> & {channel_id?: string | null; connection_id?: string},
|
||||
): Promise<void> => {
|
||||
params: Partial<GuildMemberData> & {channel_id?: string | null; connection_id?: string},
|
||||
): Promise<void> {
|
||||
try {
|
||||
await http.patch({url: Endpoints.GUILD_MEMBER(guildId, userId), body: params});
|
||||
logger.debug(`Updated member ${userId} in guild ${guildId}`, {connection_id: params.connection_id});
|
||||
logger.debug(`Updated member ${userId} in guild ${guildId}`, {connection_id: params['connection_id']});
|
||||
} catch (error) {
|
||||
logger.error(`Failed to update member ${userId} in guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const addRole = async (guildId: string, userId: string, roleId: string): Promise<void> => {
|
||||
export async function addRole(guildId: string, userId: string, roleId: string): Promise<void> {
|
||||
try {
|
||||
await http.put({url: Endpoints.GUILD_MEMBER_ROLE(guildId, userId, roleId)});
|
||||
logger.debug(`Added role ${roleId} to member ${userId} in guild ${guildId}`);
|
||||
@@ -46,9 +46,9 @@ export const addRole = async (guildId: string, userId: string, roleId: string):
|
||||
logger.error(`Failed to add role ${roleId} to member ${userId} in guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const removeRole = async (guildId: string, userId: string, roleId: string): Promise<void> => {
|
||||
export async function removeRole(guildId: string, userId: string, roleId: string): Promise<void> {
|
||||
try {
|
||||
await http.delete({url: Endpoints.GUILD_MEMBER_ROLE(guildId, userId, roleId)});
|
||||
logger.debug(`Removed role ${roleId} from member ${userId} in guild ${guildId}`);
|
||||
@@ -56,9 +56,9 @@ export const removeRole = async (guildId: string, userId: string, roleId: string
|
||||
logger.error(`Failed to remove role ${roleId} from member ${userId} in guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const updateProfile = async (
|
||||
export async function updateProfile(
|
||||
guildId: string,
|
||||
params: {
|
||||
avatar?: string | null;
|
||||
@@ -69,7 +69,7 @@ export const updateProfile = async (
|
||||
nick?: string | null;
|
||||
profile_flags?: number | null;
|
||||
},
|
||||
): Promise<void> => {
|
||||
): Promise<void> {
|
||||
try {
|
||||
await http.patch({url: Endpoints.GUILD_MEMBER(guildId), body: params});
|
||||
logger.debug(`Updated current user's per-guild profile in guild ${guildId}`);
|
||||
@@ -77,9 +77,9 @@ export const updateProfile = async (
|
||||
logger.error(`Failed to update current user's per-guild profile in guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const kick = async (guildId: string, userId: string): Promise<void> => {
|
||||
export async function kick(guildId: string, userId: string): Promise<void> {
|
||||
try {
|
||||
await http.delete({url: Endpoints.GUILD_MEMBER(guildId, userId)});
|
||||
logger.debug(`Kicked member ${userId} from guild ${guildId}`);
|
||||
@@ -87,14 +87,14 @@ export const kick = async (guildId: string, userId: string): Promise<void> => {
|
||||
logger.error(`Failed to kick member ${userId} from guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const timeout = async (
|
||||
export async function timeout(
|
||||
guildId: string,
|
||||
userId: string,
|
||||
communicationDisabledUntil: string | null,
|
||||
timeoutReason?: string | null,
|
||||
): Promise<void> => {
|
||||
): Promise<void> {
|
||||
try {
|
||||
const body: Record<string, string | null> = {
|
||||
communication_disabled_until: communicationDisabledUntil,
|
||||
@@ -111,4 +111,4 @@ export const timeout = async (
|
||||
logger.error(`Failed to update timeout for member ${userId} in guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,8 +17,12 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import GuildNSFWAgreeStore from '~/stores/GuildNSFWAgreeStore';
|
||||
import GuildNSFWAgreeStore from '@app/stores/GuildNSFWAgreeStore';
|
||||
|
||||
export function agreeToNSFWChannel(channelId: string): void {
|
||||
export function agreeToChannel(channelId: string): void {
|
||||
GuildNSFWAgreeStore.agreeToChannel(channelId);
|
||||
}
|
||||
|
||||
export function agreeToGuild(guildId: string): void {
|
||||
GuildNSFWAgreeStore.agreeToGuild(guildId);
|
||||
}
|
||||
|
||||
@@ -17,23 +17,23 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import type {GuildStickerWithUser} from '~/records/GuildStickerRecord';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import type {GuildStickerWithUser} from '@fluxer/schema/src/domains/guild/GuildEmojiSchemas';
|
||||
|
||||
const logger = new Logger('Stickers');
|
||||
|
||||
export const sanitizeStickerName = (fileName: string): string => {
|
||||
export function sanitizeStickerName(fileName: string): string {
|
||||
const name =
|
||||
fileName
|
||||
.split('.')
|
||||
.shift()
|
||||
?.replace(/[^a-zA-Z0-9_]/g, '') ?? '';
|
||||
return name.padEnd(2, '_').slice(0, 30);
|
||||
};
|
||||
}
|
||||
|
||||
export const list = async (guildId: string): Promise<ReadonlyArray<GuildStickerWithUser>> => {
|
||||
export async function list(guildId: string): Promise<ReadonlyArray<GuildStickerWithUser>> {
|
||||
try {
|
||||
const response = await http.get<ReadonlyArray<GuildStickerWithUser>>({url: Endpoints.GUILD_STICKERS(guildId)});
|
||||
const stickers = response.body;
|
||||
@@ -43,12 +43,12 @@ export const list = async (guildId: string): Promise<ReadonlyArray<GuildStickerW
|
||||
logger.error(`Failed to list stickers for guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const create = async (
|
||||
export async function create(
|
||||
guildId: string,
|
||||
sticker: {name: string; description: string; tags: Array<string>; image: string},
|
||||
): Promise<void> => {
|
||||
): Promise<void> {
|
||||
try {
|
||||
await http.post({url: Endpoints.GUILD_STICKERS(guildId), body: sticker});
|
||||
logger.debug(`Created sticker ${sticker.name} in guild ${guildId}`);
|
||||
@@ -56,13 +56,13 @@ export const create = async (
|
||||
logger.error(`Failed to create sticker ${sticker.name} in guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const update = async (
|
||||
export async function update(
|
||||
guildId: string,
|
||||
stickerId: string,
|
||||
data: {name?: string; description?: string; tags?: Array<string>},
|
||||
): Promise<void> => {
|
||||
): Promise<void> {
|
||||
try {
|
||||
await http.patch({url: Endpoints.GUILD_STICKER(guildId, stickerId), body: data});
|
||||
logger.debug(`Updated sticker ${stickerId} in guild ${guildId}`);
|
||||
@@ -70,15 +70,17 @@ export const update = async (
|
||||
logger.error(`Failed to update sticker ${stickerId} in guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const remove = async (guildId: string, stickerId: string, purge = false): Promise<void> => {
|
||||
export async function remove(guildId: string, stickerId: string, purge = false): Promise<void> {
|
||||
try {
|
||||
const query = purge ? '?purge=true' : '';
|
||||
await http.delete({url: `${Endpoints.GUILD_STICKER(guildId, stickerId)}${query}`});
|
||||
await http.delete({
|
||||
url: Endpoints.GUILD_STICKER(guildId, stickerId),
|
||||
query: purge ? {purge: true} : undefined,
|
||||
});
|
||||
logger.debug(`Removed sticker ${stickerId} from guild ${guildId}`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to remove sticker ${stickerId} from guild ${guildId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import AutocompleteStore from '~/stores/AutocompleteStore';
|
||||
import AutocompleteStore from '@app/stores/AutocompleteStore';
|
||||
|
||||
export const highlightChannel = (channelId: string): void => {
|
||||
export function highlightChannel(channelId: string): void {
|
||||
AutocompleteStore.highlightChannel(channelId);
|
||||
};
|
||||
}
|
||||
|
||||
export const clearChannelHighlight = (): void => {
|
||||
export function clearChannelHighlight(): void {
|
||||
AutocompleteStore.highlightChannelClear();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,18 +17,18 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
|
||||
const logger = new Logger('IAR');
|
||||
|
||||
export const reportMessage = async (
|
||||
export async function reportMessage(
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
category: string,
|
||||
additionalInfo?: string,
|
||||
): Promise<void> => {
|
||||
): Promise<void> {
|
||||
try {
|
||||
logger.debug(`Reporting message ${messageId} in channel ${channelId}`);
|
||||
await http.post({
|
||||
@@ -45,14 +45,14 @@ export const reportMessage = async (
|
||||
logger.error('Failed to submit message report:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const reportUser = async (
|
||||
export async function reportUser(
|
||||
userId: string,
|
||||
category: string,
|
||||
additionalInfo?: string,
|
||||
guildId?: string,
|
||||
): Promise<void> => {
|
||||
): Promise<void> {
|
||||
try {
|
||||
logger.debug(`Reporting user ${userId}${guildId ? ` in guild ${guildId}` : ''}`);
|
||||
await http.post({
|
||||
@@ -69,9 +69,9 @@ export const reportUser = async (
|
||||
logger.error('Failed to submit user report:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const reportGuild = async (guildId: string, category: string, additionalInfo?: string): Promise<void> => {
|
||||
export async function reportGuild(guildId: string, category: string, additionalInfo?: string): Promise<void> {
|
||||
try {
|
||||
logger.debug(`Reporting guild ${guildId}`);
|
||||
await http.post({
|
||||
@@ -87,4 +87,4 @@ export const reportGuild = async (guildId: string, category: string, additionalI
|
||||
logger.error('Failed to submit guild report:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {InboxTab} from '~/stores/InboxStore';
|
||||
import InboxStore from '~/stores/InboxStore';
|
||||
import type {InboxTab} from '@app/stores/InboxStore';
|
||||
import InboxStore from '@app/stores/InboxStore';
|
||||
|
||||
export const setTab = (tab: InboxTab): void => {
|
||||
export function setTab(tab: InboxTab): void {
|
||||
InboxStore.setTab(tab);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,47 +17,47 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as ModalActionCreators from '@app/actions/ModalActionCreators';
|
||||
import {modal} from '@app/actions/ModalActionCreators';
|
||||
import * as NavigationActionCreators from '@app/actions/NavigationActionCreators';
|
||||
import * as ToastActionCreators from '@app/actions/ToastActionCreators';
|
||||
import {FeatureTemporarilyDisabledModal} from '@app/components/alerts/FeatureTemporarilyDisabledModal';
|
||||
import {GenericErrorModal} from '@app/components/alerts/GenericErrorModal';
|
||||
import {GuildAtCapacityModal} from '@app/components/alerts/GuildAtCapacityModal';
|
||||
import {InviteAcceptFailedModal} from '@app/components/alerts/InviteAcceptFailedModal';
|
||||
import {InvitesDisabledModal} from '@app/components/alerts/InvitesDisabledModal';
|
||||
import {MaxGuildsModal} from '@app/components/alerts/MaxGuildsModal';
|
||||
import {TemporaryInviteRequiresPresenceModal} from '@app/components/alerts/TemporaryInviteRequiresPresenceModal';
|
||||
import {UserBannedFromGuildModal} from '@app/components/alerts/UserBannedFromGuildModal';
|
||||
import {UserIpBannedFromGuildModal} from '@app/components/alerts/UserIpBannedFromGuildModal';
|
||||
import {InviteAcceptModal} from '@app/components/modals/InviteAcceptModal';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {HttpError} from '@app/lib/HttpError';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import AuthenticationStore from '@app/stores/AuthenticationStore';
|
||||
import GuildMemberStore from '@app/stores/GuildMemberStore';
|
||||
import InviteStore from '@app/stores/InviteStore';
|
||||
import {isGroupDmInvite, isGuildInvite, isPackInvite} from '@app/types/InviteTypes';
|
||||
import {getApiErrorCode, getApiErrorMessage} from '@app/utils/ApiErrorUtils';
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {ME} from '@fluxer/constants/src/AppConstants';
|
||||
import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
|
||||
import type {Invite} from '@fluxer/schema/src/domains/invite/InviteSchemas';
|
||||
import type {I18n} from '@lingui/core';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import {Trans} from '@lingui/react/macro';
|
||||
import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {modal} from '~/actions/ModalActionCreators';
|
||||
import * as ToastActionCreators from '~/actions/ToastActionCreators';
|
||||
import {APIErrorCodes} from '~/Constants';
|
||||
import {FeatureTemporarilyDisabledModal} from '~/components/alerts/FeatureTemporarilyDisabledModal';
|
||||
import {GenericErrorModal} from '~/components/alerts/GenericErrorModal';
|
||||
import {GuildAtCapacityModal} from '~/components/alerts/GuildAtCapacityModal';
|
||||
import {InviteAcceptFailedModal} from '~/components/alerts/InviteAcceptFailedModal';
|
||||
import {InvitesDisabledModal} from '~/components/alerts/InvitesDisabledModal';
|
||||
import {MaxGuildsModal} from '~/components/alerts/MaxGuildsModal';
|
||||
import {TemporaryInviteRequiresPresenceModal} from '~/components/alerts/TemporaryInviteRequiresPresenceModal';
|
||||
import {UserBannedFromGuildModal} from '~/components/alerts/UserBannedFromGuildModal';
|
||||
import {UserIpBannedFromGuildModal} from '~/components/alerts/UserIpBannedFromGuildModal';
|
||||
import {InviteAcceptModal} from '~/components/modals/InviteAcceptModal';
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http, {HttpError} from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import {Routes} from '~/Routes';
|
||||
import type {Invite} from '~/records/MessageRecord';
|
||||
import AuthenticationStore from '~/stores/AuthenticationStore';
|
||||
import GuildMemberStore from '~/stores/GuildMemberStore';
|
||||
import InviteStore from '~/stores/InviteStore';
|
||||
import {isGroupDmInvite, isGuildInvite, isPackInvite} from '~/types/InviteTypes';
|
||||
import * as RouterUtils from '~/utils/RouterUtils';
|
||||
|
||||
const logger = new Logger('Invites');
|
||||
const extractErrorCode = (error: unknown): string | undefined => {
|
||||
if (error instanceof HttpError) {
|
||||
const body = error.body;
|
||||
if (body && typeof body === 'object' && 'code' in body) {
|
||||
const {code} = body as {code?: unknown};
|
||||
return typeof code === 'string' ? code : undefined;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
|
||||
const isUnclaimedAccountInviteError = (code?: string): boolean => {
|
||||
return code === APIErrorCodes.UNCLAIMED_ACCOUNT_CANNOT_JOIN_GROUP_DMS;
|
||||
};
|
||||
|
||||
export const fetch = async (code: string): Promise<Invite> => {
|
||||
const shouldOpenInviteGuildChannel = (channelType: number): boolean =>
|
||||
channelType !== ChannelTypes.GUILD_CATEGORY && channelType !== ChannelTypes.GUILD_LINK;
|
||||
|
||||
export async function fetch(code: string): Promise<Invite> {
|
||||
try {
|
||||
logger.debug(`Fetching invite with code ${code}`);
|
||||
const response = await http.get<Invite>(Endpoints.INVITE(code));
|
||||
@@ -66,11 +66,11 @@ export const fetch = async (code: string): Promise<Invite> => {
|
||||
logger.error(`Failed to fetch invite with code ${code}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const fetchWithCoalescing = async (code: string): Promise<Invite> => {
|
||||
export async function fetchWithCoalescing(code: string): Promise<Invite> {
|
||||
return InviteStore.fetchInvite(code);
|
||||
};
|
||||
}
|
||||
|
||||
const accept = async (code: string): Promise<Invite> => {
|
||||
try {
|
||||
@@ -85,7 +85,7 @@ const accept = async (code: string): Promise<Invite> => {
|
||||
|
||||
export const acceptInvite = accept;
|
||||
|
||||
export const acceptAndTransitionToChannel = async (code: string, i18n: I18n): Promise<void> => {
|
||||
export async function acceptAndTransitionToChannel(code: string, i18n: I18n): Promise<void> {
|
||||
let invite: Invite | null = null;
|
||||
try {
|
||||
logger.debug(`Fetching invite details before accepting: ${code}`);
|
||||
@@ -112,7 +112,7 @@ export const acceptAndTransitionToChannel = async (code: string, i18n: I18n): Pr
|
||||
const channelId = invite.channel.id;
|
||||
logger.debug(`Accepting group DM invite ${code} and opening channel ${channelId}`);
|
||||
await accept(code);
|
||||
RouterUtils.transitionTo(Routes.dmChannel(channelId));
|
||||
NavigationActionCreators.selectChannel(ME, channelId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -121,21 +121,31 @@ export const acceptAndTransitionToChannel = async (code: string, i18n: I18n): Pr
|
||||
}
|
||||
|
||||
const channelId = invite.channel.id;
|
||||
const inviteTargetAllowed = shouldOpenInviteGuildChannel(invite.channel.type);
|
||||
const targetChannelId = inviteTargetAllowed ? channelId : undefined;
|
||||
const currentUserId = AuthenticationStore.currentUserId;
|
||||
const guildId = invite.guild.id;
|
||||
const isMember = currentUserId ? GuildMemberStore.getMember(guildId, currentUserId) != null : false;
|
||||
if (isMember) {
|
||||
logger.debug(`User already in guild ${guildId}, transitioning to channel ${channelId}`);
|
||||
RouterUtils.transitionTo(Routes.guildChannel(guildId, channelId));
|
||||
logger.debug(
|
||||
inviteTargetAllowed
|
||||
? `User already in guild ${guildId}, transitioning to channel ${channelId}`
|
||||
: `User already in guild ${guildId}, invite target is non-viewable, transitioning to guild root`,
|
||||
);
|
||||
NavigationActionCreators.selectChannel(guildId, targetChannelId);
|
||||
return;
|
||||
}
|
||||
logger.debug(`User not in guild ${guildId}, accepting invite ${code}`);
|
||||
await accept(code);
|
||||
logger.debug(`Transitioning to channel ${channelId} in guild ${guildId}`);
|
||||
RouterUtils.transitionTo(Routes.guildChannel(guildId, channelId));
|
||||
logger.debug(
|
||||
inviteTargetAllowed
|
||||
? `Transitioning to channel ${channelId} in guild ${guildId}`
|
||||
: `Invite target channel ${channelId} in guild ${guildId} is non-viewable, transitioning to guild root`,
|
||||
);
|
||||
NavigationActionCreators.selectChannel(guildId, targetChannelId);
|
||||
} catch (error) {
|
||||
const httpError = error instanceof HttpError ? error : null;
|
||||
const errorCode = extractErrorCode(error);
|
||||
const errorCode = getApiErrorCode(error);
|
||||
logger.error(`Failed to accept invite and transition for code ${code}:`, error);
|
||||
|
||||
if (httpError?.status === 404 || errorCode === APIErrorCodes.UNKNOWN_INVITE) {
|
||||
@@ -161,18 +171,7 @@ export const acceptAndTransitionToChannel = async (code: string, i18n: I18n): Pr
|
||||
ModalActionCreators.push(modal(() => <UserBannedFromGuildModal />));
|
||||
} else if (errorCode === APIErrorCodes.USER_IP_BANNED_FROM_GUILD) {
|
||||
ModalActionCreators.push(modal(() => <UserIpBannedFromGuildModal />));
|
||||
} else if (errorCode === APIErrorCodes.GUILD_DISALLOWS_UNCLAIMED_ACCOUNTS) {
|
||||
ModalActionCreators.push(
|
||||
modal(() => (
|
||||
<GenericErrorModal
|
||||
title={i18n._(msg`Cannot Join Community`)}
|
||||
message={i18n._(
|
||||
msg`This community requires you to verify your account before joining. Please set an email and password for your account.`,
|
||||
)}
|
||||
/>
|
||||
)),
|
||||
);
|
||||
} else if (errorCode === APIErrorCodes.UNCLAIMED_ACCOUNT_RESTRICTED) {
|
||||
} else if (isUnclaimedAccountInviteError(errorCode)) {
|
||||
ModalActionCreators.push(
|
||||
modal(() => (
|
||||
<GenericErrorModal
|
||||
@@ -189,15 +188,15 @@ export const acceptAndTransitionToChannel = async (code: string, i18n: I18n): Pr
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const openAcceptModal = async (code: string): Promise<void> => {
|
||||
export async function openAcceptModal(code: string): Promise<void> {
|
||||
void fetchWithCoalescing(code).catch(() => {});
|
||||
ModalActionCreators.pushWithKey(
|
||||
modal(() => <InviteAcceptModal code={code} />),
|
||||
`invite-accept-${code}`,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
interface HandlePackInviteErrorParams {
|
||||
invite: Invite | null;
|
||||
@@ -215,11 +214,13 @@ interface PackLimitPayload {
|
||||
const getPackLimitPayload = (httpError?: HttpError | null): PackLimitPayload | null => {
|
||||
const body = httpError?.body;
|
||||
if (!body || typeof body !== 'object') return null;
|
||||
const data = (body as {data?: unknown}).data;
|
||||
const record = body as Record<string, unknown>;
|
||||
const data = record.data;
|
||||
if (!data || typeof data !== 'object') return null;
|
||||
const limit = (data as {limit?: unknown}).limit;
|
||||
const packType = (data as {pack_type?: unknown}).pack_type;
|
||||
const action = (data as {action?: unknown}).action;
|
||||
const dataRecord = data as Record<string, unknown>;
|
||||
const limit = dataRecord.limit;
|
||||
const packType = dataRecord.pack_type;
|
||||
const action = dataRecord.action;
|
||||
return {
|
||||
packType: packType === 'emoji' || packType === 'sticker' ? packType : undefined,
|
||||
limit: typeof limit === 'number' ? limit : undefined,
|
||||
@@ -301,7 +302,7 @@ const buildPackLimitStrings = (
|
||||
}
|
||||
};
|
||||
|
||||
export const handlePackInviteError = (params: HandlePackInviteErrorParams): boolean => {
|
||||
export function handlePackInviteError(params: HandlePackInviteErrorParams): boolean {
|
||||
const {invite, errorCode, httpError, i18n} = params;
|
||||
if (!invite || !isPackInvite(invite)) {
|
||||
return false;
|
||||
@@ -321,18 +322,6 @@ export const handlePackInviteError = (params: HandlePackInviteErrorParams): bool
|
||||
? i18n._(msg`Failed to install this emoji pack. Please try again later.`)
|
||||
: i18n._(msg`Failed to install this sticker pack. Please try again later.`);
|
||||
|
||||
if (errorCode === APIErrorCodes.PREMIUM_REQUIRED) {
|
||||
ModalActionCreators.push(
|
||||
modal(() => (
|
||||
<GenericErrorModal
|
||||
title={i18n._(msg`Premium required`)}
|
||||
message={i18n._(msg`Installing emoji and sticker packs requires a premium subscription.`)}
|
||||
/>
|
||||
)),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (errorCode === APIErrorCodes.MISSING_ACCESS) {
|
||||
ModalActionCreators.push(
|
||||
modal(() => <GenericErrorModal title={cannotInstallTitle} message={cannotInstallMessage} />),
|
||||
@@ -351,21 +340,18 @@ export const handlePackInviteError = (params: HandlePackInviteErrorParams): bool
|
||||
return true;
|
||||
}
|
||||
|
||||
const fallbackMessage =
|
||||
httpError?.body && typeof httpError.body === 'object' && 'message' in httpError.body
|
||||
? (httpError.body as {message?: unknown}).message?.toString()
|
||||
: null;
|
||||
const fallbackMessage = httpError ? getApiErrorMessage(httpError) : null;
|
||||
|
||||
ModalActionCreators.push(
|
||||
modal(() => <GenericErrorModal title={defaultTitle} message={fallbackMessage || defaultMessage} />),
|
||||
);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
export const create = async (
|
||||
export async function create(
|
||||
channelId: string,
|
||||
params?: {max_age?: number; max_uses?: number; temporary?: boolean},
|
||||
): Promise<Invite> => {
|
||||
): Promise<Invite> {
|
||||
try {
|
||||
logger.debug(`Creating invite for channel ${channelId}`);
|
||||
const response = await http.post<Invite>(Endpoints.CHANNEL_INVITES(channelId), params ?? {});
|
||||
@@ -374,9 +360,9 @@ export const create = async (
|
||||
logger.error(`Failed to create invite for channel ${channelId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const list = async (channelId: string): Promise<Array<Invite>> => {
|
||||
export async function list(channelId: string): Promise<Array<Invite>> {
|
||||
try {
|
||||
logger.debug(`Listing invites for channel ${channelId}`);
|
||||
const response = await http.get<Array<Invite>>(Endpoints.CHANNEL_INVITES(channelId));
|
||||
@@ -385,9 +371,9 @@ export const list = async (channelId: string): Promise<Array<Invite>> => {
|
||||
logger.error(`Failed to list invites for channel ${channelId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const remove = async (code: string): Promise<void> => {
|
||||
export async function remove(code: string): Promise<void> {
|
||||
try {
|
||||
logger.debug(`Deleting invite with code ${code}`);
|
||||
await http.delete({url: Endpoints.INVITE(code)});
|
||||
@@ -395,4 +381,4 @@ export const remove = async (code: string): Promise<void> => {
|
||||
logger.error(`Failed to delete invite with code ${code}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,16 +17,16 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import * as LocaleUtils from '~/utils/LocaleUtils';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import * as LocaleUtils from '@app/utils/LocaleUtils';
|
||||
|
||||
const logger = new Logger('Tenor');
|
||||
const logger = new Logger('KLIPY');
|
||||
|
||||
const getLocale = (): string => LocaleUtils.getCurrentLocale();
|
||||
|
||||
export interface TenorGif {
|
||||
export interface KlipyGif {
|
||||
id: string;
|
||||
title: string;
|
||||
url: string;
|
||||
@@ -36,24 +36,24 @@ export interface TenorGif {
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface TenorCategory {
|
||||
interface KlipyCategory {
|
||||
name: string;
|
||||
src: string;
|
||||
proxy_src: string;
|
||||
}
|
||||
|
||||
export interface TenorFeatured {
|
||||
categories: Array<TenorCategory>;
|
||||
gifs: Array<TenorGif>;
|
||||
export interface KlipyFeatured {
|
||||
categories: Array<KlipyCategory>;
|
||||
gifs: Array<KlipyGif>;
|
||||
}
|
||||
|
||||
let tenorFeaturedCache: TenorFeatured | null = null;
|
||||
let klipyFeaturedCache: KlipyFeatured | null = null;
|
||||
|
||||
export const search = async (q: string): Promise<Array<TenorGif>> => {
|
||||
export async function search(q: string): Promise<Array<KlipyGif>> {
|
||||
try {
|
||||
logger.debug(`Searching for GIFs with query: "${q}"`);
|
||||
const response = await http.get<Array<TenorGif>>({
|
||||
url: Endpoints.TENOR_SEARCH,
|
||||
const response = await http.get<Array<KlipyGif>>({
|
||||
url: Endpoints.KLIPY_SEARCH,
|
||||
query: {q, locale: getLocale()},
|
||||
});
|
||||
const gifs = response.body;
|
||||
@@ -63,63 +63,63 @@ export const search = async (q: string): Promise<Array<TenorGif>> => {
|
||||
logger.error(`Failed to search for GIFs with query "${q}":`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const getFeatured = async (): Promise<TenorFeatured> => {
|
||||
if (tenorFeaturedCache) {
|
||||
logger.debug('Returning cached featured Tenor content');
|
||||
return tenorFeaturedCache;
|
||||
export async function getFeatured(): Promise<KlipyFeatured> {
|
||||
if (klipyFeaturedCache) {
|
||||
logger.debug('Returning cached featured KLIPY content');
|
||||
return klipyFeaturedCache;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.debug('Fetching featured Tenor content');
|
||||
const response = await http.get<TenorFeatured>({
|
||||
url: Endpoints.TENOR_FEATURED,
|
||||
logger.debug('Fetching featured KLIPY content');
|
||||
const response = await http.get<KlipyFeatured>({
|
||||
url: Endpoints.KLIPY_FEATURED,
|
||||
query: {locale: getLocale()},
|
||||
});
|
||||
const featured = response.body;
|
||||
tenorFeaturedCache = featured;
|
||||
klipyFeaturedCache = featured;
|
||||
logger.debug(
|
||||
`Fetched featured Tenor content: ${featured.categories.length} categories and ${featured.gifs.length} GIFs`,
|
||||
`Fetched featured KLIPY content: ${featured.categories.length} categories and ${featured.gifs.length} GIFs`,
|
||||
);
|
||||
return featured;
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch featured Tenor content:', error);
|
||||
logger.error('Failed to fetch featured KLIPY content:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const getTrending = async (): Promise<Array<TenorGif>> => {
|
||||
export async function getTrending(): Promise<Array<KlipyGif>> {
|
||||
try {
|
||||
logger.debug('Fetching trending Tenor GIFs');
|
||||
const response = await http.get<Array<TenorGif>>({
|
||||
url: Endpoints.TENOR_TRENDING_GIFS,
|
||||
logger.debug('Fetching trending KLIPY GIFs');
|
||||
const response = await http.get<Array<KlipyGif>>({
|
||||
url: Endpoints.KLIPY_TRENDING_GIFS,
|
||||
query: {locale: getLocale()},
|
||||
});
|
||||
const gifs = response.body;
|
||||
logger.debug(`Fetched ${gifs.length} trending Tenor GIFs`);
|
||||
logger.debug(`Fetched ${gifs.length} trending KLIPY GIFs`);
|
||||
return gifs;
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch trending Tenor GIFs:', error);
|
||||
logger.error('Failed to fetch trending KLIPY GIFs:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const registerShare = async (id: string, q: string): Promise<void> => {
|
||||
export async function registerShare(id: string, q: string): Promise<void> {
|
||||
try {
|
||||
logger.debug(`Registering GIF share: id=${id}, query="${q}"`);
|
||||
await http.post({url: Endpoints.TENOR_REGISTER_SHARE, body: {id, q, locale: getLocale()}});
|
||||
await http.post({url: Endpoints.KLIPY_REGISTER_SHARE, body: {id, q, locale: getLocale()}});
|
||||
logger.debug(`Successfully registered GIF share for id=${id}`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to register GIF share for id=${id}:`, error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const suggest = async (q: string): Promise<Array<string>> => {
|
||||
export async function suggest(q: string): Promise<Array<string>> {
|
||||
try {
|
||||
logger.debug(`Getting Tenor search suggestions for: "${q}"`);
|
||||
logger.debug(`Getting KLIPY search suggestions for: "${q}"`);
|
||||
const response = await http.get<Array<string>>({
|
||||
url: Endpoints.TENOR_SUGGEST,
|
||||
url: Endpoints.KLIPY_SUGGEST,
|
||||
query: {q, locale: getLocale()},
|
||||
});
|
||||
const suggestions = response.body;
|
||||
@@ -129,4 +129,4 @@ export const suggest = async (q: string): Promise<Array<string>> => {
|
||||
logger.error(`Failed to get suggestions for query "${q}":`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -17,17 +17,17 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import MemberListStore from '~/stores/MemberListStore';
|
||||
import MobileLayoutStore from '~/stores/MobileLayoutStore';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import MemberListStore from '@app/stores/MemberListStore';
|
||||
import MobileLayoutStore from '@app/stores/MobileLayoutStore';
|
||||
|
||||
const logger = new Logger('Layout');
|
||||
|
||||
export const updateMobileLayoutState = (navExpanded: boolean, chatExpanded: boolean): void => {
|
||||
export function updateMobileLayoutState(navExpanded: boolean, chatExpanded: boolean): void {
|
||||
logger.debug(`Updating mobile layout state: nav=${navExpanded}, chat=${chatExpanded}`);
|
||||
MobileLayoutStore.updateState({navExpanded, chatExpanded});
|
||||
};
|
||||
}
|
||||
|
||||
export const toggleMembers = (_isOpen: boolean): void => {
|
||||
export function toggleMembers(_isOpen: boolean): void {
|
||||
MemberListStore.toggleMembers();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {MessageRecord} from '~/records/MessageRecord';
|
||||
import MediaViewerStore, {type MediaViewerItem} from '~/stores/MediaViewerStore';
|
||||
import type {MessageRecord} from '@app/records/MessageRecord';
|
||||
import MediaViewerStore, {type MediaViewerItem} from '@app/stores/MediaViewerStore';
|
||||
|
||||
export function openMediaViewer(
|
||||
items: ReadonlyArray<MediaViewerItem>,
|
||||
|
||||
@@ -17,45 +17,63 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {I18n} from '@lingui/core';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {modal} from '~/actions/ModalActionCreators';
|
||||
import * as ReadStateActionCreators from '~/actions/ReadStateActionCreators';
|
||||
import {APIErrorCodes, type JumpTypes, MAX_MESSAGES_PER_CHANNEL, MessageFlags} from '~/Constants';
|
||||
import {FeatureTemporarilyDisabledModal} from '~/components/alerts/FeatureTemporarilyDisabledModal';
|
||||
import {MessageDeleteFailedModal} from '~/components/alerts/MessageDeleteFailedModal';
|
||||
import {MessageDeleteTooQuickModal} from '~/components/alerts/MessageDeleteTooQuickModal';
|
||||
import {ConfirmModal} from '~/components/modals/ConfirmModal';
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import type {JumpOptions} from '~/lib/ChannelMessages';
|
||||
import http, {HttpError} from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import MessageQueue from '~/lib/MessageQueue';
|
||||
import * as ModalActionCreators from '@app/actions/ModalActionCreators';
|
||||
import {modal} from '@app/actions/ModalActionCreators';
|
||||
import * as NavigationActionCreators from '@app/actions/NavigationActionCreators';
|
||||
import * as ReadStateActionCreators from '@app/actions/ReadStateActionCreators';
|
||||
import {FeatureTemporarilyDisabledModal} from '@app/components/alerts/FeatureTemporarilyDisabledModal';
|
||||
import {MessageDeleteFailedModal} from '@app/components/alerts/MessageDeleteFailedModal';
|
||||
import {MessageDeleteTooQuickModal} from '@app/components/alerts/MessageDeleteTooQuickModal';
|
||||
import {ConfirmModal} from '@app/components/modals/ConfirmModal';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import type {JumpOptions} from '@app/lib/ChannelMessages';
|
||||
import {ComponentDispatch} from '@app/lib/ComponentDispatch';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {HttpError} from '@app/lib/HttpError';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import MessageQueue from '@app/lib/MessageQueue';
|
||||
import type {MessageRecord} from '@app/records/MessageRecord';
|
||||
import AuthenticationStore from '@app/stores/AuthenticationStore';
|
||||
import ChannelStore from '@app/stores/ChannelStore';
|
||||
import DeveloperOptionsStore from '@app/stores/DeveloperOptionsStore';
|
||||
import GuildMemberStore from '@app/stores/GuildMemberStore';
|
||||
import GuildNSFWAgreeStore from '@app/stores/GuildNSFWAgreeStore';
|
||||
import MessageEditMobileStore from '@app/stores/MessageEditMobileStore';
|
||||
import MessageEditStore from '@app/stores/MessageEditStore';
|
||||
import MessageReferenceStore from '@app/stores/MessageReferenceStore';
|
||||
import MessageReplyStore from '@app/stores/MessageReplyStore';
|
||||
import MessageStore from '@app/stores/MessageStore';
|
||||
import ReadStateStore from '@app/stores/ReadStateStore';
|
||||
import {getApiErrorCode} from '@app/utils/ApiErrorUtils';
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {MessageFlags} from '@fluxer/constants/src/ChannelConstants';
|
||||
import type {JumpType} from '@fluxer/constants/src/JumpConstants';
|
||||
import {MAX_MESSAGES_PER_CHANNEL} from '@fluxer/constants/src/LimitConstants';
|
||||
import type {MessageId} from '@fluxer/schema/src/branded/WireIds';
|
||||
import type {
|
||||
AllowedMentions,
|
||||
Message,
|
||||
MessageRecord,
|
||||
MessageReference,
|
||||
MessageStickerItem,
|
||||
} from '~/records/MessageRecord';
|
||||
import AuthenticationStore from '~/stores/AuthenticationStore';
|
||||
import ChannelStore from '~/stores/ChannelStore';
|
||||
import DeveloperOptionsStore from '~/stores/DeveloperOptionsStore';
|
||||
import GuildMemberStore from '~/stores/GuildMemberStore';
|
||||
import MessageEditMobileStore from '~/stores/MessageEditMobileStore';
|
||||
import MessageEditStore from '~/stores/MessageEditStore';
|
||||
import MessageReferenceStore from '~/stores/MessageReferenceStore';
|
||||
import MessageReplyStore from '~/stores/MessageReplyStore';
|
||||
import MessageStore from '~/stores/MessageStore';
|
||||
import ReadStateStore from '~/stores/ReadStateStore';
|
||||
import * as SnowflakeUtils from '~/utils/SnowflakeUtils';
|
||||
} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
|
||||
import * as SnowflakeUtils from '@fluxer/snowflake/src/SnowflakeUtils';
|
||||
import type {I18n} from '@lingui/core';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
|
||||
const logger = new Logger('MessageActionCreators');
|
||||
|
||||
const pendingDeletePromises = new Map<string, Promise<void>>();
|
||||
const pendingFetchPromises = new Map<string, Promise<Array<Message>>>();
|
||||
|
||||
function shouldBlockMessageFetch(channelId: string): boolean {
|
||||
const channel = ChannelStore.getChannel(channelId);
|
||||
if (!channel || channel.isPrivate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return GuildNSFWAgreeStore.shouldShowGate({channelId: channel.id, guildId: channel.guildId ?? null});
|
||||
}
|
||||
|
||||
function makeFetchKey(
|
||||
channelId: string,
|
||||
before: string | null,
|
||||
@@ -113,7 +131,9 @@ interface SendMessageParams {
|
||||
tts?: boolean;
|
||||
}
|
||||
|
||||
export const jumpToPresent = (channelId: string, limit = MAX_MESSAGES_PER_CHANNEL): void => {
|
||||
export function jumpToPresent(channelId: string, limit = MAX_MESSAGES_PER_CHANNEL): void {
|
||||
NavigationActionCreators.clearMessageIdForChannel(channelId);
|
||||
|
||||
logger.debug(`Jumping to present in channel ${channelId}`);
|
||||
ReadStateActionCreators.clearStickyUnread(channelId);
|
||||
|
||||
@@ -126,26 +146,26 @@ export const jumpToPresent = (channelId: string, limit = MAX_MESSAGES_PER_CHANNE
|
||||
} else {
|
||||
fetchMessages(channelId, null, null, limit, jump);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const jumpToMessage = (
|
||||
export function jumpToMessage(
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
flash = true,
|
||||
offset?: number,
|
||||
returnTargetId?: string,
|
||||
jumpType?: JumpTypes,
|
||||
): void => {
|
||||
jumpType?: JumpType,
|
||||
): void {
|
||||
logger.debug(`Jumping to message ${messageId} in channel ${channelId}`);
|
||||
|
||||
fetchMessages(channelId, null, null, MAX_MESSAGES_PER_CHANNEL, {
|
||||
messageId,
|
||||
messageId: messageId as MessageId,
|
||||
flash,
|
||||
offset,
|
||||
returnMessageId: returnTargetId,
|
||||
returnMessageId: returnTargetId as MessageId | null | undefined,
|
||||
jumpType,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const tryFetchMessagesCached = (
|
||||
channelId: string,
|
||||
@@ -170,13 +190,13 @@ const tryFetchMessagesCached = (
|
||||
return false;
|
||||
};
|
||||
|
||||
export const fetchMessages = async (
|
||||
export async function fetchMessages(
|
||||
channelId: string,
|
||||
before: string | null,
|
||||
after: string | null,
|
||||
limit: number,
|
||||
jump?: JumpOptions,
|
||||
): Promise<Array<Message>> => {
|
||||
): Promise<Array<Message>> {
|
||||
const key = makeFetchKey(channelId, before, after, limit, jump);
|
||||
const inFlight = pendingFetchPromises.get(key);
|
||||
if (inFlight) {
|
||||
@@ -184,6 +204,12 @@ export const fetchMessages = async (
|
||||
return inFlight;
|
||||
}
|
||||
|
||||
if (shouldBlockMessageFetch(channelId)) {
|
||||
logger.debug(`Skipping message fetch for gated channel ${channelId}`);
|
||||
MessageStore.handleLoadMessagesBlocked({channelId});
|
||||
return [];
|
||||
}
|
||||
|
||||
if (tryFetchMessagesCached(channelId, before, after, limit, jump)) {
|
||||
return [];
|
||||
}
|
||||
@@ -271,10 +297,10 @@ export const fetchMessages = async (
|
||||
pendingFetchPromises.set(key, promise);
|
||||
promise.finally(() => pendingFetchPromises.delete(key));
|
||||
return promise;
|
||||
};
|
||||
}
|
||||
|
||||
export const send = async (channelId: string, params: SendMessageParams): Promise<Message> => {
|
||||
const promise = new Promise<Message>((resolve, reject) => {
|
||||
export function send(channelId: string, params: SendMessageParams): Promise<Message | null> {
|
||||
return new Promise<Message | null>((resolve) => {
|
||||
logger.debug(`Enqueueing message for channel ${channelId}`);
|
||||
|
||||
MessageQueue.enqueue(
|
||||
@@ -296,28 +322,18 @@ export const send = async (channelId: string, params: SendMessageParams): Promis
|
||||
logger.debug(`Message sent successfully in channel ${channelId}`);
|
||||
resolve(result.body);
|
||||
} else {
|
||||
const reason = error ?? new Error('Message send failed');
|
||||
logger.error(`Message send failed in channel ${channelId}`, reason);
|
||||
reject(reason);
|
||||
if (error) {
|
||||
logger.debug(`Message send failed in channel ${channelId}`, error);
|
||||
}
|
||||
resolve(null);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
promise.catch((error) => {
|
||||
logger.error(`Unhandled message send rejection in channel ${channelId}`, error);
|
||||
});
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
export const edit = async (
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
content?: string,
|
||||
flags?: number,
|
||||
): Promise<Message> => {
|
||||
const promise = new Promise<Message>((resolve, reject) => {
|
||||
export function edit(channelId: string, messageId: string, content?: string, flags?: number): Promise<Message | null> {
|
||||
return new Promise<Message | null>((resolve) => {
|
||||
logger.debug(`Enqueueing edit for message ${messageId} in channel ${channelId}`);
|
||||
|
||||
MessageQueue.enqueue(
|
||||
@@ -333,22 +349,17 @@ export const edit = async (
|
||||
logger.debug(`Message edited successfully: ${messageId} in channel ${channelId}`);
|
||||
resolve(result.body);
|
||||
} else {
|
||||
const reason = error ?? new Error('Message edit failed');
|
||||
logger.error(`Message edit failed: ${messageId} in channel ${channelId}`, reason);
|
||||
reject(reason);
|
||||
if (error) {
|
||||
logger.debug(`Message edit failed: ${messageId} in channel ${channelId}`, error);
|
||||
}
|
||||
resolve(null);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
promise.catch((error) => {
|
||||
logger.error(`Unhandled message edit rejection for ${messageId} in channel ${channelId}`, error);
|
||||
});
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
export const remove = async (channelId: string, messageId: string): Promise<void> => {
|
||||
export async function remove(channelId: string, messageId: string): Promise<void> {
|
||||
const pendingPromise = pendingDeletePromises.get(messageId);
|
||||
if (pendingPromise) {
|
||||
logger.debug(`Using in-flight delete request for message ${messageId}`);
|
||||
@@ -364,9 +375,8 @@ export const remove = async (channelId: string, messageId: string): Promise<void
|
||||
logger.error(`Failed to delete message ${messageId} in channel ${channelId}:`, error);
|
||||
|
||||
if (error instanceof HttpError) {
|
||||
const {status, body} = error;
|
||||
const errorCode =
|
||||
typeof body === 'object' && body != null && 'code' in body ? (body as {code?: string}).code : undefined;
|
||||
const {status} = error;
|
||||
const errorCode = getApiErrorCode(error);
|
||||
|
||||
if (status === 429) {
|
||||
ModalActionCreators.push(modal(() => <MessageDeleteTooQuickModal />));
|
||||
@@ -389,14 +399,14 @@ export const remove = async (channelId: string, messageId: string): Promise<void
|
||||
|
||||
pendingDeletePromises.set(messageId, deletePromise);
|
||||
return deletePromise;
|
||||
};
|
||||
}
|
||||
|
||||
interface ShowDeleteConfirmationOptions {
|
||||
message: MessageRecord;
|
||||
onDelete?: () => void;
|
||||
}
|
||||
|
||||
export const showDeleteConfirmation = (i18n: I18n, {message, onDelete}: ShowDeleteConfirmationOptions): void => {
|
||||
export function showDeleteConfirmation(i18n: I18n, {message, onDelete}: ShowDeleteConfirmationOptions): void {
|
||||
ModalActionCreators.push(
|
||||
modal(() => (
|
||||
<ConfirmModal
|
||||
@@ -411,92 +421,100 @@ export const showDeleteConfirmation = (i18n: I18n, {message, onDelete}: ShowDele
|
||||
/>
|
||||
)),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const deleteLocal = (channelId: string, messageId: string): void => {
|
||||
export function deleteLocal(channelId: string, messageId: string): void {
|
||||
logger.debug(`Deleting message ${messageId} locally in channel ${channelId}`);
|
||||
MessageStore.handleMessageDelete({id: messageId, channelId});
|
||||
};
|
||||
}
|
||||
|
||||
export const revealMessage = (channelId: string, messageId: string | null): void => {
|
||||
export function revealMessage(channelId: string, messageId: string | null): void {
|
||||
logger.debug(`Revealing message ${messageId} in channel ${channelId}`);
|
||||
MessageStore.handleMessageReveal({channelId, messageId});
|
||||
};
|
||||
}
|
||||
|
||||
export const startReply = (channelId: string, messageId: string, mentioning: boolean): void => {
|
||||
export function startReply(channelId: string, messageId: string, mentioning: boolean): void {
|
||||
logger.debug(`Starting reply to message ${messageId} in channel ${channelId}, mentioning=${mentioning}`);
|
||||
MessageReplyStore.startReply(channelId, messageId, mentioning);
|
||||
};
|
||||
ComponentDispatch.dispatch('FOCUS_TEXTAREA', {channelId});
|
||||
}
|
||||
|
||||
export const stopReply = (channelId: string): void => {
|
||||
export function stopReply(channelId: string): void {
|
||||
logger.debug(`Stopping reply in channel ${channelId}`);
|
||||
MessageReplyStore.stopReply(channelId);
|
||||
};
|
||||
}
|
||||
|
||||
export const setReplyMentioning = (channelId: string, mentioning: boolean): void => {
|
||||
export function setReplyMentioning(channelId: string, mentioning: boolean): void {
|
||||
logger.debug(`Setting reply mentioning in channel ${channelId}: ${mentioning}`);
|
||||
MessageReplyStore.setMentioning(channelId, mentioning);
|
||||
};
|
||||
}
|
||||
|
||||
export const startEdit = (channelId: string, messageId: string, initialContent: string): void => {
|
||||
export function startEdit(channelId: string, messageId: string, initialContent: string): void {
|
||||
logger.debug(`Starting edit for message ${messageId} in channel ${channelId}`);
|
||||
MessageEditStore.startEditing(channelId, messageId, initialContent);
|
||||
};
|
||||
const draftContent = MessageEditStore.getDraftContent(messageId);
|
||||
const contentToUse = draftContent ?? initialContent;
|
||||
MessageEditStore.startEditing(channelId, messageId, contentToUse);
|
||||
}
|
||||
|
||||
export const stopEdit = (channelId: string): void => {
|
||||
export function stopEdit(channelId: string): void {
|
||||
logger.debug(`Stopping edit in channel ${channelId}`);
|
||||
MessageEditStore.stopEditing(channelId);
|
||||
};
|
||||
}
|
||||
|
||||
export const startEditMobile = (channelId: string, messageId: string): void => {
|
||||
export function startEditMobile(channelId: string, messageId: string): void {
|
||||
logger.debug(`Starting mobile edit for message ${messageId} in channel ${channelId}`);
|
||||
MessageEditMobileStore.startEditingMobile(channelId, messageId);
|
||||
};
|
||||
}
|
||||
|
||||
export const stopEditMobile = (channelId: string): void => {
|
||||
export function stopEditMobile(channelId: string): void {
|
||||
logger.debug(`Stopping mobile edit in channel ${channelId}`);
|
||||
MessageEditMobileStore.stopEditingMobile(channelId);
|
||||
};
|
||||
}
|
||||
|
||||
export const createOptimistic = (channelId: string, message: Message): void => {
|
||||
export function createOptimistic(channelId: string, message: Message): void {
|
||||
logger.debug(`Creating optimistic message in channel ${channelId}`);
|
||||
MessageStore.handleIncomingMessage({channelId, message});
|
||||
};
|
||||
}
|
||||
|
||||
export const deleteOptimistic = (channelId: string, messageId: string): void => {
|
||||
export function deleteOptimistic(channelId: string, messageId: string): void {
|
||||
logger.debug(`Deleting optimistic message ${messageId} in channel ${channelId}`);
|
||||
MessageStore.handleMessageDelete({channelId, id: messageId});
|
||||
};
|
||||
}
|
||||
|
||||
export const sendError = (channelId: string, nonce: string): void => {
|
||||
export function sendError(channelId: string, nonce: string): void {
|
||||
logger.debug(`Message send error for nonce ${nonce} in channel ${channelId}`);
|
||||
MessageStore.handleSendFailed({channelId, nonce});
|
||||
};
|
||||
}
|
||||
|
||||
export const editOptimistic = (
|
||||
export function retryLocal(channelId: string, messageId: string): void {
|
||||
logger.debug(`Retrying optimistic message ${messageId} in channel ${channelId}`);
|
||||
MessageStore.handleSendRetry({channelId, messageId});
|
||||
}
|
||||
|
||||
export function editOptimistic(
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
content: string,
|
||||
): {originalContent: string; originalEditedTimestamp: string | null} | null => {
|
||||
): {originalContent: string; originalEditedTimestamp: string | null} | null {
|
||||
logger.debug(`Applying optimistic edit for message ${messageId} in channel ${channelId}`);
|
||||
return MessageStore.handleOptimisticEdit({channelId, messageId, content});
|
||||
};
|
||||
}
|
||||
|
||||
export const editRollback = (
|
||||
export function editRollback(
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
originalContent: string,
|
||||
originalEditedTimestamp: string | null,
|
||||
): void => {
|
||||
): void {
|
||||
logger.debug(`Rolling back edit for message ${messageId} in channel ${channelId}`);
|
||||
MessageStore.handleEditRollback({channelId, messageId, originalContent, originalEditedTimestamp});
|
||||
};
|
||||
}
|
||||
|
||||
export const forward = async (
|
||||
export async function forward(
|
||||
channelIds: Array<string>,
|
||||
messageReference: {message_id: string; channel_id: string; guild_id?: string | null},
|
||||
optionalMessage?: string,
|
||||
): Promise<void> => {
|
||||
): Promise<void> {
|
||||
logger.debug(`Forwarding message ${messageReference.message_id} to ${channelIds.length} channels`);
|
||||
|
||||
try {
|
||||
@@ -527,13 +545,9 @@ export const forward = async (
|
||||
logger.error('Failed to forward message:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const toggleSuppressEmbeds = async (
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
currentFlags: number,
|
||||
): Promise<void> => {
|
||||
export async function toggleSuppressEmbeds(channelId: string, messageId: string, currentFlags: number): Promise<void> {
|
||||
try {
|
||||
const isSuppressed = (currentFlags & MessageFlags.SUPPRESS_EMBEDS) === MessageFlags.SUPPRESS_EMBEDS;
|
||||
const newFlags = isSuppressed
|
||||
@@ -552,9 +566,9 @@ export const toggleSuppressEmbeds = async (
|
||||
logger.error('Failed to toggle suppress embeds:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const deleteAttachment = async (channelId: string, messageId: string, attachmentId: string): Promise<void> => {
|
||||
export async function deleteAttachment(channelId: string, messageId: string, attachmentId: string): Promise<void> {
|
||||
try {
|
||||
logger.debug(`Deleting attachment ${attachmentId} from message ${messageId}`);
|
||||
|
||||
@@ -567,4 +581,4 @@ export const deleteAttachment = async (channelId: string, messageId: string, att
|
||||
logger.error('Failed to delete attachment:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,15 +17,15 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import type {BackupCode} from '~/records/UserRecord';
|
||||
import SudoStore from '~/stores/SudoStore';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import SudoStore from '@app/stores/SudoStore';
|
||||
import type {BackupCode} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
|
||||
|
||||
const logger = new Logger('MFA');
|
||||
|
||||
export const enableMfaTotp = async (secret: string, code: string): Promise<Array<BackupCode>> => {
|
||||
export async function enableMfaTotp(secret: string, code: string): Promise<Array<BackupCode>> {
|
||||
try {
|
||||
logger.debug('Enabling TOTP-based MFA');
|
||||
const response = await http.post<{backup_codes: Array<BackupCode>}>({
|
||||
@@ -40,9 +40,9 @@ export const enableMfaTotp = async (secret: string, code: string): Promise<Array
|
||||
logger.error('Failed to enable TOTP-based MFA:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const disableMfaTotp = async (code: string): Promise<void> => {
|
||||
export async function disableMfaTotp(code: string): Promise<void> {
|
||||
try {
|
||||
logger.debug('Disabling TOTP-based MFA');
|
||||
await http.post({url: Endpoints.USER_MFA_TOTP_DISABLE, body: {code}});
|
||||
@@ -51,9 +51,9 @@ export const disableMfaTotp = async (code: string): Promise<void> => {
|
||||
logger.error('Failed to disable TOTP-based MFA:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const getBackupCodes = async (regenerate = false): Promise<Array<BackupCode>> => {
|
||||
export async function getBackupCodes(regenerate = false): Promise<Array<BackupCode>> {
|
||||
try {
|
||||
logger.debug(`${regenerate ? 'Regenerating' : 'Fetching'} MFA backup codes`);
|
||||
const response = await http.post<{backup_codes: Array<BackupCode>}>({
|
||||
@@ -68,4 +68,4 @@ export const getBackupCodes = async (regenerate = false): Promise<Array<BackupCo
|
||||
logger.error(`Failed to ${regenerate ? 'regenerate' : 'fetch'} MFA backup codes:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,13 +17,14 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {ModalRender} from '@app/actions/ModalRender';
|
||||
import {ChannelSettingsModal} from '@app/components/modals/ChannelSettingsModal';
|
||||
import {GuildSettingsModal} from '@app/components/modals/GuildSettingsModal';
|
||||
import {UserSettingsModal} from '@app/components/modals/UserSettingsModal';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import ModalStore from '@app/stores/ModalStore';
|
||||
import lodash from 'lodash';
|
||||
import type React from 'react';
|
||||
import {ChannelSettingsModal} from '~/components/modals/ChannelSettingsModal';
|
||||
import {GuildSettingsModal} from '~/components/modals/GuildSettingsModal';
|
||||
import {UserSettingsModal} from '~/components/modals/UserSettingsModal';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import ModalStore from '~/stores/ModalStore';
|
||||
|
||||
const logger = new Logger('Modal');
|
||||
|
||||
@@ -33,18 +34,11 @@ const isBackgroundModal = (element: React.ReactElement): boolean => {
|
||||
return BACKGROUND_MODAL_TYPES.some((type) => element.type === type);
|
||||
};
|
||||
|
||||
declare const ModalRenderBrand: unique symbol;
|
||||
|
||||
export interface ModalRender {
|
||||
(): React.ReactElement;
|
||||
[ModalRenderBrand]: true;
|
||||
}
|
||||
|
||||
export function modal(render: () => React.ReactElement): ModalRender {
|
||||
return render as ModalRender;
|
||||
}
|
||||
|
||||
export const push = (modal: ModalRender): void => {
|
||||
export function push(modal: ModalRender): void {
|
||||
const renderedModal = modal();
|
||||
const isBackground = isBackgroundModal(renderedModal);
|
||||
|
||||
@@ -64,9 +58,9 @@ export const push = (modal: ModalRender): void => {
|
||||
const key = lodash.uniqueId('modal');
|
||||
logger.debug(`Pushing modal: ${key} (background=${isBackground})`);
|
||||
ModalStore.push(modal, key, {isBackground});
|
||||
};
|
||||
}
|
||||
|
||||
export const pushWithKey = (modal: ModalRender, key: string): void => {
|
||||
export function pushWithKey(modal: ModalRender, key: string): void {
|
||||
const renderedModal = modal();
|
||||
const isBackground = isBackgroundModal(renderedModal);
|
||||
|
||||
@@ -91,24 +85,29 @@ export const pushWithKey = (modal: ModalRender, key: string): void => {
|
||||
|
||||
logger.debug(`Pushing modal with key: ${key} (background=${isBackground})`);
|
||||
ModalStore.push(modal, key, {isBackground});
|
||||
};
|
||||
}
|
||||
|
||||
export const update = (key: string, updater: (currentModal: ModalRender) => ModalRender): void => {
|
||||
export function update(key: string, updater: (currentModal: ModalRender) => ModalRender): void {
|
||||
logger.debug(`Updating modal with key: ${key}`);
|
||||
ModalStore.update(key, updater);
|
||||
};
|
||||
}
|
||||
|
||||
export const pop = (): void => {
|
||||
export function pop(): void {
|
||||
logger.debug('Popping most recent modal');
|
||||
ModalStore.pop();
|
||||
};
|
||||
}
|
||||
|
||||
export const popWithKey = (key: string): void => {
|
||||
export function popWithKey(key: string): void {
|
||||
logger.debug(`Popping modal with key: ${key}`);
|
||||
ModalStore.pop(key);
|
||||
};
|
||||
}
|
||||
|
||||
export const popAll = (): void => {
|
||||
export function popByType<T>(component: React.ComponentType<T>): void {
|
||||
logger.debug(`Popping modal by type: ${component.displayName ?? component.name ?? 'unknown'}`);
|
||||
ModalStore.popByType(component);
|
||||
}
|
||||
|
||||
export function popAll(): void {
|
||||
logger.debug('Popping all modals');
|
||||
ModalStore.popAll();
|
||||
};
|
||||
}
|
||||
|
||||
22
fluxer_app/src/actions/ModalRender.tsx
Normal file
22
fluxer_app/src/actions/ModalRender.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
export type ModalRender = () => React.ReactElement;
|
||||
@@ -17,32 +17,32 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import NagbarStore, {type NagbarToggleKey} from '~/stores/NagbarStore';
|
||||
import NagbarStore, {type NagbarToggleKey} from '@app/stores/NagbarStore';
|
||||
|
||||
export const dismissNagbar = (nagbarType: NagbarToggleKey): void => {
|
||||
export function dismissNagbar(nagbarType: NagbarToggleKey): void {
|
||||
NagbarStore.dismiss(nagbarType);
|
||||
};
|
||||
}
|
||||
|
||||
export const dismissInvitesDisabledNagbar = (guildId: string): void => {
|
||||
export function dismissInvitesDisabledNagbar(guildId: string): void {
|
||||
NagbarStore.dismissInvitesDisabled(guildId);
|
||||
};
|
||||
}
|
||||
|
||||
export const resetNagbar = (nagbarType: NagbarToggleKey): void => {
|
||||
export function resetNagbar(nagbarType: NagbarToggleKey): void {
|
||||
NagbarStore.reset(nagbarType);
|
||||
};
|
||||
}
|
||||
|
||||
export const resetAllNagbars = (): void => {
|
||||
export function resetAllNagbars(): void {
|
||||
NagbarStore.resetAll();
|
||||
};
|
||||
}
|
||||
|
||||
export const setForceHideNagbar = (key: NagbarToggleKey, value: boolean): void => {
|
||||
export function setForceHideNagbar(key: NagbarToggleKey, value: boolean): void {
|
||||
NagbarStore.setFlag(key, value);
|
||||
};
|
||||
}
|
||||
|
||||
export const dismissPendingBulkDeletionNagbar = (scheduleKey: string): void => {
|
||||
export function dismissPendingBulkDeletionNagbar(scheduleKey: string): void {
|
||||
NagbarStore.dismissPendingBulkDeletion(scheduleKey);
|
||||
};
|
||||
}
|
||||
|
||||
export const clearPendingBulkDeletionNagbarDismissal = (scheduleKey: string): void => {
|
||||
export function clearPendingBulkDeletionNagbarDismissal(scheduleKey: string): void {
|
||||
NagbarStore.clearPendingBulkDeletionDismissed(scheduleKey);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,27 +17,66 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import MessageStore from '~/stores/MessageStore';
|
||||
import NotificationStore from '~/stores/NotificationStore';
|
||||
import SelectedChannelStore from '~/stores/SelectedChannelStore';
|
||||
import SelectedGuildStore from '~/stores/SelectedGuildStore';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import NavigationStore from '@app/stores/NavigationStore';
|
||||
import {FAVORITES_GUILD_ID, ME} from '@fluxer/constants/src/AppConstants';
|
||||
|
||||
const logger = new Logger('Navigation');
|
||||
|
||||
export const selectChannel = (guildId?: string, channelId?: string | null, messageId?: string): void => {
|
||||
type NavigationMode = 'push' | 'replace';
|
||||
|
||||
export function selectChannel(
|
||||
guildId?: string,
|
||||
channelId?: string | null,
|
||||
messageId?: string,
|
||||
mode: NavigationMode = 'push',
|
||||
): void {
|
||||
logger.debug(`Selecting channel: guildId=${guildId}, channelId=${channelId}, messageId=${messageId}`);
|
||||
MessageStore.handleChannelSelect({guildId, channelId, messageId});
|
||||
NotificationStore.handleChannelSelect({channelId});
|
||||
SelectedChannelStore.selectChannel(guildId, channelId);
|
||||
};
|
||||
|
||||
export const selectGuild = (guildId: string): void => {
|
||||
if (!guildId || guildId === ME) {
|
||||
NavigationStore.navigateToDM(channelId ?? undefined, messageId, mode);
|
||||
} else if (guildId === FAVORITES_GUILD_ID || guildId === '@favorites') {
|
||||
NavigationStore.navigateToFavorites(channelId ?? undefined, messageId, mode);
|
||||
} else {
|
||||
NavigationStore.navigateToGuild(guildId, channelId ?? undefined, messageId, mode);
|
||||
}
|
||||
}
|
||||
|
||||
export function selectGuild(guildId: string, channelId?: string, mode: NavigationMode = 'push'): void {
|
||||
logger.debug(`Selecting guild: ${guildId}`);
|
||||
SelectedGuildStore.selectGuild(guildId);
|
||||
};
|
||||
|
||||
export const deselectGuild = (): void => {
|
||||
if (guildId === ME) {
|
||||
NavigationStore.navigateToDM(channelId, undefined, mode);
|
||||
} else if (guildId === FAVORITES_GUILD_ID || guildId === '@favorites') {
|
||||
NavigationStore.navigateToFavorites(channelId, undefined, mode);
|
||||
} else {
|
||||
NavigationStore.navigateToGuild(guildId, channelId, undefined, mode);
|
||||
}
|
||||
}
|
||||
|
||||
export function deselectGuild(): void {
|
||||
logger.debug('Deselecting guild');
|
||||
SelectedGuildStore.deselectGuild();
|
||||
};
|
||||
NavigationStore.navigateToDM();
|
||||
}
|
||||
|
||||
export function navigateToMessage(
|
||||
guildId: string | null | undefined,
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
mode: NavigationMode = 'push',
|
||||
): void {
|
||||
logger.debug(`Navigating to message: channel=${channelId}, message=${messageId}`);
|
||||
|
||||
if (!guildId || guildId === ME) {
|
||||
NavigationStore.navigateToDM(channelId, messageId, mode);
|
||||
} else if (guildId === FAVORITES_GUILD_ID || guildId === '@favorites') {
|
||||
NavigationStore.navigateToFavorites(channelId, messageId, mode);
|
||||
} else {
|
||||
NavigationStore.navigateToGuild(guildId, channelId, messageId, mode);
|
||||
}
|
||||
}
|
||||
|
||||
export function clearMessageIdForChannel(channelId: string, mode: NavigationMode = 'replace'): void {
|
||||
logger.debug(`Clearing messageId for channel: ${channelId}`);
|
||||
NavigationStore.clearMessageIdForChannel(channelId, mode);
|
||||
}
|
||||
|
||||
@@ -17,18 +17,18 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as ModalActionCreators from '@app/actions/ModalActionCreators';
|
||||
import {modal} from '@app/actions/ModalActionCreators';
|
||||
import {ConfirmModal} from '@app/components/modals/ConfirmModal';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import NotificationStore from '@app/stores/NotificationStore';
|
||||
import type {I18n} from '@lingui/core';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import {Trans} from '@lingui/react/macro';
|
||||
import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {modal} from '~/actions/ModalActionCreators';
|
||||
import {ConfirmModal} from '~/components/modals/ConfirmModal';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import NotificationStore from '~/stores/NotificationStore';
|
||||
|
||||
const logger = new Logger('Notification');
|
||||
|
||||
export const permissionDenied = (i18n: I18n, suppressModal = false): void => {
|
||||
export function permissionDenied(i18n: I18n, suppressModal = false): void {
|
||||
logger.debug('Notification permission denied');
|
||||
NotificationStore.handleNotificationPermissionDenied();
|
||||
|
||||
@@ -53,13 +53,13 @@ export const permissionDenied = (i18n: I18n, suppressModal = false): void => {
|
||||
/>
|
||||
)),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const permissionGranted = (): void => {
|
||||
export function permissionGranted(): void {
|
||||
logger.debug('Notification permission granted');
|
||||
NotificationStore.handleNotificationPermissionGranted();
|
||||
};
|
||||
}
|
||||
|
||||
export const toggleUnreadMessageBadge = (enabled: boolean): void => {
|
||||
export function toggleUnreadMessageBadge(enabled: boolean): void {
|
||||
NotificationStore.handleNotificationSoundToggle(enabled);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
|
||||
const logger = new Logger('OAuth2AuthorizationActionCreators');
|
||||
|
||||
@@ -35,7 +35,7 @@ export interface OAuth2Authorization {
|
||||
authorized_at: string;
|
||||
}
|
||||
|
||||
export const listAuthorizations = async (): Promise<Array<OAuth2Authorization>> => {
|
||||
export async function listAuthorizations(): Promise<Array<OAuth2Authorization>> {
|
||||
try {
|
||||
const response = await http.get<Array<OAuth2Authorization>>({url: Endpoints.OAUTH_AUTHORIZATIONS});
|
||||
return response.body;
|
||||
@@ -43,13 +43,13 @@ export const listAuthorizations = async (): Promise<Array<OAuth2Authorization>>
|
||||
logger.error('Failed to list OAuth2 authorizations:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const deauthorize = async (applicationId: string): Promise<void> => {
|
||||
export async function deauthorize(applicationId: string): Promise<void> {
|
||||
try {
|
||||
await http.delete({url: Endpoints.OAUTH_AUTHORIZATION(applicationId)});
|
||||
} catch (error) {
|
||||
logger.error('Failed to deauthorize application:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import type {PackDashboardResponse, PackSummary} from '~/types/PackTypes';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import type {PackDashboardResponse, PackSummaryResponse} from '@fluxer/schema/src/domains/pack/PackSchemas';
|
||||
|
||||
const logger = new Logger('Packs');
|
||||
|
||||
export const list = async (): Promise<PackDashboardResponse> => {
|
||||
export async function list(): Promise<PackDashboardResponse> {
|
||||
try {
|
||||
logger.debug('Requesting pack dashboard');
|
||||
const response = await http.get<PackDashboardResponse>({url: Endpoints.PACKS});
|
||||
@@ -33,16 +33,16 @@ export const list = async (): Promise<PackDashboardResponse> => {
|
||||
logger.error('Failed to fetch pack dashboard:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const create = async (
|
||||
export async function create(
|
||||
type: 'emoji' | 'sticker',
|
||||
name: string,
|
||||
description?: string | null,
|
||||
): Promise<PackSummary> => {
|
||||
): Promise<PackSummaryResponse> {
|
||||
try {
|
||||
logger.debug(`Creating ${type} pack ${name}`);
|
||||
const response = await http.post<PackSummary>({
|
||||
const response = await http.post<PackSummaryResponse>({
|
||||
url: Endpoints.PACK_CREATE(type),
|
||||
body: {name, description: description ?? null},
|
||||
});
|
||||
@@ -51,23 +51,23 @@ export const create = async (
|
||||
logger.error(`Failed to create ${type} pack:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const update = async (
|
||||
export async function update(
|
||||
packId: string,
|
||||
data: {name?: string; description?: string | null},
|
||||
): Promise<PackSummary> => {
|
||||
): Promise<PackSummaryResponse> {
|
||||
try {
|
||||
logger.debug(`Updating pack ${packId}`);
|
||||
const response = await http.patch<PackSummary>({url: Endpoints.PACK(packId), body: data});
|
||||
const response = await http.patch<PackSummaryResponse>({url: Endpoints.PACK(packId), body: data});
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to update pack ${packId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const remove = async (packId: string): Promise<void> => {
|
||||
export async function remove(packId: string): Promise<void> {
|
||||
try {
|
||||
logger.debug(`Deleting pack ${packId}`);
|
||||
await http.delete({url: Endpoints.PACK(packId)});
|
||||
@@ -75,9 +75,9 @@ export const remove = async (packId: string): Promise<void> => {
|
||||
logger.error(`Failed to delete pack ${packId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const install = async (packId: string): Promise<void> => {
|
||||
export async function install(packId: string): Promise<void> {
|
||||
try {
|
||||
logger.debug(`Installing pack ${packId}`);
|
||||
await http.post({url: Endpoints.PACK_INSTALL(packId)});
|
||||
@@ -85,9 +85,9 @@ export const install = async (packId: string): Promise<void> => {
|
||||
logger.error(`Failed to install pack ${packId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const uninstall = async (packId: string): Promise<void> => {
|
||||
export async function uninstall(packId: string): Promise<void> {
|
||||
try {
|
||||
logger.debug(`Uninstalling pack ${packId}`);
|
||||
await http.delete({url: Endpoints.PACK_INSTALL(packId)});
|
||||
@@ -95,4 +95,4 @@ export const uninstall = async (packId: string): Promise<void> => {
|
||||
logger.error(`Failed to uninstall pack ${packId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import type {PackInviteMetadata} from '~/types/PackTypes';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import type {PackInviteMetadataResponse} from '@fluxer/schema/src/domains/invite/InviteSchemas';
|
||||
|
||||
const logger = new Logger('PackInvites');
|
||||
|
||||
@@ -31,10 +31,10 @@ export interface CreatePackInviteParams {
|
||||
unique?: boolean;
|
||||
}
|
||||
|
||||
export const createInvite = async (params: CreatePackInviteParams): Promise<PackInviteMetadata> => {
|
||||
export async function createInvite(params: CreatePackInviteParams): Promise<PackInviteMetadataResponse> {
|
||||
try {
|
||||
logger.debug(`Creating invite for pack ${params.packId}`);
|
||||
const response = await http.post<PackInviteMetadata>({
|
||||
const response = await http.post<PackInviteMetadataResponse>({
|
||||
url: Endpoints.PACK_INVITES(params.packId),
|
||||
body: {
|
||||
max_uses: params.maxUses ?? 0,
|
||||
@@ -47,4 +47,4 @@ export const createInvite = async (params: CreatePackInviteParams): Promise<Pack
|
||||
logger.error(`Failed to create invite for pack ${params.packId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
36
fluxer_app/src/actions/PiPActionCreators.tsx
Normal file
36
fluxer_app/src/actions/PiPActionCreators.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import PiPStore, {type PiPContent, type PiPCorner} from '@app/stores/PiPStore';
|
||||
|
||||
export function openPiP(content: PiPContent): void {
|
||||
PiPStore.open(content);
|
||||
}
|
||||
|
||||
export function closePiP(): void {
|
||||
PiPStore.close();
|
||||
}
|
||||
|
||||
export function showFocusedTileMirror(content: PiPContent, corner: PiPCorner = 'top-right'): void {
|
||||
PiPStore.showFocusedTileMirror(content, corner);
|
||||
}
|
||||
|
||||
export function hideFocusedTileMirror(): void {
|
||||
PiPStore.hideFocusedTileMirror();
|
||||
}
|
||||
@@ -17,17 +17,17 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {Popout} from '~/components/uikit/Popout';
|
||||
import PopoutStore from '~/stores/PopoutStore';
|
||||
import type {Popout} from '@app/components/uikit/popout';
|
||||
import PopoutStore from '@app/stores/PopoutStore';
|
||||
|
||||
export const open = (popout: Popout): void => {
|
||||
export function open(popout: Popout): void {
|
||||
PopoutStore.open(popout);
|
||||
};
|
||||
}
|
||||
|
||||
export const close = (key?: string | number): void => {
|
||||
export function close(key?: string | number): void {
|
||||
PopoutStore.close(key);
|
||||
};
|
||||
}
|
||||
|
||||
export const closeAll = (): void => {
|
||||
export function closeAll(): void {
|
||||
PopoutStore.closeAll();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,53 +17,30 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import type {PriceIdsResponse} from '@fluxer/schema/src/domains/premium/PremiumSchemas';
|
||||
|
||||
const logger = new Logger('Premium');
|
||||
|
||||
export interface VisionarySlots {
|
||||
total: number;
|
||||
remaining: number;
|
||||
}
|
||||
export type PriceIds = PriceIdsResponse;
|
||||
|
||||
export interface PriceIds {
|
||||
monthly: string | null;
|
||||
yearly: string | null;
|
||||
visionary: string | null;
|
||||
giftVisionary: string | null;
|
||||
gift1Month: string | null;
|
||||
gift1Year: string | null;
|
||||
currency: 'USD' | 'EUR';
|
||||
}
|
||||
|
||||
export const fetchVisionarySlots = async (): Promise<VisionarySlots> => {
|
||||
export async function fetchPriceIds(countryCode?: string): Promise<PriceIds> {
|
||||
try {
|
||||
const response = await http.get<VisionarySlots>(Endpoints.PREMIUM_VISIONARY_SLOTS);
|
||||
logger.debug('Visionary slots fetched', response.body);
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
logger.error('Visionary slots fetch failed', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchPriceIds = async (countryCode?: string): Promise<PriceIds> => {
|
||||
try {
|
||||
const url = countryCode
|
||||
? `${Endpoints.PREMIUM_PRICE_IDS}?country_code=${encodeURIComponent(countryCode)}`
|
||||
: Endpoints.PREMIUM_PRICE_IDS;
|
||||
const response = await http.get<PriceIds>(url);
|
||||
const response = await http.get<PriceIds>({
|
||||
url: Endpoints.PREMIUM_PRICE_IDS,
|
||||
query: countryCode ? {country_code: countryCode} : undefined,
|
||||
});
|
||||
logger.debug('Price IDs fetched', response.body);
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
logger.error('Price IDs fetch failed', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const createCustomerPortalSession = async (): Promise<string> => {
|
||||
export async function createCustomerPortalSession(): Promise<string> {
|
||||
try {
|
||||
const response = await http.post<{url: string}>(Endpoints.PREMIUM_CUSTOMER_PORTAL);
|
||||
logger.info('Customer portal session created');
|
||||
@@ -72,9 +49,9 @@ export const createCustomerPortalSession = async (): Promise<string> => {
|
||||
logger.error('Customer portal session creation failed', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const createCheckoutSession = async (priceId: string, isGift: boolean = false): Promise<string> => {
|
||||
export async function createCheckoutSession(priceId: string, isGift: boolean = false): Promise<string> {
|
||||
try {
|
||||
const url = isGift ? Endpoints.STRIPE_CHECKOUT_GIFT : Endpoints.STRIPE_CHECKOUT_SUBSCRIPTION;
|
||||
const response = await http.post<{url: string}>(url, {price_id: priceId});
|
||||
@@ -84,9 +61,9 @@ export const createCheckoutSession = async (priceId: string, isGift: boolean = f
|
||||
logger.error('Checkout session creation failed', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const cancelSubscriptionAtPeriodEnd = async (): Promise<void> => {
|
||||
export async function cancelSubscriptionAtPeriodEnd(): Promise<void> {
|
||||
try {
|
||||
await http.post({url: Endpoints.PREMIUM_CANCEL_SUBSCRIPTION});
|
||||
logger.info('Subscription set to cancel at period end');
|
||||
@@ -94,9 +71,9 @@ export const cancelSubscriptionAtPeriodEnd = async (): Promise<void> => {
|
||||
logger.error('Failed to cancel subscription at period end', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const reactivateSubscription = async (): Promise<void> => {
|
||||
export async function reactivateSubscription(): Promise<void> {
|
||||
try {
|
||||
await http.post({url: Endpoints.PREMIUM_REACTIVATE_SUBSCRIPTION});
|
||||
logger.info('Subscription reactivated');
|
||||
@@ -104,9 +81,9 @@ export const reactivateSubscription = async (): Promise<void> => {
|
||||
logger.error('Failed to reactivate subscription', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const rejoinVisionaryGuild = async (): Promise<void> => {
|
||||
export async function rejoinVisionaryGuild(): Promise<void> {
|
||||
try {
|
||||
await http.post({url: Endpoints.PREMIUM_VISIONARY_REJOIN});
|
||||
logger.info('Visionary guild rejoin requested');
|
||||
@@ -114,9 +91,9 @@ export const rejoinVisionaryGuild = async (): Promise<void> => {
|
||||
logger.error('Failed to rejoin Visionary guild', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const rejoinOperatorGuild = async (): Promise<void> => {
|
||||
export async function rejoinOperatorGuild(): Promise<void> {
|
||||
try {
|
||||
await http.post({url: Endpoints.PREMIUM_OPERATOR_REJOIN});
|
||||
logger.info('Operator guild rejoin requested');
|
||||
@@ -124,4 +101,4 @@ export const rejoinOperatorGuild = async (): Promise<void> => {
|
||||
logger.error('Failed to rejoin Operator guild', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,16 +17,16 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {modal} from '~/actions/ModalActionCreators';
|
||||
import {PremiumModal} from '~/components/modals/PremiumModal';
|
||||
import RuntimeConfigStore from '~/stores/RuntimeConfigStore';
|
||||
import * as ModalActionCreators from '@app/actions/ModalActionCreators';
|
||||
import {modal} from '@app/actions/ModalActionCreators';
|
||||
import {PremiumModal} from '@app/components/modals/PremiumModal';
|
||||
import RuntimeConfigStore from '@app/stores/RuntimeConfigStore';
|
||||
|
||||
interface OpenOptions {
|
||||
defaultGiftMode?: boolean;
|
||||
}
|
||||
|
||||
export const open = (optionsOrDefaultGiftMode: OpenOptions | boolean = {}): void => {
|
||||
export function open(optionsOrDefaultGiftMode: OpenOptions | boolean = {}): void {
|
||||
if (RuntimeConfigStore.isSelfHosted()) {
|
||||
return;
|
||||
}
|
||||
@@ -37,4 +37,4 @@ export const open = (optionsOrDefaultGiftMode: OpenOptions | boolean = {}): void
|
||||
: optionsOrDefaultGiftMode;
|
||||
const {defaultGiftMode = false} = options;
|
||||
ModalActionCreators.push(modal(() => <PremiumModal defaultGiftMode={defaultGiftMode} />));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,18 +17,18 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {ChannelTypes} from '~/Constants';
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import {Routes} from '~/Routes';
|
||||
import type {Channel} from '~/records/ChannelRecord';
|
||||
import ChannelStore from '~/stores/ChannelStore';
|
||||
import * as RouterUtils from '~/utils/RouterUtils';
|
||||
import * as NavigationActionCreators from '@app/actions/NavigationActionCreators';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import ChannelStore from '@app/stores/ChannelStore';
|
||||
import {ME} from '@fluxer/constants/src/AppConstants';
|
||||
import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
|
||||
import type {Channel} from '@fluxer/schema/src/domains/channel/ChannelSchemas';
|
||||
|
||||
const logger = new Logger('PrivateChannelActionCreators');
|
||||
|
||||
export const create = async (userId: string) => {
|
||||
export async function create(userId: string) {
|
||||
try {
|
||||
const response = await http.post<Channel>({
|
||||
url: Endpoints.USER_CHANNELS,
|
||||
@@ -40,9 +40,9 @@ export const create = async (userId: string) => {
|
||||
logger.error('Failed to create private channel:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const createGroupDM = async (recipientIds: Array<string>) => {
|
||||
export async function createGroupDM(recipientIds: Array<string>) {
|
||||
try {
|
||||
const response = await http.post<Channel>({
|
||||
url: Endpoints.USER_CHANNELS,
|
||||
@@ -54,9 +54,9 @@ export const createGroupDM = async (recipientIds: Array<string>) => {
|
||||
logger.error('Failed to create group DM:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const removeRecipient = async (channelId: string, userId: string) => {
|
||||
export async function removeRecipient(channelId: string, userId: string) {
|
||||
try {
|
||||
await http.delete({
|
||||
url: Endpoints.CHANNEL_RECIPIENT(channelId, userId),
|
||||
@@ -65,9 +65,9 @@ export const removeRecipient = async (channelId: string, userId: string) => {
|
||||
logger.error('Failed to remove recipient:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const ensureDMChannel = async (userId: string): Promise<string> => {
|
||||
export async function ensureDMChannel(userId: string): Promise<string> {
|
||||
try {
|
||||
const existingChannels = ChannelStore.dmChannels;
|
||||
const existingChannel = existingChannels.find(
|
||||
@@ -84,19 +84,19 @@ export const ensureDMChannel = async (userId: string): Promise<string> => {
|
||||
logger.error('Failed to ensure DM channel:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const openDMChannel = async (userId: string): Promise<void> => {
|
||||
export async function openDMChannel(userId: string): Promise<void> {
|
||||
try {
|
||||
const channelId = await ensureDMChannel(userId);
|
||||
RouterUtils.transitionTo(Routes.dmChannel(channelId));
|
||||
NavigationActionCreators.selectChannel(ME, channelId);
|
||||
} catch (error) {
|
||||
logger.error('Failed to open DM channel:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const pinDmChannel = async (channelId: string): Promise<void> => {
|
||||
export async function pinDmChannel(channelId: string): Promise<void> {
|
||||
try {
|
||||
await http.put({
|
||||
url: Endpoints.USER_CHANNEL_PIN(channelId),
|
||||
@@ -105,9 +105,9 @@ export const pinDmChannel = async (channelId: string): Promise<void> => {
|
||||
logger.error('Failed to pin DM channel:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const unpinDmChannel = async (channelId: string): Promise<void> => {
|
||||
export async function unpinDmChannel(channelId: string): Promise<void> {
|
||||
try {
|
||||
await http.delete({
|
||||
url: Endpoints.USER_CHANNEL_PIN(channelId),
|
||||
@@ -116,9 +116,9 @@ export const unpinDmChannel = async (channelId: string): Promise<void> => {
|
||||
logger.error('Failed to unpin DM channel:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const addRecipient = async (channelId: string, userId: string): Promise<void> => {
|
||||
export async function addRecipient(channelId: string, userId: string): Promise<void> {
|
||||
try {
|
||||
await http.put({
|
||||
url: Endpoints.CHANNEL_RECIPIENT(channelId, userId),
|
||||
@@ -127,4 +127,4 @@ export const addRecipient = async (channelId: string, userId: string): Promise<v
|
||||
logger.error('Failed to add recipient:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,109 +17,90 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {modal} from '~/actions/ModalActionCreators';
|
||||
import * as NavigationActionCreators from '~/actions/NavigationActionCreators';
|
||||
import * as PrivateChannelActionCreators from '~/actions/PrivateChannelActionCreators';
|
||||
import {FAVORITES_GUILD_ID, ME, QuickSwitcherResultTypes} from '~/Constants';
|
||||
import {UserSettingsModal} from '~/components/modals/UserSettingsModal';
|
||||
import {Routes} from '~/Routes';
|
||||
import type {QuickSwitcherExecutableResult} from '~/stores/QuickSwitcherStore';
|
||||
import QuickSwitcherStore from '~/stores/QuickSwitcherStore';
|
||||
import SelectedChannelStore from '~/stores/SelectedChannelStore';
|
||||
import {goToMessage, parseMessagePath} from '~/utils/MessageNavigator';
|
||||
import * as RouterUtils from '~/utils/RouterUtils';
|
||||
import * as ModalActionCreators from '@app/actions/ModalActionCreators';
|
||||
import {modal} from '@app/actions/ModalActionCreators';
|
||||
import * as NavigationActionCreators from '@app/actions/NavigationActionCreators';
|
||||
import * as PrivateChannelActionCreators from '@app/actions/PrivateChannelActionCreators';
|
||||
import {UserSettingsModal} from '@app/components/modals/UserSettingsModal';
|
||||
import {Routes} from '@app/Routes';
|
||||
import type {QuickSwitcherExecutableResult} from '@app/stores/QuickSwitcherStore';
|
||||
import QuickSwitcherStore from '@app/stores/QuickSwitcherStore';
|
||||
import SelectedChannelStore from '@app/stores/SelectedChannelStore';
|
||||
import {goToMessage, parseMessagePath} from '@app/utils/MessageNavigator';
|
||||
import * as RouterUtils from '@app/utils/RouterUtils';
|
||||
import {FAVORITES_GUILD_ID, ME} from '@fluxer/constants/src/AppConstants';
|
||||
import {QuickSwitcherResultTypes} from '@fluxer/constants/src/QuickSwitcherConstants';
|
||||
|
||||
const QUICK_SWITCHER_MODAL_KEY = 'quick-switcher';
|
||||
const QUICK_SWITCHER_MODAL_KEY = 'quick_switcher';
|
||||
|
||||
export const hide = (): void => {
|
||||
export function hide(): void {
|
||||
QuickSwitcherStore.hide();
|
||||
};
|
||||
}
|
||||
|
||||
export const search = (query: string): void => {
|
||||
export function search(query: string): void {
|
||||
QuickSwitcherStore.search(query);
|
||||
};
|
||||
}
|
||||
|
||||
export const select = (selectedIndex: number): void => {
|
||||
export function select(selectedIndex: number): void {
|
||||
QuickSwitcherStore.select(selectedIndex);
|
||||
};
|
||||
}
|
||||
|
||||
export const moveSelection = (direction: 'up' | 'down'): void => {
|
||||
export function moveSelection(direction: 'up' | 'down'): void {
|
||||
const nextIndex = QuickSwitcherStore.findNextSelectableIndex(direction);
|
||||
select(nextIndex);
|
||||
};
|
||||
}
|
||||
|
||||
export const confirmSelection = async (): Promise<void> => {
|
||||
export async function confirmSelection(): Promise<void> {
|
||||
const result = QuickSwitcherStore.getSelectedResult();
|
||||
if (!result) return;
|
||||
await switchTo(result);
|
||||
};
|
||||
}
|
||||
|
||||
export const switchTo = async (result: QuickSwitcherExecutableResult): Promise<void> => {
|
||||
export async function switchTo(result: QuickSwitcherExecutableResult): Promise<void> {
|
||||
try {
|
||||
switch (result.type) {
|
||||
case QuickSwitcherResultTypes.USER: {
|
||||
if (result.dmChannelId) {
|
||||
RouterUtils.transitionTo(Routes.dmChannel(result.dmChannelId));
|
||||
NavigationActionCreators.selectChannel(ME, result.dmChannelId);
|
||||
} else {
|
||||
await PrivateChannelActionCreators.openDMChannel(result.user.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case QuickSwitcherResultTypes.GROUP_DM: {
|
||||
RouterUtils.transitionTo(Routes.dmChannel(result.channel.id));
|
||||
NavigationActionCreators.selectChannel(ME, result.channel.id);
|
||||
break;
|
||||
}
|
||||
case QuickSwitcherResultTypes.TEXT_CHANNEL: {
|
||||
if (result.viewContext === FAVORITES_GUILD_ID) {
|
||||
NavigationActionCreators.selectChannel(FAVORITES_GUILD_ID, result.channel.id);
|
||||
RouterUtils.transitionTo(Routes.favoritesChannel(result.channel.id));
|
||||
} else if (result.guild) {
|
||||
NavigationActionCreators.selectGuild(result.guild.id);
|
||||
NavigationActionCreators.selectChannel(result.guild.id, result.channel.id);
|
||||
RouterUtils.transitionTo(Routes.guildChannel(result.guild.id, result.channel.id));
|
||||
} else {
|
||||
RouterUtils.transitionTo(Routes.dmChannel(result.channel.id));
|
||||
NavigationActionCreators.selectChannel(ME, result.channel.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case QuickSwitcherResultTypes.VOICE_CHANNEL: {
|
||||
if (result.viewContext === FAVORITES_GUILD_ID) {
|
||||
NavigationActionCreators.selectChannel(FAVORITES_GUILD_ID, result.channel.id);
|
||||
RouterUtils.transitionTo(Routes.favoritesChannel(result.channel.id));
|
||||
} else if (result.guild) {
|
||||
NavigationActionCreators.selectGuild(result.guild.id);
|
||||
NavigationActionCreators.selectChannel(result.guild.id, result.channel.id);
|
||||
RouterUtils.transitionTo(Routes.guildChannel(result.guild.id, result.channel.id));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case QuickSwitcherResultTypes.GUILD: {
|
||||
const channelId = SelectedChannelStore.selectedChannelIds.get(result.guild.id);
|
||||
NavigationActionCreators.selectGuild(result.guild.id);
|
||||
if (channelId) {
|
||||
NavigationActionCreators.selectChannel(result.guild.id, channelId);
|
||||
RouterUtils.transitionTo(Routes.guildChannel(result.guild.id, channelId));
|
||||
} else {
|
||||
RouterUtils.transitionTo(Routes.guildChannel(result.guild.id));
|
||||
}
|
||||
NavigationActionCreators.selectGuild(result.guild.id, channelId);
|
||||
break;
|
||||
}
|
||||
case QuickSwitcherResultTypes.VIRTUAL_GUILD: {
|
||||
if (result.virtualGuildType === 'favorites') {
|
||||
const validChannelId = SelectedChannelStore.getValidatedFavoritesChannel();
|
||||
if (validChannelId) {
|
||||
RouterUtils.transitionTo(Routes.favoritesChannel(validChannelId));
|
||||
} else {
|
||||
RouterUtils.transitionTo(Routes.FAVORITES);
|
||||
}
|
||||
NavigationActionCreators.selectGuild(FAVORITES_GUILD_ID, validChannelId ?? undefined);
|
||||
} else if (result.virtualGuildType === 'home') {
|
||||
const dmChannelId = SelectedChannelStore.selectedChannelIds.get(ME);
|
||||
if (dmChannelId) {
|
||||
RouterUtils.transitionTo(Routes.dmChannel(dmChannelId));
|
||||
} else {
|
||||
RouterUtils.transitionTo(Routes.ME);
|
||||
}
|
||||
NavigationActionCreators.selectGuild(ME, dmChannelId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -154,6 +135,8 @@ export const switchTo = async (result: QuickSwitcherExecutableResult): Promise<v
|
||||
} finally {
|
||||
hide();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const getModalKey = (): string => QUICK_SWITCHER_MODAL_KEY;
|
||||
export function getModalKey(): string {
|
||||
return QUICK_SWITCHER_MODAL_KEY;
|
||||
}
|
||||
|
||||
@@ -17,31 +17,35 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as ModalActionCreators from '@app/actions/ModalActionCreators';
|
||||
import {modal} from '@app/actions/ModalActionCreators';
|
||||
import * as ToastActionCreators from '@app/actions/ToastActionCreators';
|
||||
import {FeatureTemporarilyDisabledModal} from '@app/components/alerts/FeatureTemporarilyDisabledModal';
|
||||
import {TooManyReactionsModal} from '@app/components/alerts/TooManyReactionsModal';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {HttpError} from '@app/lib/HttpError';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import AuthenticationStore from '@app/stores/AuthenticationStore';
|
||||
import GatewayConnectionStore from '@app/stores/gateway/GatewayConnectionStore';
|
||||
import MessageReactionsStore from '@app/stores/MessageReactionsStore';
|
||||
import MessageStore from '@app/stores/MessageStore';
|
||||
import {getApiErrorCode, getApiErrorRetryAfter} from '@app/utils/ApiErrorUtils';
|
||||
import type {ReactionEmoji} from '@app/utils/ReactionUtils';
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {ME} from '@fluxer/constants/src/AppConstants';
|
||||
import type {UserPartial} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
|
||||
import type {I18n} from '@lingui/core';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {modal} from '~/actions/ModalActionCreators';
|
||||
import * as ToastActionCreators from '~/actions/ToastActionCreators';
|
||||
import {APIErrorCodes, ME} from '~/Constants';
|
||||
import {FeatureTemporarilyDisabledModal} from '~/components/alerts/FeatureTemporarilyDisabledModal';
|
||||
import {TooManyReactionsModal} from '~/components/alerts/TooManyReactionsModal';
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http, {HttpError} from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import type {UserPartial} from '~/records/UserRecord';
|
||||
import AuthenticationStore from '~/stores/AuthenticationStore';
|
||||
import ConnectionStore from '~/stores/ConnectionStore';
|
||||
import MessageReactionsStore from '~/stores/MessageReactionsStore';
|
||||
import MessageStore from '~/stores/MessageStore';
|
||||
import type {ReactionEmoji} from '~/utils/ReactionUtils';
|
||||
|
||||
const logger = new Logger('MessageReactions');
|
||||
|
||||
const MAX_RETRIES = 3;
|
||||
|
||||
const checkReactionResponse = (i18n: I18n, error: any, retry: () => void): boolean => {
|
||||
const checkReactionResponse = (i18n: I18n, error: HttpError, retry: () => void): boolean => {
|
||||
const errorCode = getApiErrorCode(error);
|
||||
|
||||
if (error.status === 403) {
|
||||
const errorCode = error.body?.code as string;
|
||||
if (errorCode === APIErrorCodes.FEATURE_TEMPORARILY_DISABLED) {
|
||||
logger.debug('Feature temporarily disabled, not retrying');
|
||||
ModalActionCreators.push(modal(() => <FeatureTemporarilyDisabledModal />));
|
||||
@@ -58,14 +62,13 @@ const checkReactionResponse = (i18n: I18n, error: any, retry: () => void): boole
|
||||
}
|
||||
|
||||
if (error.status === 429) {
|
||||
const retryAfter = error.body?.retry_after || 1000;
|
||||
const retryAfter = getApiErrorRetryAfter(error) || 1000;
|
||||
logger.debug(`Rate limited, retrying after ${retryAfter}ms`);
|
||||
setTimeout(retry, retryAfter);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (error.status === 400) {
|
||||
const errorCode = error.body?.code as string;
|
||||
switch (errorCode) {
|
||||
case APIErrorCodes.MAX_REACTIONS:
|
||||
logger.debug(`Reaction limit reached: ${errorCode}`);
|
||||
@@ -114,6 +117,10 @@ const optimisticUpdate = (
|
||||
emoji,
|
||||
optimistic: true,
|
||||
});
|
||||
} else if (type === 'MESSAGE_REACTION_REMOVE_ALL') {
|
||||
MessageStore.handleRemoveAllReactions({channelId, messageId});
|
||||
} else if (type === 'MESSAGE_REACTION_REMOVE_EMOJI') {
|
||||
MessageStore.handleRemoveReactionEmoji({channelId, messageId, emoji});
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
@@ -139,7 +146,7 @@ const makeUrl = ({
|
||||
: Endpoints.CHANNEL_MESSAGE_REACTION(channelId, messageId, emojiCode);
|
||||
};
|
||||
|
||||
const retryWithExponentialBackoff = async (func: () => Promise<any>, attempts = 0): Promise<any> => {
|
||||
async function retryWithExponentialBackoff<T>(func: () => Promise<T>, attempts = 0): Promise<T> {
|
||||
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
|
||||
|
||||
try {
|
||||
@@ -160,12 +167,12 @@ const retryWithExponentialBackoff = async (func: () => Promise<any>, attempts =
|
||||
logger.error(`Operation failed after ${MAX_RETRIES} attempts:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const performReactionAction = (
|
||||
i18n: I18n,
|
||||
type: 'MESSAGE_REACTION_ADD' | 'MESSAGE_REACTION_REMOVE',
|
||||
apiFunc: () => Promise<any>,
|
||||
apiFunc: () => Promise<unknown>,
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
emoji: ReactionEmoji,
|
||||
@@ -191,12 +198,12 @@ const performReactionAction = (
|
||||
});
|
||||
};
|
||||
|
||||
export const getReactions = async (
|
||||
export async function getReactions(
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
emoji: ReactionEmoji,
|
||||
limit?: number,
|
||||
): Promise<Array<UserPartial>> => {
|
||||
): Promise<Array<UserPartial>> {
|
||||
MessageReactionsStore.handleFetchPending(messageId, emoji);
|
||||
|
||||
try {
|
||||
@@ -205,7 +212,7 @@ export const getReactions = async (
|
||||
);
|
||||
|
||||
const query: Record<string, number> = {};
|
||||
if (limit !== undefined) query.limit = limit;
|
||||
if (limit !== undefined) query['limit'] = limit;
|
||||
|
||||
const response = await http.get<Array<UserPartial>>({
|
||||
url: makeUrl({channelId, messageId, emoji}),
|
||||
@@ -221,39 +228,39 @@ export const getReactions = async (
|
||||
MessageReactionsStore.handleFetchError(messageId, emoji);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const addReaction = (i18n: I18n, channelId: string, messageId: string, emoji: ReactionEmoji): void => {
|
||||
export function addReaction(i18n: I18n, channelId: string, messageId: string, emoji: ReactionEmoji): void {
|
||||
logger.debug(`Adding reaction ${emoji.name} to message ${messageId}`);
|
||||
|
||||
const apiFunc = () =>
|
||||
http.put({
|
||||
url: makeUrl({channelId, messageId, emoji, userId: ME}),
|
||||
query: {session_id: ConnectionStore.sessionId ?? null},
|
||||
query: {session_id: GatewayConnectionStore.sessionId ?? null},
|
||||
});
|
||||
|
||||
performReactionAction(i18n, 'MESSAGE_REACTION_ADD', apiFunc, channelId, messageId, emoji);
|
||||
};
|
||||
}
|
||||
|
||||
export const removeReaction = (
|
||||
export function removeReaction(
|
||||
i18n: I18n,
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
emoji: ReactionEmoji,
|
||||
userId?: string,
|
||||
): void => {
|
||||
): void {
|
||||
logger.debug(`Removing reaction ${emoji.name} from message ${messageId}`);
|
||||
|
||||
const apiFunc = () =>
|
||||
http.delete({
|
||||
url: makeUrl({channelId, messageId, emoji, userId: userId || ME}),
|
||||
query: {session_id: ConnectionStore.sessionId ?? null},
|
||||
query: {session_id: GatewayConnectionStore.sessionId ?? null},
|
||||
});
|
||||
|
||||
performReactionAction(i18n, 'MESSAGE_REACTION_REMOVE', apiFunc, channelId, messageId, emoji, userId);
|
||||
};
|
||||
}
|
||||
|
||||
export const removeAllReactions = (i18n: I18n, channelId: string, messageId: string): void => {
|
||||
export function removeAllReactions(i18n: I18n, channelId: string, messageId: string): void {
|
||||
logger.debug(`Removing all reactions from message ${messageId} in channel ${channelId}`);
|
||||
|
||||
const apiFunc = () =>
|
||||
@@ -264,4 +271,19 @@ export const removeAllReactions = (i18n: I18n, channelId: string, messageId: str
|
||||
retryWithExponentialBackoff(apiFunc).catch((error) => {
|
||||
checkReactionResponse(i18n, error, () => removeAllReactions(i18n, channelId, messageId));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function removeReactionEmoji(i18n: I18n, channelId: string, messageId: string, emoji: ReactionEmoji): void {
|
||||
logger.debug(`Removing all ${emoji.name} reactions from message ${messageId} in channel ${channelId}`);
|
||||
|
||||
optimisticUpdate('MESSAGE_REACTION_REMOVE_EMOJI', channelId, messageId, emoji);
|
||||
|
||||
const apiFunc = () =>
|
||||
http.delete({
|
||||
url: makeUrl({channelId, messageId, emoji}),
|
||||
});
|
||||
|
||||
retryWithExponentialBackoff(apiFunc).catch((error) => {
|
||||
checkReactionResponse(i18n, error, () => removeReactionEmoji(i18n, channelId, messageId, emoji));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,30 +17,27 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import ChannelStore from '~/stores/ChannelStore';
|
||||
import MessageStore from '~/stores/MessageStore';
|
||||
import ReadStateStore from '~/stores/ReadStateStore';
|
||||
import SnowflakeUtil from '~/utils/SnowflakeUtil';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import ChannelStore from '@app/stores/ChannelStore';
|
||||
import MessageStore from '@app/stores/MessageStore';
|
||||
import ReadStateStore from '@app/stores/ReadStateStore';
|
||||
import {atPreviousMillisecond} from '@fluxer/snowflake/src/SnowflakeUtils';
|
||||
|
||||
const logger = new Logger('ReadStateActionCreators');
|
||||
|
||||
type ChannelId = string;
|
||||
type MessageId = string;
|
||||
|
||||
export const ack = (channelId: ChannelId, immediate = false, force = false): void => {
|
||||
export function ack(channelId: string, immediate = false, force = false): void {
|
||||
logger.debug(`Acking channel ${channelId}, immediate=${immediate}, force=${force}`);
|
||||
ReadStateStore.handleChannelAck({channelId, immediate, force});
|
||||
};
|
||||
}
|
||||
|
||||
export const ackWithStickyUnread = (channelId: ChannelId): void => {
|
||||
export function ackWithStickyUnread(channelId: string): void {
|
||||
logger.debug(`Acking channel ${channelId} with sticky unread preservation`);
|
||||
ReadStateStore.handleChannelAckWithStickyUnread({channelId});
|
||||
};
|
||||
}
|
||||
|
||||
export const manualAck = async (channelId: ChannelId, messageId: MessageId): Promise<void> => {
|
||||
export async function manualAck(channelId: string, messageId: string): Promise<void> {
|
||||
try {
|
||||
logger.debug(`Manual ack: ${messageId} in ${channelId}`);
|
||||
const mentionCount = ReadStateStore.getManualAckMentionCount(channelId, messageId);
|
||||
@@ -59,9 +56,9 @@ export const manualAck = async (channelId: ChannelId, messageId: MessageId): Pro
|
||||
logger.error(`Failed to manual ack ${messageId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const markAsUnread = async (channelId: ChannelId, messageId: MessageId): Promise<void> => {
|
||||
export async function markAsUnread(channelId: string, messageId: string): Promise<void> {
|
||||
const messages = MessageStore.getMessages(channelId);
|
||||
const messagesArray = messages.toArray();
|
||||
const messageIndex = messagesArray.findIndex((m) => m.id === messageId);
|
||||
@@ -73,8 +70,7 @@ export const markAsUnread = async (channelId: ChannelId, messageId: MessageId):
|
||||
return;
|
||||
}
|
||||
|
||||
const ackMessageId =
|
||||
messageIndex > 0 ? messagesArray[messageIndex - 1].id : SnowflakeUtil.atPreviousMillisecond(messageId);
|
||||
const ackMessageId = messageIndex > 0 ? messagesArray[messageIndex - 1].id : atPreviousMillisecond(messageId);
|
||||
|
||||
if (!ackMessageId || ackMessageId === '0') {
|
||||
logger.debug('Unable to determine a previous message to ack; skipping mark-as-unread request');
|
||||
@@ -83,20 +79,20 @@ export const markAsUnread = async (channelId: ChannelId, messageId: MessageId):
|
||||
|
||||
logger.debug(`Acking ${ackMessageId} to mark ${messageId} as unread`);
|
||||
await manualAck(channelId, ackMessageId);
|
||||
};
|
||||
}
|
||||
|
||||
export const clearManualAck = (channelId: ChannelId): void => {
|
||||
export function clearManualAck(channelId: string): void {
|
||||
ReadStateStore.handleClearManualAck({channelId});
|
||||
};
|
||||
}
|
||||
|
||||
export const clearStickyUnread = (channelId: ChannelId): void => {
|
||||
export function clearStickyUnread(channelId: string): void {
|
||||
logger.debug(`Clearing sticky unread for ${channelId}`);
|
||||
ReadStateStore.clearStickyUnread(channelId);
|
||||
};
|
||||
}
|
||||
|
||||
interface BulkAckEntry {
|
||||
channelId: ChannelId;
|
||||
messageId: MessageId;
|
||||
channelId: string;
|
||||
messageId: string;
|
||||
}
|
||||
|
||||
const BULK_ACK_BATCH_SIZE = 100;
|
||||
@@ -109,7 +105,7 @@ function chunkEntries<T>(entries: Array<T>, size: number): Array<Array<T>> {
|
||||
return chunks;
|
||||
}
|
||||
|
||||
function createBulkEntry(channelId: ChannelId): BulkAckEntry | null {
|
||||
function createBulkEntry(channelId: string): BulkAckEntry | null {
|
||||
const messageId =
|
||||
ReadStateStore.lastMessageId(channelId) ?? ChannelStore.getChannel(channelId)?.lastMessageId ?? null;
|
||||
|
||||
@@ -144,7 +140,7 @@ function updateReadStatesLocally(entries: Array<BulkAckEntry>): void {
|
||||
}
|
||||
}
|
||||
|
||||
export async function bulkAckChannels(channelIds: Array<ChannelId>): Promise<void> {
|
||||
export async function bulkAckChannels(channelIds: Array<string>): Promise<void> {
|
||||
const entries = channelIds
|
||||
.map((channelId) => createBulkEntry(channelId))
|
||||
.filter((entry): entry is BulkAckEntry => entry != null);
|
||||
|
||||
@@ -17,16 +17,17 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import type {Message} from '~/records/MessageRecord';
|
||||
import type {MentionFilters} from '~/stores/RecentMentionsStore';
|
||||
import RecentMentionsStore from '~/stores/RecentMentionsStore';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import type {MentionFilters} from '@app/stores/RecentMentionsStore';
|
||||
import RecentMentionsStore from '@app/stores/RecentMentionsStore';
|
||||
import {MAX_MESSAGES_PER_CHANNEL} from '@fluxer/constants/src/LimitConstants';
|
||||
import type {Message} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
|
||||
|
||||
const logger = new Logger('Mentions');
|
||||
|
||||
export const fetch = async (): Promise<Array<Message>> => {
|
||||
export async function fetch(): Promise<Array<Message>> {
|
||||
RecentMentionsStore.handleFetchPending();
|
||||
try {
|
||||
const filters = RecentMentionsStore.getFilters();
|
||||
@@ -37,7 +38,7 @@ export const fetch = async (): Promise<Array<Message>> => {
|
||||
everyone: filters.includeEveryone,
|
||||
roles: filters.includeRoles,
|
||||
guilds: filters.includeGuilds,
|
||||
limit: 25,
|
||||
limit: MAX_MESSAGES_PER_CHANNEL,
|
||||
},
|
||||
});
|
||||
const data = response.body ?? [];
|
||||
@@ -49,9 +50,9 @@ export const fetch = async (): Promise<Array<Message>> => {
|
||||
logger.error('Failed to fetch recent mentions:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const loadMore = async (): Promise<Array<Message>> => {
|
||||
export async function loadMore(): Promise<Array<Message>> {
|
||||
const recentMentions = RecentMentionsStore.recentMentions;
|
||||
if (recentMentions.length === 0) {
|
||||
return [];
|
||||
@@ -69,7 +70,7 @@ export const loadMore = async (): Promise<Array<Message>> => {
|
||||
everyone: filters.includeEveryone,
|
||||
roles: filters.includeRoles,
|
||||
guilds: filters.includeGuilds,
|
||||
limit: 25,
|
||||
limit: MAX_MESSAGES_PER_CHANNEL,
|
||||
before: lastMessage.id,
|
||||
},
|
||||
});
|
||||
@@ -82,13 +83,13 @@ export const loadMore = async (): Promise<Array<Message>> => {
|
||||
logger.error('Failed to load more mentions:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const updateFilters = (filters: Partial<MentionFilters>): void => {
|
||||
export function updateFilters(filters: Partial<MentionFilters>): void {
|
||||
RecentMentionsStore.updateFilters(filters);
|
||||
};
|
||||
}
|
||||
|
||||
export const remove = async (messageId: string): Promise<void> => {
|
||||
export async function remove(messageId: string): Promise<void> {
|
||||
try {
|
||||
RecentMentionsStore.handleMessageDelete(messageId);
|
||||
logger.debug(`Removing message ${messageId} from recent mentions`);
|
||||
@@ -98,4 +99,4 @@ export const remove = async (messageId: string): Promise<void> => {
|
||||
logger.error(`Failed to remove message ${messageId} from recent mentions:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,63 +17,63 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {RelationshipTypes} from '~/Constants';
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import {RelationshipTypes} from '@fluxer/constants/src/UserConstants';
|
||||
|
||||
const logger = new Logger('RelationshipActionCreators');
|
||||
|
||||
export const sendFriendRequest = async (userId: string) => {
|
||||
export async function sendFriendRequest(userId: string) {
|
||||
try {
|
||||
await http.post({url: Endpoints.USER_RELATIONSHIP(userId)});
|
||||
} catch (error) {
|
||||
logger.error('Failed to send friend request:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const sendFriendRequestByTag = async (username: string, discriminator: string) => {
|
||||
export async function sendFriendRequestByTag(username: string, discriminator: string) {
|
||||
try {
|
||||
await http.post({url: Endpoints.USER_RELATIONSHIPS, body: {username, discriminator}});
|
||||
} catch (error) {
|
||||
logger.error('Failed to send friend request by tag:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const acceptFriendRequest = async (userId: string) => {
|
||||
export async function acceptFriendRequest(userId: string) {
|
||||
try {
|
||||
await http.put({url: Endpoints.USER_RELATIONSHIP(userId)});
|
||||
} catch (error) {
|
||||
logger.error('Failed to accept friend request:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const removeRelationship = async (userId: string) => {
|
||||
export async function removeRelationship(userId: string) {
|
||||
try {
|
||||
await http.delete({url: Endpoints.USER_RELATIONSHIP(userId)});
|
||||
} catch (error) {
|
||||
logger.error('Failed to remove relationship:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const blockUser = async (userId: string) => {
|
||||
export async function blockUser(userId: string) {
|
||||
try {
|
||||
await http.put({url: Endpoints.USER_RELATIONSHIP(userId), body: {type: RelationshipTypes.BLOCKED}});
|
||||
} catch (error) {
|
||||
logger.error('Failed to block user:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const updateFriendNickname = async (userId: string, nickname: string | null) => {
|
||||
export async function updateFriendNickname(userId: string, nickname: string | null) {
|
||||
try {
|
||||
await http.patch({url: Endpoints.USER_RELATIONSHIP(userId), body: {nickname}});
|
||||
} catch (error) {
|
||||
logger.error('Failed to update friend nickname:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,25 +17,26 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as ModalActionCreators from '@app/actions/ModalActionCreators';
|
||||
import {modal} from '@app/actions/ModalActionCreators';
|
||||
import * as ToastActionCreators from '@app/actions/ToastActionCreators';
|
||||
import {MaxBookmarksModal} from '@app/components/alerts/MaxBookmarksModal';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import {type SavedMessageEntry, SavedMessageEntryRecord} from '@app/records/SavedMessageEntryRecord';
|
||||
import SavedMessagesStore from '@app/stores/SavedMessagesStore';
|
||||
import {getApiErrorCode} from '@app/utils/ApiErrorUtils';
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import type {I18n} from '@lingui/core';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {modal} from '~/actions/ModalActionCreators';
|
||||
import * as ToastActionCreators from '~/actions/ToastActionCreators';
|
||||
import {APIErrorCodes} from '~/Constants';
|
||||
import {MaxBookmarksModal} from '~/components/alerts/MaxBookmarksModal';
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http, {HttpError} from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import {SavedMessageEntryRecord, type SavedMessageEntryResponse} from '~/records/SavedMessageEntryRecord';
|
||||
import SavedMessagesStore from '~/stores/SavedMessagesStore';
|
||||
|
||||
const logger = new Logger('SavedMessages');
|
||||
|
||||
export const fetch = async (): Promise<Array<SavedMessageEntryRecord>> => {
|
||||
export async function fetch(): Promise<Array<SavedMessageEntryRecord>> {
|
||||
try {
|
||||
logger.debug('Fetching saved messages');
|
||||
const response = await http.get<Array<SavedMessageEntryResponse>>({url: Endpoints.USER_SAVED_MESSAGES});
|
||||
const response = await http.get<Array<SavedMessageEntry>>({url: Endpoints.USER_SAVED_MESSAGES});
|
||||
const data = response.body ?? [];
|
||||
const entries = data.map(SavedMessageEntryRecord.fromResponse);
|
||||
SavedMessagesStore.fetchSuccess(entries);
|
||||
@@ -46,9 +47,9 @@ export const fetch = async (): Promise<Array<SavedMessageEntryRecord>> => {
|
||||
logger.error('Failed to fetch saved messages:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const create = async (i18n: I18n, channelId: string, messageId: string): Promise<void> => {
|
||||
export async function create(i18n: I18n, channelId: string, messageId: string): Promise<void> {
|
||||
try {
|
||||
logger.debug(`Saving message ${messageId} from channel ${channelId}`);
|
||||
await http.post({url: Endpoints.USER_SAVED_MESSAGES, body: {channel_id: channelId, message_id: messageId}});
|
||||
@@ -60,22 +61,16 @@ export const create = async (i18n: I18n, channelId: string, messageId: string):
|
||||
} catch (error) {
|
||||
logger.error(`Failed to save message ${messageId}:`, error);
|
||||
|
||||
if (
|
||||
error instanceof HttpError &&
|
||||
typeof error.body === 'object' &&
|
||||
error.body != null &&
|
||||
'code' in error.body &&
|
||||
(error.body as {code?: string}).code === APIErrorCodes.MAX_BOOKMARKS
|
||||
) {
|
||||
if (getApiErrorCode(error) === APIErrorCodes.MAX_BOOKMARKS) {
|
||||
ModalActionCreators.push(modal(() => <MaxBookmarksModal />));
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const remove = async (i18n: I18n, messageId: string): Promise<void> => {
|
||||
export async function remove(i18n: I18n, messageId: string): Promise<void> {
|
||||
try {
|
||||
SavedMessagesStore.handleMessageDelete(messageId);
|
||||
logger.debug(`Removing message ${messageId} from saved messages`);
|
||||
@@ -89,4 +84,4 @@ export const remove = async (i18n: I18n, messageId: string): Promise<void> => {
|
||||
logger.error(`Failed to remove message ${messageId} from saved messages:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,46 +17,49 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {I18n} from '@lingui/core';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import * as DraftActionCreators from '~/actions/DraftActionCreators';
|
||||
import * as MessageActionCreators from '~/actions/MessageActionCreators';
|
||||
import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {modal} from '~/actions/ModalActionCreators';
|
||||
import * as SlowmodeActionCreators from '~/actions/SlowmodeActionCreators';
|
||||
import * as ToastActionCreators from '~/actions/ToastActionCreators';
|
||||
import {APIErrorCodes} from '~/Constants';
|
||||
import {FeatureTemporarilyDisabledModal} from '~/components/alerts/FeatureTemporarilyDisabledModal';
|
||||
import {FileSizeTooLargeModal} from '~/components/alerts/FileSizeTooLargeModal';
|
||||
import {MessageSendFailedModal} from '~/components/alerts/MessageSendFailedModal';
|
||||
import {MessageSendTooQuickModal} from '~/components/alerts/MessageSendTooQuickModal';
|
||||
import {NSFWContentRejectedModal} from '~/components/alerts/NSFWContentRejectedModal';
|
||||
import {SlowmodeRateLimitedModal} from '~/components/alerts/SlowmodeRateLimitedModal';
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import {CloudUpload} from '~/lib/CloudUpload';
|
||||
import http, {type HttpError, type HttpResponse} from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import type {AllowedMentions} from '~/records/MessageRecord';
|
||||
import {
|
||||
type ScheduledAttachment,
|
||||
type ScheduledMessagePayload,
|
||||
ScheduledMessageRecord,
|
||||
type ScheduledMessageResponse,
|
||||
} from '~/records/ScheduledMessageRecord';
|
||||
import ScheduledMessagesStore from '~/stores/ScheduledMessagesStore';
|
||||
import {prepareAttachmentsForNonce} from '~/utils/MessageAttachmentUtils';
|
||||
import * as DraftActionCreators from '@app/actions/DraftActionCreators';
|
||||
import * as MessageActionCreators from '@app/actions/MessageActionCreators';
|
||||
import * as ModalActionCreators from '@app/actions/ModalActionCreators';
|
||||
import {modal} from '@app/actions/ModalActionCreators';
|
||||
import * as SlowmodeActionCreators from '@app/actions/SlowmodeActionCreators';
|
||||
import * as ToastActionCreators from '@app/actions/ToastActionCreators';
|
||||
import {FeatureTemporarilyDisabledModal} from '@app/components/alerts/FeatureTemporarilyDisabledModal';
|
||||
import {FileSizeTooLargeModal} from '@app/components/alerts/FileSizeTooLargeModal';
|
||||
import {MessageSendFailedModal} from '@app/components/alerts/MessageSendFailedModal';
|
||||
import {MessageSendTooQuickModal} from '@app/components/alerts/MessageSendTooQuickModal';
|
||||
import {NSFWContentRejectedModal} from '@app/components/alerts/NSFWContentRejectedModal';
|
||||
import {SlowmodeRateLimitedModal} from '@app/components/alerts/SlowmodeRateLimitedModal';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import {CloudUpload} from '@app/lib/CloudUpload';
|
||||
import http, {type HttpResponse} from '@app/lib/HttpClient';
|
||||
import type {HttpError} from '@app/lib/HttpError';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import type {
|
||||
ScheduledAttachment,
|
||||
ScheduledMessagePayload,
|
||||
ScheduledMessageResponse,
|
||||
} from '@app/records/ScheduledMessageRecord';
|
||||
import {ScheduledMessageRecord} from '@app/records/ScheduledMessageRecord';
|
||||
import ScheduledMessagesStore from '@app/stores/ScheduledMessagesStore';
|
||||
import {prepareAttachmentsForNonce} from '@app/utils/MessageAttachmentUtils';
|
||||
import {
|
||||
type ApiAttachmentMetadata,
|
||||
buildMessageCreateRequest,
|
||||
type MessageCreateRequest,
|
||||
type MessageReference,
|
||||
type MessageStickerItem,
|
||||
type NormalizedMessageContent,
|
||||
normalizeMessageContent,
|
||||
} from '~/utils/MessageRequestUtils';
|
||||
import * as MessageSubmitUtils from '~/utils/MessageSubmitUtils';
|
||||
import * as SnowflakeUtils from '~/utils/SnowflakeUtils';
|
||||
import {TypingUtils} from '~/utils/TypingUtils';
|
||||
} from '@app/utils/MessageRequestUtils';
|
||||
import * as MessageSubmitUtils from '@app/utils/MessageSubmitUtils';
|
||||
import {TypingUtils} from '@app/utils/TypingUtils';
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import type {
|
||||
AllowedMentions,
|
||||
MessageReference,
|
||||
MessageStickerItem,
|
||||
} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
|
||||
import * as SnowflakeUtils from '@fluxer/snowflake/src/SnowflakeUtils';
|
||||
import type {I18n} from '@lingui/core';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
|
||||
const logger = new Logger('ScheduledMessages');
|
||||
|
||||
@@ -113,7 +116,7 @@ function mapScheduledAttachments(
|
||||
}));
|
||||
}
|
||||
|
||||
export const fetchScheduledMessages = async (): Promise<Array<ScheduledMessageRecord>> => {
|
||||
export async function fetchScheduledMessages(): Promise<Array<ScheduledMessageRecord>> {
|
||||
logger.debug('Fetching scheduled messages');
|
||||
ScheduledMessagesStore.fetchStart();
|
||||
|
||||
@@ -131,9 +134,9 @@ export const fetchScheduledMessages = async (): Promise<Array<ScheduledMessageRe
|
||||
logger.error('Failed to fetch scheduled messages:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const scheduleMessage = async (i18n: I18n, params: ScheduleMessageParams): Promise<ScheduledMessageRecord> => {
|
||||
export async function scheduleMessage(i18n: I18n, params: ScheduleMessageParams): Promise<ScheduledMessageRecord> {
|
||||
logger.debug('Scheduling message', params);
|
||||
const nonce = SnowflakeUtils.fromTimestamp(Date.now());
|
||||
const normalized = normalizeMessageContent(params.content, params.favoriteMemeId);
|
||||
@@ -207,12 +210,12 @@ export const scheduleMessage = async (i18n: I18n, params: ScheduleMessageParams)
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const updateScheduledMessage = async (
|
||||
export async function updateScheduledMessage(
|
||||
i18n: I18n,
|
||||
params: UpdateScheduledMessageParams,
|
||||
): Promise<ScheduledMessageRecord> => {
|
||||
): Promise<ScheduledMessageRecord> {
|
||||
logger.debug('Updating scheduled message', params);
|
||||
const requestBody: ScheduledMessageRequest = {
|
||||
content: params.normalized.content,
|
||||
@@ -257,9 +260,9 @@ export const updateScheduledMessage = async (
|
||||
logger.error('Failed to update scheduled message', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const cancelScheduledMessage = async (i18n: I18n, scheduledMessageId: string): Promise<void> => {
|
||||
export async function cancelScheduledMessage(i18n: I18n, scheduledMessageId: string): Promise<void> {
|
||||
logger.debug('Canceling scheduled message', scheduledMessageId);
|
||||
try {
|
||||
await http.delete({url: Endpoints.USER_SCHEDULED_MESSAGE(scheduledMessageId)});
|
||||
@@ -272,7 +275,7 @@ export const cancelScheduledMessage = async (i18n: I18n, scheduledMessageId: str
|
||||
logger.error('Failed to cancel scheduled message', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function restoreDraftAfterScheduleFailure(
|
||||
channelId: string,
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import ChannelStickerStore from '~/stores/ChannelStickerStore';
|
||||
import SlowmodeStore from '~/stores/SlowmodeStore';
|
||||
import ChannelStickerStore from '@app/stores/ChannelStickerStore';
|
||||
import SlowmodeStore from '@app/stores/SlowmodeStore';
|
||||
|
||||
export function recordMessageSend(channelId: string): void {
|
||||
ChannelStickerStore.clearPendingStickerOnMessageSend(channelId);
|
||||
|
||||
@@ -17,21 +17,21 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import SoundStore from '~/stores/SoundStore';
|
||||
import type {SoundType} from '~/utils/SoundUtils';
|
||||
import SoundStore from '@app/stores/SoundStore';
|
||||
import type {SoundType} from '@app/utils/SoundUtils';
|
||||
|
||||
export const playSound = (sound: SoundType, loop = false): void => {
|
||||
export function playSound(sound: SoundType, loop?: boolean): void {
|
||||
SoundStore.playSound(sound, loop);
|
||||
};
|
||||
}
|
||||
|
||||
export const stopAllSounds = (): void => {
|
||||
export function stopAllSounds(): void {
|
||||
SoundStore.stopAllSounds();
|
||||
};
|
||||
}
|
||||
|
||||
export const updateSoundSettings = (settings: {
|
||||
export function updateSoundSettings(settings: {
|
||||
allSoundsDisabled?: boolean;
|
||||
soundType?: SoundType;
|
||||
enabled?: boolean;
|
||||
}): void => {
|
||||
}): void {
|
||||
SoundStore.updateSettings(settings);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {GuildStickerRecord} from '~/records/GuildStickerRecord';
|
||||
import StickerPickerStore from '~/stores/StickerPickerStore';
|
||||
import type {GuildStickerRecord} from '@app/records/GuildStickerRecord';
|
||||
import StickerPickerStore from '@app/stores/StickerPickerStore';
|
||||
|
||||
function getStickerKey(sticker: GuildStickerRecord): string {
|
||||
return `${sticker.guildId}:${sticker.id}`;
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as ToastActionCreators from '@app/actions/ToastActionCreators';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import {getElectronAPI, isDesktop} from '@app/utils/NativeUtils';
|
||||
import type {I18n} from '@lingui/core';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import * as ToastActionCreators from '~/actions/ToastActionCreators';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import {getElectronAPI, isDesktop} from '~/utils/NativeUtils';
|
||||
|
||||
const logger = new Logger('Clipboard');
|
||||
|
||||
@@ -54,7 +54,7 @@ const writeWithFallback = async (text: string): Promise<void> => {
|
||||
throw new Error('No clipboard API available');
|
||||
};
|
||||
|
||||
export const copy = async (i18n: I18n, text: string, suppressToast = false): Promise<boolean> => {
|
||||
export async function copy(i18n: I18n, text: string, suppressToast = false): Promise<boolean> {
|
||||
try {
|
||||
logger.debug('Copying text to clipboard');
|
||||
if (!isDesktop()) {
|
||||
@@ -73,4 +73,4 @@ export const copy = async (i18n: I18n, text: string, suppressToast = false): Pro
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,24 +17,18 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as AccessibilityActionCreators from '@app/actions/AccessibilityActionCreators';
|
||||
import * as ModalActionCreators from '@app/actions/ModalActionCreators';
|
||||
import {modal} from '@app/actions/ModalActionCreators';
|
||||
import * as ToastActionCreators from '@app/actions/ToastActionCreators';
|
||||
import {ThemeAcceptModal} from '@app/components/modals/ThemeAcceptModal';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import type {I18n} from '@lingui/core';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import * as AccessibilityActionCreators from '~/actions/AccessibilityActionCreators';
|
||||
import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {modal} from '~/actions/ModalActionCreators';
|
||||
import * as ToastActionCreators from '~/actions/ToastActionCreators';
|
||||
import {ThemeAcceptModal} from '~/components/modals/ThemeAcceptModal';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import type {ThemeData} from '~/stores/ThemeStore';
|
||||
import ThemeStore from '~/stores/ThemeStore';
|
||||
|
||||
const logger = new Logger('Themes');
|
||||
|
||||
export const fetchWithCoalescing = async (themeId: string): Promise<ThemeData> => {
|
||||
return ThemeStore.fetchTheme(themeId);
|
||||
};
|
||||
|
||||
export const applyTheme = (css: string, i18n: I18n): void => {
|
||||
export function applyTheme(css: string, i18n: I18n): void {
|
||||
try {
|
||||
AccessibilityActionCreators.update({customThemeCss: css});
|
||||
ToastActionCreators.success(i18n._(msg`Imported theme has been applied.`));
|
||||
@@ -43,18 +37,16 @@ export const applyTheme = (css: string, i18n: I18n): void => {
|
||||
ToastActionCreators.error(i18n._(msg`We couldn't apply this theme.`));
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const openAcceptModal = (themeId: string | undefined, i18n: I18n): void => {
|
||||
export function openAcceptModal(themeId: string | undefined, i18n: I18n): void {
|
||||
if (!themeId) {
|
||||
ToastActionCreators.error(i18n._(msg`This theme link is missing data.`));
|
||||
return;
|
||||
}
|
||||
|
||||
void fetchWithCoalescing(themeId).catch(() => {});
|
||||
|
||||
ModalActionCreators.pushWithKey(
|
||||
modal(() => <ThemeAcceptModal themeId={themeId} />),
|
||||
`theme-accept-${themeId}`,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
45
fluxer_app/src/actions/ThemePreferenceActionCreators.tsx
Normal file
45
fluxer_app/src/actions/ThemePreferenceActionCreators.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as UserSettingsActionCreators from '@app/actions/UserSettingsActionCreators';
|
||||
import ThemeStore from '@app/stores/ThemeStore';
|
||||
import type {ThemeType} from '@fluxer/constants/src/UserConstants';
|
||||
import {ThemeTypes} from '@fluxer/constants/src/UserConstants';
|
||||
|
||||
export function updateThemePreference(theme: ThemeType): void {
|
||||
if (theme === ThemeTypes.SYSTEM) {
|
||||
ThemeStore.setSyncAcrossDevices(false);
|
||||
ThemeStore.setTheme(theme);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ThemeStore.syncAcrossDevices) {
|
||||
void UserSettingsActionCreators.update({theme});
|
||||
} else {
|
||||
ThemeStore.setTheme(theme);
|
||||
}
|
||||
}
|
||||
|
||||
export function setSyncAcrossDevices(sync: boolean): void {
|
||||
if (sync) {
|
||||
ThemeStore.setSyncAcrossDevices(true);
|
||||
} else {
|
||||
ThemeStore.setSyncAcrossDevices(false);
|
||||
}
|
||||
}
|
||||
@@ -17,21 +17,21 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {ToastProps} from '~/components/uikit/Toast';
|
||||
import ToastStore from '~/stores/ToastStore';
|
||||
import type {ToastProps} from '@app/components/uikit/toast';
|
||||
import ToastStore from '@app/stores/ToastStore';
|
||||
|
||||
export const createToast = (data: ToastProps): string => {
|
||||
export function createToast(data: ToastProps): string {
|
||||
return ToastStore.createToast(data);
|
||||
};
|
||||
}
|
||||
|
||||
export const destroyToast = (id: string): void => {
|
||||
export function destroyToast(id: string): void {
|
||||
ToastStore.destroyToast(id);
|
||||
};
|
||||
}
|
||||
|
||||
export const success = (message: string): string => {
|
||||
export function success(message: string): string {
|
||||
return ToastStore.success(message);
|
||||
};
|
||||
}
|
||||
|
||||
export const error = (message: string): string => {
|
||||
export function error(message: string): string {
|
||||
return ToastStore.error(message);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,12 +17,31 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import TrustedDomainStore from '~/stores/TrustedDomainStore';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import TrustedDomainStore from '@app/stores/TrustedDomainStore';
|
||||
|
||||
const logger = new Logger('TrustedDomain');
|
||||
|
||||
export const addTrustedDomain = (domain: string): void => {
|
||||
export async function addTrustedDomain(domain: string): Promise<void> {
|
||||
logger.debug(`Adding trusted domain: ${domain}`);
|
||||
TrustedDomainStore.addTrustedDomain(domain);
|
||||
};
|
||||
await TrustedDomainStore.addTrustedDomain(domain);
|
||||
}
|
||||
|
||||
export async function removeTrustedDomain(domain: string): Promise<void> {
|
||||
logger.debug(`Removing trusted domain: ${domain}`);
|
||||
await TrustedDomainStore.removeTrustedDomain(domain);
|
||||
}
|
||||
|
||||
export async function clearAllTrustedDomains(): Promise<void> {
|
||||
logger.debug('Clearing all trusted domains');
|
||||
await TrustedDomainStore.clearAllTrustedDomains();
|
||||
}
|
||||
|
||||
export async function setTrustAllDomains(trustAll: boolean): Promise<void> {
|
||||
logger.debug(`Setting trust all domains: ${trustAll}`);
|
||||
await TrustedDomainStore.setTrustAllDomains(trustAll);
|
||||
}
|
||||
|
||||
export function checkAndMigrateLegacyData(): void {
|
||||
void TrustedDomainStore.checkAndMigrateLegacyData();
|
||||
}
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import TypingStore from '~/stores/TypingStore';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import TypingStore from '@app/stores/TypingStore';
|
||||
|
||||
const logger = new Logger('Typing');
|
||||
|
||||
export const sendTyping = async (channelId: string): Promise<void> => {
|
||||
export async function sendTyping(channelId: string): Promise<void> {
|
||||
try {
|
||||
logger.debug(`Sending typing indicator to channel ${channelId}`);
|
||||
await http.post({url: Endpoints.CHANNEL_TYPING(channelId)});
|
||||
@@ -32,14 +32,14 @@ export const sendTyping = async (channelId: string): Promise<void> => {
|
||||
} catch (error) {
|
||||
logger.error(`Failed to send typing indicator to channel ${channelId}:`, error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const startTyping = (channelId: string, userId: string): void => {
|
||||
export function startTyping(channelId: string, userId: string): void {
|
||||
logger.debug(`Starting typing indicator for user ${userId} in channel ${channelId}`);
|
||||
TypingStore.startTyping(channelId, userId);
|
||||
};
|
||||
}
|
||||
|
||||
export const stopTyping = (channelId: string, userId: string): void => {
|
||||
export function stopTyping(channelId: string, userId: string): void {
|
||||
logger.debug(`Stopping typing indicator for user ${userId} in channel ${channelId}`);
|
||||
TypingStore.stopTyping(channelId, userId);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,23 +17,23 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import UnsavedChangesStore from '~/stores/UnsavedChangesStore';
|
||||
import UnsavedChangesStore from '@app/stores/UnsavedChangesStore';
|
||||
|
||||
export const setUnsavedChanges = (tabId: string, hasChanges: boolean): void => {
|
||||
export function setUnsavedChanges(tabId: string, hasChanges: boolean): void {
|
||||
UnsavedChangesStore.setUnsavedChanges(tabId, hasChanges);
|
||||
};
|
||||
}
|
||||
|
||||
export const triggerFlashEffect = (tabId: string): void => {
|
||||
export function triggerFlashEffect(tabId: string): void {
|
||||
UnsavedChangesStore.triggerFlash(tabId);
|
||||
};
|
||||
}
|
||||
|
||||
export const clearUnsavedChanges = (tabId: string): void => {
|
||||
export function clearUnsavedChanges(tabId: string): void {
|
||||
UnsavedChangesStore.clearUnsavedChanges(tabId);
|
||||
};
|
||||
}
|
||||
|
||||
export const setTabData = (
|
||||
export function setTabData(
|
||||
tabId: string,
|
||||
data: {onReset?: () => void; onSave?: () => void; isSubmitting?: boolean},
|
||||
): void => {
|
||||
): void {
|
||||
UnsavedChangesStore.setTabData(tabId, data);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,14 +17,16 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import MessageStore from '@app/stores/MessageStore';
|
||||
import SudoStore from '@app/stores/SudoStore';
|
||||
import type {SudoVerificationPayload} from '@app/types/Sudo';
|
||||
import type {Message} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
|
||||
import type {HarvestStatusResponse} from '@fluxer/schema/src/domains/user/UserHarvestSchemas';
|
||||
import type {UserPrivate} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
|
||||
import type {PublicKeyCredentialCreationOptionsJSON, RegistrationResponseJSON} from '@simplewebauthn/browser';
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import type {Message} from '~/records/MessageRecord';
|
||||
import type {UserPrivate} from '~/records/UserRecord';
|
||||
import MessageStore from '~/stores/MessageStore';
|
||||
import SudoStore from '~/stores/SudoStore';
|
||||
|
||||
const logger = new Logger('User');
|
||||
|
||||
@@ -66,9 +68,20 @@ interface EmailChangeVerifyNewResponse {
|
||||
email_token: string;
|
||||
}
|
||||
|
||||
export const update = async (
|
||||
interface PasswordChangeStartResponse {
|
||||
ticket: string;
|
||||
code_expires_at: string;
|
||||
resend_available_at: string | null;
|
||||
}
|
||||
|
||||
interface PasswordChangeVerifyResponse {
|
||||
verification_proof: string;
|
||||
}
|
||||
|
||||
export async function update(
|
||||
user: Partial<UserPrivate> & {
|
||||
avatar?: string | null;
|
||||
password?: string;
|
||||
new_password?: string;
|
||||
premium_badge_hidden?: boolean;
|
||||
premium_badge_masked?: boolean;
|
||||
@@ -79,7 +92,7 @@ export const update = async (
|
||||
has_unread_gift_inventory?: boolean;
|
||||
email_token?: string;
|
||||
},
|
||||
): Promise<UserPrivate & {token?: string}> => {
|
||||
): Promise<UserPrivate & {token?: string}> {
|
||||
try {
|
||||
logger.debug('Updating current user profile');
|
||||
const response = await http.patch<UserPrivate & {token?: string}>(Endpoints.USER_ME, user);
|
||||
@@ -97,15 +110,15 @@ export const update = async (
|
||||
logger.error('Failed to update user profile:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const checkFluxerTagAvailability = async ({
|
||||
export async function checkFluxerTagAvailability({
|
||||
username,
|
||||
discriminator,
|
||||
}: {
|
||||
username: string;
|
||||
discriminator: string;
|
||||
}): Promise<boolean> => {
|
||||
}): Promise<boolean> {
|
||||
try {
|
||||
logger.debug(`Checking availability for FluxerTag ${username}#${discriminator}`);
|
||||
const response = await http.get<FluxerTagAvailabilityResponse>({
|
||||
@@ -117,9 +130,9 @@ export const checkFluxerTagAvailability = async ({
|
||||
logger.error('Failed to check FluxerTag availability:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const sendPhoneVerification = async (phone: string): Promise<void> => {
|
||||
export async function sendPhoneVerification(phone: string): Promise<void> {
|
||||
try {
|
||||
logger.debug('Sending phone verification code');
|
||||
await http.post({url: Endpoints.USER_PHONE_SEND_VERIFICATION, body: {phone}});
|
||||
@@ -128,9 +141,9 @@ export const sendPhoneVerification = async (phone: string): Promise<void> => {
|
||||
logger.error('Failed to send phone verification code', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const verifyPhone = async (phone: string, code: string): Promise<PhoneTokenResponse> => {
|
||||
export async function verifyPhone(phone: string, code: string): Promise<PhoneTokenResponse> {
|
||||
try {
|
||||
logger.debug('Verifying phone code');
|
||||
const response = await http.post<PhoneTokenResponse>(Endpoints.USER_PHONE_VERIFY, {phone, code});
|
||||
@@ -140,9 +153,9 @@ export const verifyPhone = async (phone: string, code: string): Promise<PhoneTok
|
||||
logger.error('Failed to verify phone code', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const addPhone = async (phoneToken: string): Promise<void> => {
|
||||
export async function addPhone(phoneToken: string): Promise<void> {
|
||||
try {
|
||||
logger.debug('Adding phone to account');
|
||||
await http.post({url: Endpoints.USER_PHONE, body: {phone_token: phoneToken}});
|
||||
@@ -151,9 +164,9 @@ export const addPhone = async (phoneToken: string): Promise<void> => {
|
||||
logger.error('Failed to add phone to account', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const startEmailChange = async (): Promise<EmailChangeStartResponse> => {
|
||||
export async function startEmailChange(): Promise<EmailChangeStartResponse> {
|
||||
try {
|
||||
logger.debug('Starting email change flow');
|
||||
const response = await http.post<EmailChangeStartResponse>({
|
||||
@@ -165,9 +178,9 @@ export const startEmailChange = async (): Promise<EmailChangeStartResponse> => {
|
||||
logger.error('Failed to start email change', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const resendEmailChangeOriginal = async (ticket: string): Promise<void> => {
|
||||
export async function resendEmailChangeOriginal(ticket: string): Promise<void> {
|
||||
try {
|
||||
logger.debug('Resending email change original code');
|
||||
await http.post({
|
||||
@@ -178,12 +191,12 @@ export const resendEmailChangeOriginal = async (ticket: string): Promise<void> =
|
||||
logger.error('Failed to resend original email code', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const verifyEmailChangeOriginal = async (
|
||||
export async function verifyEmailChangeOriginal(
|
||||
ticket: string,
|
||||
code: string,
|
||||
): Promise<EmailChangeVerifyOriginalResponse> => {
|
||||
): Promise<EmailChangeVerifyOriginalResponse> {
|
||||
try {
|
||||
logger.debug('Verifying original email code');
|
||||
const response = await http.post<EmailChangeVerifyOriginalResponse>({
|
||||
@@ -195,13 +208,13 @@ export const verifyEmailChangeOriginal = async (
|
||||
logger.error('Failed to verify original email code', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const requestEmailChangeNew = async (
|
||||
export async function requestEmailChangeNew(
|
||||
ticket: string,
|
||||
newEmail: string,
|
||||
originalProof: string,
|
||||
): Promise<EmailChangeRequestNewResponse> => {
|
||||
): Promise<EmailChangeRequestNewResponse> {
|
||||
try {
|
||||
logger.debug('Requesting new email code');
|
||||
const response = await http.post<EmailChangeRequestNewResponse>({
|
||||
@@ -213,9 +226,9 @@ export const requestEmailChangeNew = async (
|
||||
logger.error('Failed to request new email code', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const resendEmailChangeNew = async (ticket: string): Promise<void> => {
|
||||
export async function resendEmailChangeNew(ticket: string): Promise<void> {
|
||||
try {
|
||||
logger.debug('Resending new email code');
|
||||
await http.post({
|
||||
@@ -226,13 +239,13 @@ export const resendEmailChangeNew = async (ticket: string): Promise<void> => {
|
||||
logger.error('Failed to resend new email code', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const verifyEmailChangeNew = async (
|
||||
export async function verifyEmailChangeNew(
|
||||
ticket: string,
|
||||
code: string,
|
||||
originalProof: string,
|
||||
): Promise<EmailChangeVerifyNewResponse> => {
|
||||
): Promise<EmailChangeVerifyNewResponse> {
|
||||
try {
|
||||
logger.debug('Verifying new email code');
|
||||
const response = await http.post<EmailChangeVerifyNewResponse>({
|
||||
@@ -244,9 +257,113 @@ export const verifyEmailChangeNew = async (
|
||||
logger.error('Failed to verify new email code', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const removePhone = async (): Promise<void> => {
|
||||
export async function requestBouncedEmailChangeNew(newEmail: string): Promise<EmailChangeRequestNewResponse> {
|
||||
try {
|
||||
logger.debug('Requesting bounced email replacement code');
|
||||
const response = await http.post<EmailChangeRequestNewResponse>({
|
||||
url: Endpoints.USER_EMAIL_CHANGE_BOUNCED_REQUEST_NEW,
|
||||
body: {new_email: newEmail},
|
||||
});
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
logger.error('Failed to request bounced email replacement code', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function resendBouncedEmailChangeNew(ticket: string): Promise<void> {
|
||||
try {
|
||||
logger.debug('Resending bounced email replacement code');
|
||||
await http.post({
|
||||
url: Endpoints.USER_EMAIL_CHANGE_BOUNCED_RESEND_NEW,
|
||||
body: {ticket},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to resend bounced email replacement code', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifyBouncedEmailChangeNew(ticket: string, code: string): Promise<UserPrivate> {
|
||||
try {
|
||||
logger.debug('Verifying bounced email replacement code');
|
||||
const response = await http.post<UserPrivate>({
|
||||
url: Endpoints.USER_EMAIL_CHANGE_BOUNCED_VERIFY_NEW,
|
||||
body: {ticket, code},
|
||||
});
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
logger.error('Failed to verify bounced email replacement code', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function startPasswordChange(): Promise<PasswordChangeStartResponse> {
|
||||
try {
|
||||
logger.debug('Starting password change flow');
|
||||
const response = await http.post<PasswordChangeStartResponse>({
|
||||
url: Endpoints.USER_PASSWORD_CHANGE_START,
|
||||
body: {},
|
||||
});
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
logger.error('Failed to start password change', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function resendPasswordChangeCode(ticket: string): Promise<void> {
|
||||
try {
|
||||
logger.debug('Resending password change code');
|
||||
await http.post({
|
||||
url: Endpoints.USER_PASSWORD_CHANGE_RESEND,
|
||||
body: {ticket},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to resend password change code', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifyPasswordChangeCode(ticket: string, code: string): Promise<PasswordChangeVerifyResponse> {
|
||||
try {
|
||||
logger.debug('Verifying password change code');
|
||||
const response = await http.post<PasswordChangeVerifyResponse>({
|
||||
url: Endpoints.USER_PASSWORD_CHANGE_VERIFY,
|
||||
body: {ticket, code},
|
||||
});
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
logger.error('Failed to verify password change code', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function completePasswordChange(
|
||||
ticket: string,
|
||||
verificationProof: string,
|
||||
newPassword: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
logger.debug('Completing password change');
|
||||
await http.post({
|
||||
url: Endpoints.USER_PASSWORD_CHANGE_COMPLETE,
|
||||
body: {
|
||||
ticket,
|
||||
verification_proof: verificationProof,
|
||||
new_password: newPassword,
|
||||
},
|
||||
});
|
||||
logger.info('Password changed successfully');
|
||||
} catch (error) {
|
||||
logger.error('Failed to complete password change', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function removePhone(): Promise<void> {
|
||||
try {
|
||||
logger.debug('Removing phone from account');
|
||||
await http.delete({url: Endpoints.USER_PHONE, body: {}});
|
||||
@@ -255,9 +372,9 @@ export const removePhone = async (): Promise<void> => {
|
||||
logger.error('Failed to remove phone from account', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const enableSmsMfa = async (): Promise<void> => {
|
||||
export async function enableSmsMfa(): Promise<void> {
|
||||
try {
|
||||
logger.debug('Enabling SMS MFA');
|
||||
await http.post({url: Endpoints.USER_MFA_SMS_ENABLE, body: {}});
|
||||
@@ -267,9 +384,9 @@ export const enableSmsMfa = async (): Promise<void> => {
|
||||
logger.error('Failed to enable SMS MFA', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const disableSmsMfa = async (): Promise<void> => {
|
||||
export async function disableSmsMfa(): Promise<void> {
|
||||
try {
|
||||
logger.debug('Disabling SMS MFA');
|
||||
await http.post({url: Endpoints.USER_MFA_SMS_DISABLE, body: {}});
|
||||
@@ -278,9 +395,9 @@ export const disableSmsMfa = async (): Promise<void> => {
|
||||
logger.error('Failed to disable SMS MFA', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const listWebAuthnCredentials = async (): Promise<Array<WebAuthnCredential>> => {
|
||||
export async function listWebAuthnCredentials(): Promise<Array<WebAuthnCredential>> {
|
||||
try {
|
||||
logger.debug('Fetching WebAuthn credentials');
|
||||
const response = await http.get<Array<WebAuthnCredential>>({url: Endpoints.USER_MFA_WEBAUTHN_CREDENTIALS});
|
||||
@@ -291,9 +408,9 @@ export const listWebAuthnCredentials = async (): Promise<Array<WebAuthnCredentia
|
||||
logger.error('Failed to fetch WebAuthn credentials', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const getWebAuthnRegistrationOptions = async (): Promise<PublicKeyCredentialCreationOptionsJSON> => {
|
||||
export async function getWebAuthnRegistrationOptions(): Promise<PublicKeyCredentialCreationOptionsJSON> {
|
||||
try {
|
||||
logger.debug('Getting WebAuthn registration options');
|
||||
const response = await http.post<PublicKeyCredentialCreationOptionsJSON>({
|
||||
@@ -307,13 +424,13 @@ export const getWebAuthnRegistrationOptions = async (): Promise<PublicKeyCredent
|
||||
logger.error('Failed to get WebAuthn registration options', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const registerWebAuthnCredential = async (
|
||||
export async function registerWebAuthnCredential(
|
||||
response: RegistrationResponseJSON,
|
||||
challenge: string,
|
||||
name: string,
|
||||
): Promise<void> => {
|
||||
): Promise<void> {
|
||||
try {
|
||||
logger.debug('Registering WebAuthn credential');
|
||||
await http.post({url: Endpoints.USER_MFA_WEBAUTHN_CREDENTIALS, body: {response, challenge, name}});
|
||||
@@ -323,9 +440,9 @@ export const registerWebAuthnCredential = async (
|
||||
logger.error('Failed to register WebAuthn credential', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const renameWebAuthnCredential = async (credentialId: string, name: string): Promise<void> => {
|
||||
export async function renameWebAuthnCredential(credentialId: string, name: string): Promise<void> {
|
||||
try {
|
||||
logger.debug('Renaming WebAuthn credential');
|
||||
await http.patch({url: Endpoints.USER_MFA_WEBAUTHN_CREDENTIAL(credentialId), body: {name}});
|
||||
@@ -334,9 +451,9 @@ export const renameWebAuthnCredential = async (credentialId: string, name: strin
|
||||
logger.error('Failed to rename WebAuthn credential', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const deleteWebAuthnCredential = async (credentialId: string): Promise<void> => {
|
||||
export async function deleteWebAuthnCredential(credentialId: string): Promise<void> {
|
||||
try {
|
||||
logger.debug('Deleting WebAuthn credential');
|
||||
await http.delete({url: Endpoints.USER_MFA_WEBAUTHN_CREDENTIAL(credentialId), body: {}});
|
||||
@@ -345,9 +462,9 @@ export const deleteWebAuthnCredential = async (credentialId: string): Promise<vo
|
||||
logger.error('Failed to delete WebAuthn credential', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const disableAccount = async (): Promise<void> => {
|
||||
export async function disableAccount(): Promise<void> {
|
||||
try {
|
||||
logger.debug('Disabling account');
|
||||
await http.post({url: Endpoints.USER_DISABLE, body: {}});
|
||||
@@ -356,9 +473,9 @@ export const disableAccount = async (): Promise<void> => {
|
||||
logger.error('Failed to disable account', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const deleteAccount = async (): Promise<void> => {
|
||||
export async function deleteAccount(): Promise<void> {
|
||||
try {
|
||||
logger.debug('Deleting account');
|
||||
await http.post({url: Endpoints.USER_DELETE, body: {}});
|
||||
@@ -367,9 +484,20 @@ export const deleteAccount = async (): Promise<void> => {
|
||||
logger.error('Failed to delete account', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const bulkDeleteAllMessages = async (): Promise<void> => {
|
||||
export async function forgetAuthorizedIps(sudoPayload: SudoVerificationPayload): Promise<void> {
|
||||
try {
|
||||
logger.debug('Forgetting authorised IPs');
|
||||
await http.delete({url: Endpoints.USER_AUTHORIZED_IPS, body: sudoPayload});
|
||||
logger.info('Authorised IPs cleared');
|
||||
} catch (error) {
|
||||
logger.error('Failed to forget authorised IPs', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function bulkDeleteAllMessages(): Promise<void> {
|
||||
try {
|
||||
logger.debug('Requesting bulk deletion of all messages');
|
||||
await http.post({url: Endpoints.USER_BULK_DELETE_MESSAGES, body: {}});
|
||||
@@ -378,9 +506,9 @@ export const bulkDeleteAllMessages = async (): Promise<void> => {
|
||||
logger.error('Failed to queue bulk message deletion', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const cancelBulkDeleteAllMessages = async (): Promise<void> => {
|
||||
export async function cancelBulkDeleteAllMessages(): Promise<void> {
|
||||
try {
|
||||
logger.debug('Cancelling bulk deletion of all messages');
|
||||
await http.delete({url: Endpoints.USER_BULK_DELETE_MESSAGES, body: {}});
|
||||
@@ -389,9 +517,9 @@ export const cancelBulkDeleteAllMessages = async (): Promise<void> => {
|
||||
logger.error('Failed to cancel bulk message deletion', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const testBulkDeleteAllMessages = async (): Promise<void> => {
|
||||
export async function testBulkDeleteAllMessages(): Promise<void> {
|
||||
try {
|
||||
logger.debug('Requesting test bulk deletion of all messages (15s delay)');
|
||||
await http.post({url: Endpoints.USER_BULK_DELETE_MESSAGES_TEST});
|
||||
@@ -400,9 +528,20 @@ export const testBulkDeleteAllMessages = async (): Promise<void> => {
|
||||
logger.error('Failed to queue test bulk message deletion', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const requestDataHarvest = async (): Promise<{harvestId: string}> => {
|
||||
export async function resetPremiumState(): Promise<void> {
|
||||
try {
|
||||
logger.debug('Resetting premium state for current user');
|
||||
await http.post({url: Endpoints.USER_PREMIUM_RESET});
|
||||
logger.info('Reset premium state for current user');
|
||||
} catch (error) {
|
||||
logger.error('Failed to reset premium state', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function requestDataHarvest(): Promise<{harvestId: string}> {
|
||||
try {
|
||||
logger.debug('Requesting data harvest');
|
||||
const response = await http.post<{harvest_id: string}>({url: Endpoints.USER_HARVEST});
|
||||
@@ -412,33 +551,33 @@ export const requestDataHarvest = async (): Promise<{harvestId: string}> => {
|
||||
logger.error('Failed to request data harvest', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const getLatestHarvest = async (): Promise<any> => {
|
||||
export async function getLatestHarvest(): Promise<HarvestStatusResponse | null> {
|
||||
try {
|
||||
logger.debug('Fetching latest harvest');
|
||||
const response = await http.get<any>({url: Endpoints.USER_HARVEST_LATEST});
|
||||
const response = await http.get<HarvestStatusResponse | null>({url: Endpoints.USER_HARVEST_LATEST});
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch latest harvest', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const getHarvestStatus = async (harvestId: string): Promise<any> => {
|
||||
export async function getHarvestStatus(harvestId: string): Promise<HarvestStatusResponse> {
|
||||
try {
|
||||
logger.debug('Fetching harvest status', {harvestId});
|
||||
const response = await http.get<any>({url: Endpoints.USER_HARVEST_STATUS(harvestId)});
|
||||
const response = await http.get<HarvestStatusResponse>({url: Endpoints.USER_HARVEST_STATUS(harvestId)});
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch harvest status', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export type PreloadedDirectMessages = Record<string, Message>;
|
||||
|
||||
export const preloadDMMessages = async (channelIds: Array<string>): Promise<PreloadedDirectMessages> => {
|
||||
export async function preloadDMMessages(channelIds: Array<string>): Promise<PreloadedDirectMessages> {
|
||||
try {
|
||||
logger.debug('Preloading DM messages', {channelCount: channelIds.length});
|
||||
const response = await http.post<PreloadedDirectMessages>(Endpoints.USER_PRELOAD_MESSAGES, {
|
||||
@@ -453,4 +592,4 @@ export const preloadDMMessages = async (channelIds: Array<string>): Promise<Prel
|
||||
logger.error('Failed to preload DM messages', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,14 +17,15 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {ME, MessageNotifications} from '~/Constants';
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import type {UserGuildSettingsPartial} from '~/records/UserGuildSettingsRecord';
|
||||
import GuildStore from '~/stores/GuildStore';
|
||||
import type {ChannelOverride, GatewayGuildSettings} from '~/stores/UserGuildSettingsStore';
|
||||
import UserGuildSettingsStore from '~/stores/UserGuildSettingsStore';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import type {UserGuildSettingsPartial} from '@app/records/UserGuildSettingsRecord';
|
||||
import GuildStore from '@app/stores/GuildStore';
|
||||
import type {ChannelOverride, GatewayGuildSettings} from '@app/stores/UserGuildSettingsStore';
|
||||
import UserGuildSettingsStore from '@app/stores/UserGuildSettingsStore';
|
||||
import {ME} from '@fluxer/constants/src/AppConstants';
|
||||
import {MessageNotifications} from '@fluxer/constants/src/NotificationConstants';
|
||||
|
||||
const logger = new Logger('UserGuildSettingsActionCreators');
|
||||
|
||||
@@ -108,28 +109,28 @@ const scheduleUpdate = (guildId: string | null, updates: UserGuildSettingsPartia
|
||||
logger.debug(`Scheduled coalesced settings update for guild ${key} in 3 seconds`);
|
||||
};
|
||||
|
||||
export const updateGuildSettings = (
|
||||
export function updateGuildSettings(
|
||||
guildId: string | null,
|
||||
updates: UserGuildSettingsPartial,
|
||||
options?: PersistenceOptions,
|
||||
): void => {
|
||||
): void {
|
||||
UserGuildSettingsStore.getSettings(guildId);
|
||||
UserGuildSettingsStore.updateGuildSettings(guildId, updates as Partial<GatewayGuildSettings>);
|
||||
scheduleUpdate(guildId, updates, options);
|
||||
};
|
||||
}
|
||||
|
||||
export const toggleHideMutedChannels = (guildId: string | null): void => {
|
||||
export function toggleHideMutedChannels(guildId: string | null): void {
|
||||
const currentSettings = UserGuildSettingsStore.getSettings(guildId);
|
||||
const newValue = !currentSettings.hide_muted_channels;
|
||||
updateGuildSettings(guildId, {hide_muted_channels: newValue});
|
||||
};
|
||||
}
|
||||
|
||||
export const updateChannelOverride = (
|
||||
export function updateChannelOverride(
|
||||
guildId: string | null,
|
||||
channelId: string,
|
||||
override: Partial<ChannelOverride> | null,
|
||||
options?: PersistenceOptions,
|
||||
): void => {
|
||||
): void {
|
||||
const currentSettings = UserGuildSettingsStore.getSettings(guildId);
|
||||
const currentOverride = UserGuildSettingsStore.getChannelOverride(guildId, channelId);
|
||||
|
||||
@@ -146,7 +147,7 @@ export const updateChannelOverride = (
|
||||
};
|
||||
}
|
||||
|
||||
const newChannelOverrides = {...(currentSettings.channel_overrides ?? {})};
|
||||
const newChannelOverrides: Record<string, ChannelOverride> = {...(currentSettings.channel_overrides ?? {})};
|
||||
|
||||
if (newOverride == null) {
|
||||
delete newChannelOverrides[channelId];
|
||||
@@ -167,32 +168,32 @@ export const updateChannelOverride = (
|
||||
},
|
||||
options,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const toggleChannelCollapsed = (guildId: string | null, channelId: string): void => {
|
||||
export function toggleChannelCollapsed(guildId: string | null, channelId: string): void {
|
||||
const isCollapsed = UserGuildSettingsStore.isChannelCollapsed(guildId, channelId);
|
||||
updateChannelOverride(guildId, channelId, {collapsed: !isCollapsed});
|
||||
};
|
||||
}
|
||||
|
||||
export const updateMessageNotifications = (
|
||||
export function updateMessageNotifications(
|
||||
guildId: string | null,
|
||||
level: number,
|
||||
channelId?: string,
|
||||
options?: PersistenceOptions,
|
||||
): void => {
|
||||
): void {
|
||||
if (channelId) {
|
||||
updateChannelOverride(guildId, channelId, {message_notifications: level}, options);
|
||||
} else {
|
||||
updateGuildSettings(guildId, {message_notifications: level}, options);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const toggleChannelMuted = (guildId: string | null, channelId: string, options?: PersistenceOptions): void => {
|
||||
export function toggleChannelMuted(guildId: string | null, channelId: string, options?: PersistenceOptions): void {
|
||||
const isMuted = UserGuildSettingsStore.isChannelMuted(guildId, channelId);
|
||||
updateChannelOverride(guildId, channelId, {muted: !isMuted}, options);
|
||||
};
|
||||
}
|
||||
|
||||
export const toggleAllCategoriesCollapsed = (guildId: string | null, categoryIds: Array<string>): void => {
|
||||
export function toggleAllCategoriesCollapsed(guildId: string | null, categoryIds: Array<string>): void {
|
||||
if (categoryIds.length === 0) return;
|
||||
|
||||
const allCollapsed = categoryIds.every((categoryId) =>
|
||||
@@ -211,7 +212,7 @@ export const toggleAllCategoriesCollapsed = (guildId: string | null, categoryIds
|
||||
channel_overrides: (() => {
|
||||
const currentSettings = UserGuildSettingsStore.getSettings(guildId);
|
||||
|
||||
const newChannelOverrides = {...(currentSettings.channel_overrides ?? {})};
|
||||
const newChannelOverrides: Record<string, ChannelOverride> = {...(currentSettings.channel_overrides ?? {})};
|
||||
for (const categoryId of categoryIds) {
|
||||
const currentOverride = UserGuildSettingsStore.getChannelOverride(guildId, categoryId);
|
||||
if (newCollapsedState) {
|
||||
@@ -230,9 +231,9 @@ export const toggleAllCategoriesCollapsed = (guildId: string | null, categoryIds
|
||||
return Object.keys(newChannelOverrides).length > 0 ? newChannelOverrides : null;
|
||||
})(),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const repairGuildNotificationInheritance = (): void => {
|
||||
export function repairGuildNotificationInheritance(): void {
|
||||
const guildIds = UserGuildSettingsStore.getGuildIds();
|
||||
if (guildIds.length === 0) return;
|
||||
|
||||
@@ -248,4 +249,4 @@ export const repairGuildNotificationInheritance = (): void => {
|
||||
|
||||
updateGuildSettings(guildId, {message_notifications: MessageNotifications.INHERIT});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
|
||||
const logger = new Logger('Notes');
|
||||
|
||||
export const update = async (userId: string, note: string | null): Promise<void> => {
|
||||
export async function update(userId: string, note: string | null): Promise<void> {
|
||||
try {
|
||||
await http.put({url: Endpoints.USER_NOTE(userId), body: {note}});
|
||||
logger.debug(`Updated note for user ${userId} to ${note ? 'new value' : 'null'}`);
|
||||
@@ -31,4 +31,4 @@ export const update = async (userId: string, note: string | null): Promise<void>
|
||||
logger.error(`Failed to update note for user ${userId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,19 +17,19 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {modal} from '~/actions/ModalActionCreators';
|
||||
import {ME} from '~/Constants';
|
||||
import {UserProfileModal} from '~/components/modals/UserProfileModal';
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import {type Profile, ProfileRecord} from '~/records/ProfileRecord';
|
||||
import AuthenticationStore from '~/stores/AuthenticationStore';
|
||||
import MobileLayoutStore from '~/stores/MobileLayoutStore';
|
||||
import UserProfileMobileStore from '~/stores/UserProfileMobileStore';
|
||||
import UserProfileStore from '~/stores/UserProfileStore';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
import * as ModalActionCreators from '@app/actions/ModalActionCreators';
|
||||
import {modal} from '@app/actions/ModalActionCreators';
|
||||
import {UserProfileModal} from '@app/components/modals/UserProfileModal';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import {type Profile, ProfileRecord} from '@app/records/ProfileRecord';
|
||||
import AuthenticationStore from '@app/stores/AuthenticationStore';
|
||||
import MobileLayoutStore from '@app/stores/MobileLayoutStore';
|
||||
import UserProfileMobileStore from '@app/stores/UserProfileMobileStore';
|
||||
import UserProfileStore from '@app/stores/UserProfileStore';
|
||||
import UserStore from '@app/stores/UserStore';
|
||||
import {ME} from '@fluxer/constants/src/AppConstants';
|
||||
|
||||
const logger = new Logger('UserProfiles');
|
||||
|
||||
@@ -39,7 +39,7 @@ function buildKey(userId: string, guildId?: string): string {
|
||||
return `${userId}:${guildId ?? ME}`;
|
||||
}
|
||||
|
||||
export const fetch = async (userId: string, guildId?: string, force = false): Promise<ProfileRecord> => {
|
||||
export async function fetch(userId: string, guildId?: string, force = false): Promise<ProfileRecord> {
|
||||
try {
|
||||
const key = buildKey(userId, guildId);
|
||||
|
||||
@@ -98,9 +98,9 @@ export const fetch = async (userId: string, guildId?: string, force = false): Pr
|
||||
logger.error(`Failed to fetch profile for user ${userId}${guildId ? ` in guild ${guildId}` : ''}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const invalidate = (userId: string, guildId?: string): void => {
|
||||
export function invalidate(userId: string, guildId?: string): void {
|
||||
const scope = guildId ? ` in guild ${guildId}` : '';
|
||||
logger.debug(`Invalidating cached profile for user ${userId}${scope}`);
|
||||
try {
|
||||
@@ -109,9 +109,9 @@ export const invalidate = (userId: string, guildId?: string): void => {
|
||||
} catch (err) {
|
||||
logger.warn('Failed to invalidate cached profile:', err);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const clearCurrentUserProfiles = (): void => {
|
||||
export function clearCurrentUserProfiles(): void {
|
||||
logger.debug('Clearing cached profiles for current user');
|
||||
try {
|
||||
UserProfileStore.handleProfilesClear();
|
||||
@@ -126,9 +126,9 @@ export const clearCurrentUserProfiles = (): void => {
|
||||
} catch (err) {
|
||||
logger.warn('Failed to clear current user profiles:', err);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const openUserProfile = (userId: string, guildId?: string, autoFocusNote?: boolean): void => {
|
||||
export function openUserProfile(userId: string, guildId?: string, autoFocusNote?: boolean): void {
|
||||
if (MobileLayoutStore.enabled) {
|
||||
UserProfileMobileStore.open(userId, guildId, autoFocusNote);
|
||||
} else {
|
||||
@@ -136,4 +136,4 @@ export const openUserProfile = (userId: string, guildId?: string, autoFocusNote?
|
||||
modal(() => <UserProfileModal userId={userId} guildId={guildId} autoFocusNote={autoFocusNote} />),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {UserSettings} from '~/stores/UserSettingsStore';
|
||||
import UserSettingsStore from '~/stores/UserSettingsStore';
|
||||
import type {UserSettings} from '@app/stores/UserSettingsStore';
|
||||
import UserSettingsStore from '@app/stores/UserSettingsStore';
|
||||
|
||||
export const update = async (settings: Partial<UserSettings>): Promise<void> => {
|
||||
export async function update(settings: Partial<UserSettings>): Promise<void> {
|
||||
await UserSettingsStore.saveSettings(settings);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,17 +17,17 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {LayoutMode} from '~/stores/VoiceCallLayoutStore';
|
||||
import VoiceCallLayoutStore from '~/stores/VoiceCallLayoutStore';
|
||||
import type {LayoutMode, PinnedParticipantSource} from '@app/stores/VoiceCallLayoutStore';
|
||||
import VoiceCallLayoutStore from '@app/stores/VoiceCallLayoutStore';
|
||||
|
||||
export const setLayoutMode = (mode: LayoutMode): void => {
|
||||
export function setLayoutMode(mode: LayoutMode): void {
|
||||
VoiceCallLayoutStore.setLayoutMode(mode);
|
||||
};
|
||||
}
|
||||
|
||||
export const setPinnedParticipant = (identity: string | null): void => {
|
||||
VoiceCallLayoutStore.setPinnedParticipant(identity);
|
||||
};
|
||||
export function setPinnedParticipant(identity: string | null, source?: PinnedParticipantSource): void {
|
||||
VoiceCallLayoutStore.setPinnedParticipant(identity, source);
|
||||
}
|
||||
|
||||
export const markUserOverride = (): void => {
|
||||
export function markUserOverride(): void {
|
||||
VoiceCallLayoutStore.markUserOverride();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,9 +17,10 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import VoiceSettingsStore from '~/stores/VoiceSettingsStore';
|
||||
import VoiceSettingsStore from '@app/stores/VoiceSettingsStore';
|
||||
import MediaEngineStore from '@app/stores/voice/MediaEngineFacade';
|
||||
|
||||
export const update = (
|
||||
export function update(
|
||||
settings: Partial<{
|
||||
inputDeviceId: string;
|
||||
outputDeviceId: string;
|
||||
@@ -36,8 +37,20 @@ export const update = (
|
||||
backgroundImages: Array<{id: string; createdAt: number}>;
|
||||
showGridView: boolean;
|
||||
showMyOwnCamera: boolean;
|
||||
showMyOwnScreenShare: boolean;
|
||||
showNonVideoParticipants: boolean;
|
||||
showParticipantsCarousel: boolean;
|
||||
showVoiceConnectionAvatarStack: boolean;
|
||||
showVoiceConnectionId: boolean;
|
||||
pauseOwnScreenSharePreviewOnUnfocus: boolean;
|
||||
disablePictureInPicturePopout: boolean;
|
||||
}>,
|
||||
): void => {
|
||||
): void {
|
||||
VoiceSettingsStore.updateSettings(settings);
|
||||
};
|
||||
if (settings.outputVolume !== undefined) {
|
||||
MediaEngineStore.applyAllLocalAudioPreferences();
|
||||
}
|
||||
if (settings.inputVolume !== undefined) {
|
||||
MediaEngineStore.applyLocalInputVolume();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,27 +17,27 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as ModalActionCreators from '@app/actions/ModalActionCreators';
|
||||
import {modal} from '@app/actions/ModalActionCreators';
|
||||
import * as SoundActionCreators from '@app/actions/SoundActionCreators';
|
||||
import {MicrophonePermissionDeniedModal} from '@app/components/alerts/MicrophonePermissionDeniedModal';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import ChannelStore from '@app/stores/ChannelStore';
|
||||
import GatewayConnectionStore from '@app/stores/gateway/GatewayConnectionStore';
|
||||
import LocalVoiceStateStore from '@app/stores/LocalVoiceStateStore';
|
||||
import MediaPermissionStore from '@app/stores/MediaPermissionStore';
|
||||
import ParticipantVolumeStore from '@app/stores/ParticipantVolumeStore';
|
||||
import VoiceSettingsStore from '@app/stores/VoiceSettingsStore';
|
||||
import MediaEngineStore from '@app/stores/voice/MediaEngineFacade';
|
||||
import VoiceDevicePermissionStore from '@app/stores/voice/VoiceDevicePermissionStore';
|
||||
import {ensureNativePermission} from '@app/utils/NativePermissions';
|
||||
import {isDesktop} from '@app/utils/NativeUtils';
|
||||
import {SoundType} from '@app/utils/SoundUtils';
|
||||
import type {LocalTrackPublication, RemoteParticipant, Room} from 'livekit-client';
|
||||
import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {modal} from '~/actions/ModalActionCreators';
|
||||
import * as SoundActionCreators from '~/actions/SoundActionCreators';
|
||||
import {MicrophonePermissionDeniedModal} from '~/components/alerts/MicrophonePermissionDeniedModal';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import ChannelStore from '~/stores/ChannelStore';
|
||||
import ConnectionStore from '~/stores/ConnectionStore';
|
||||
import LocalVoiceStateStore from '~/stores/LocalVoiceStateStore';
|
||||
import MediaPermissionStore from '~/stores/MediaPermissionStore';
|
||||
import ParticipantVolumeStore from '~/stores/ParticipantVolumeStore';
|
||||
import VoiceSettingsStore from '~/stores/VoiceSettingsStore';
|
||||
import MediaEngineStore from '~/stores/voice/MediaEngineFacade';
|
||||
import VoiceDevicePermissionStore from '~/stores/voice/VoiceDevicePermissionStore';
|
||||
import {ensureNativePermission} from '~/utils/NativePermissions';
|
||||
import {isDesktop} from '~/utils/NativeUtils';
|
||||
import {SoundType} from '~/utils/SoundUtils';
|
||||
|
||||
const logger = new Logger('VoiceStateActionCreators');
|
||||
|
||||
export const toggleSelfDeaf = async (_guildId: string | null = null): Promise<void> => {
|
||||
export async function toggleSelfDeaf(_guildId: string | null = null): Promise<void> {
|
||||
const connectedGuildId = MediaEngineStore.guildId;
|
||||
const connectedChannelId = MediaEngineStore.channelId;
|
||||
|
||||
@@ -80,22 +80,42 @@ export const toggleSelfDeaf = async (_guildId: string | null = null): Promise<vo
|
||||
|
||||
const room = MediaEngineStore.room;
|
||||
if (room?.localParticipant) {
|
||||
room.localParticipant.audioTrackPublications.forEach((publication: LocalTrackPublication) => {
|
||||
const track = publication.track;
|
||||
if (!track) return;
|
||||
const operation = newMuteState ? track.mute() : track.unmute();
|
||||
operation.catch((error) =>
|
||||
logger.error(newMuteState ? 'Failed to mute local track' : 'Failed to unmute local track', {error}),
|
||||
);
|
||||
});
|
||||
const hasAudioTracks = room.localParticipant.audioTrackPublications.size > 0;
|
||||
|
||||
if (!newMuteState && !hasAudioTracks) {
|
||||
logger.info('No audio tracks found when undeafening/unmuting, enabling microphone');
|
||||
const permissionGranted = await requestMicrophoneInVoiceChannel(room, connectedChannelId);
|
||||
if (!permissionGranted) {
|
||||
logger.warn('Failed to enable microphone, reverting to muted state');
|
||||
LocalVoiceStateStore.updateSelfMute(true);
|
||||
MediaEngineStore.syncLocalVoiceStateWithServer({self_mute: true, self_deaf: newDeafState});
|
||||
if (!newDeafState) {
|
||||
SoundActionCreators.playSound(SoundType.Undeaf);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('Microphone enabled, explicitly unmuting newly created publications');
|
||||
room.localParticipant.audioTrackPublications.forEach((publication: LocalTrackPublication) => {
|
||||
publication.unmute().catch((error) => logger.error('Failed to unmute newly created publication', {error}));
|
||||
});
|
||||
} else {
|
||||
room.localParticipant.audioTrackPublications.forEach((publication: LocalTrackPublication) => {
|
||||
const operation = newMuteState ? publication.mute() : publication.unmute();
|
||||
operation.catch((error) =>
|
||||
logger.error(newMuteState ? 'Failed to mute publication' : 'Failed to unmute publication', {error}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
room.remoteParticipants.forEach((participant: RemoteParticipant) => {
|
||||
ParticipantVolumeStore.applySettingsToParticipant(participant, newDeafState);
|
||||
});
|
||||
|
||||
logger.debug('Applied mute/deafen state to LiveKit tracks immediately', {
|
||||
logger.debug('Applied mute/deafen state to LiveKit tracks', {
|
||||
newDeafState,
|
||||
newMuteState,
|
||||
hadAudioTracks: hasAudioTracks,
|
||||
localTrackCount: room.localParticipant.audioTrackPublications.size,
|
||||
remoteParticipantCount: room.remoteParticipants.size,
|
||||
});
|
||||
@@ -111,7 +131,7 @@ export const toggleSelfDeaf = async (_guildId: string | null = null): Promise<vo
|
||||
self_mute: newMuteState,
|
||||
self_deaf: newDeafState,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const showMicrophonePermissionDeniedModal = () => {
|
||||
ModalActionCreators.push(modal(() => <MicrophonePermissionDeniedModal />));
|
||||
@@ -217,7 +237,7 @@ const requestMicrophoneDirectly = async (): Promise<boolean> => {
|
||||
}
|
||||
};
|
||||
|
||||
export const toggleSelfMute = async (_guildId: string | null = null): Promise<void> => {
|
||||
export async function toggleSelfMute(_guildId: string | null = null): Promise<void> {
|
||||
const room = MediaEngineStore.room;
|
||||
const connectedChannelId = MediaEngineStore.channelId;
|
||||
|
||||
@@ -285,18 +305,35 @@ export const toggleSelfMute = async (_guildId: string | null = null): Promise<vo
|
||||
logger.debug('Voice state updated', {newMute, newDeaf});
|
||||
|
||||
if (room?.localParticipant) {
|
||||
room.localParticipant.audioTrackPublications.forEach((publication: LocalTrackPublication) => {
|
||||
const track = publication.track;
|
||||
if (!track) return;
|
||||
const operation = newMute ? track.mute() : track.unmute();
|
||||
operation.catch((error) =>
|
||||
logger.error(newMute ? 'Failed to mute local track' : 'Failed to unmute local track', {error}),
|
||||
);
|
||||
});
|
||||
const hasAudioTracks = room.localParticipant.audioTrackPublications.size > 0;
|
||||
|
||||
logger.debug('Applied mute state to LiveKit tracks immediately', {
|
||||
if (!newMute && !hasAudioTracks) {
|
||||
logger.info('No audio tracks found when unmuting, enabling microphone');
|
||||
const permissionGranted = await requestMicrophoneInVoiceChannel(room, connectedChannelId);
|
||||
if (!permissionGranted) {
|
||||
logger.warn('Failed to enable microphone, reverting to muted state');
|
||||
LocalVoiceStateStore.updateSelfMute(true);
|
||||
MediaEngineStore.syncLocalVoiceStateWithServer({self_mute: true, self_deaf: newDeaf});
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('Microphone enabled, explicitly unmuting newly created publications');
|
||||
room.localParticipant.audioTrackPublications.forEach((publication: LocalTrackPublication) => {
|
||||
publication.unmute().catch((error) => logger.error('Failed to unmute newly created publication', {error}));
|
||||
});
|
||||
} else {
|
||||
room.localParticipant.audioTrackPublications.forEach((publication: LocalTrackPublication) => {
|
||||
const operation = newMute ? publication.mute() : publication.unmute();
|
||||
operation.catch((error) =>
|
||||
logger.error(newMute ? 'Failed to mute publication' : 'Failed to unmute publication', {error}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug('Applied mute state to LiveKit tracks', {
|
||||
newMute,
|
||||
newDeaf,
|
||||
hadAudioTracks: hasAudioTracks,
|
||||
localTrackCount: room.localParticipant.audioTrackPublications.size,
|
||||
});
|
||||
}
|
||||
@@ -313,7 +350,7 @@ export const toggleSelfMute = async (_guildId: string | null = null): Promise<vo
|
||||
self_deaf: newDeaf,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
type VoiceStateProperty = 'self_mute' | 'self_deaf' | 'self_video' | 'self_stream';
|
||||
|
||||
@@ -325,7 +362,7 @@ const updateConnectionProperty = async (
|
||||
const voiceState = MediaEngineStore.getVoiceStateByConnectionId(connectionId);
|
||||
if (!voiceState) return;
|
||||
|
||||
const socket = ConnectionStore.socket;
|
||||
const socket = GatewayConnectionStore.socket;
|
||||
if (!socket) return;
|
||||
|
||||
socket.updateVoiceState({
|
||||
@@ -344,7 +381,7 @@ const updateConnectionsProperty = async (
|
||||
property: VoiceStateProperty,
|
||||
value: boolean,
|
||||
): Promise<void> => {
|
||||
const socket = ConnectionStore.socket;
|
||||
const socket = GatewayConnectionStore.socket;
|
||||
if (!socket) return;
|
||||
|
||||
for (const connectionId of connectionIds) {
|
||||
@@ -363,46 +400,46 @@ const updateConnectionsProperty = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const toggleSelfMuteForConnection = async (connectionId: string): Promise<void> => {
|
||||
export async function toggleSelfMuteForConnection(connectionId: string): Promise<void> {
|
||||
const voiceState = MediaEngineStore.getVoiceStateByConnectionId(connectionId);
|
||||
if (!voiceState) return;
|
||||
const target = !voiceState.self_mute;
|
||||
await updateConnectionProperty(connectionId, 'self_mute', target);
|
||||
if (target) SoundActionCreators.playSound(SoundType.Mute);
|
||||
else SoundActionCreators.playSound(SoundType.Unmute);
|
||||
};
|
||||
}
|
||||
|
||||
export const toggleSelfDeafenForConnection = async (connectionId: string): Promise<void> => {
|
||||
export async function toggleSelfDeafenForConnection(connectionId: string): Promise<void> {
|
||||
const voiceState = MediaEngineStore.getVoiceStateByConnectionId(connectionId);
|
||||
if (!voiceState) return;
|
||||
const target = !voiceState.self_deaf;
|
||||
await updateConnectionProperty(connectionId, 'self_deaf', target);
|
||||
if (target) SoundActionCreators.playSound(SoundType.Deaf);
|
||||
else SoundActionCreators.playSound(SoundType.Undeaf);
|
||||
};
|
||||
}
|
||||
|
||||
export const turnOffCameraForConnection = async (connectionId: string): Promise<void> => {
|
||||
export async function turnOffCameraForConnection(connectionId: string): Promise<void> {
|
||||
await updateConnectionProperty(connectionId, 'self_video', false);
|
||||
};
|
||||
}
|
||||
|
||||
export const turnOffStreamForConnection = async (connectionId: string): Promise<void> => {
|
||||
export async function turnOffStreamForConnection(connectionId: string): Promise<void> {
|
||||
await updateConnectionProperty(connectionId, 'self_stream', false);
|
||||
};
|
||||
}
|
||||
|
||||
export const bulkMuteConnections = async (connectionIds: Array<string>, mute: boolean = true): Promise<void> => {
|
||||
export async function bulkMuteConnections(connectionIds: Array<string>, mute: boolean = true): Promise<void> {
|
||||
await updateConnectionsProperty(connectionIds, 'self_mute', mute);
|
||||
};
|
||||
}
|
||||
|
||||
export const bulkDeafenConnections = async (connectionIds: Array<string>, deafen: boolean = true): Promise<void> => {
|
||||
export async function bulkDeafenConnections(connectionIds: Array<string>, deafen: boolean = true): Promise<void> {
|
||||
await updateConnectionsProperty(connectionIds, 'self_deaf', deafen);
|
||||
};
|
||||
}
|
||||
|
||||
export const bulkTurnOffCameras = async (connectionIds: Array<string>): Promise<void> => {
|
||||
export async function bulkTurnOffCameras(connectionIds: Array<string>): Promise<void> {
|
||||
await updateConnectionsProperty(connectionIds, 'self_video', false);
|
||||
};
|
||||
}
|
||||
|
||||
export const bulkDisconnect = async (connectionIds: Array<string>): Promise<void> => {
|
||||
const socket = ConnectionStore.socket;
|
||||
export async function bulkDisconnect(connectionIds: Array<string>): Promise<void> {
|
||||
const socket = GatewayConnectionStore.socket;
|
||||
if (!socket) return;
|
||||
|
||||
for (const connectionId of connectionIds) {
|
||||
@@ -419,16 +456,22 @@ export const bulkDisconnect = async (connectionIds: Array<string>): Promise<void
|
||||
self_stream: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const bulkMoveConnections = async (connectionIds: Array<string>, targetChannelId: string): Promise<void> => {
|
||||
const socket = ConnectionStore.socket;
|
||||
export async function bulkMoveConnections(connectionIds: Array<string>, targetChannelId: string): Promise<void> {
|
||||
const socket = GatewayConnectionStore.socket;
|
||||
if (!socket) return;
|
||||
const localConnectionId = MediaEngineStore.connectionId;
|
||||
|
||||
for (const connectionId of connectionIds) {
|
||||
const voiceState = MediaEngineStore.getVoiceStateByConnectionId(connectionId);
|
||||
if (!voiceState) continue;
|
||||
|
||||
if (localConnectionId && connectionId === localConnectionId) {
|
||||
await MediaEngineStore.connectToVoiceChannel(voiceState.guild_id ?? null, targetChannelId);
|
||||
continue;
|
||||
}
|
||||
|
||||
socket.updateVoiceState({
|
||||
guild_id: voiceState.guild_id,
|
||||
channel_id: targetChannelId,
|
||||
@@ -439,4 +482,4 @@ export const bulkMoveConnections = async (connectionIds: Array<string>, targetCh
|
||||
self_stream: voiceState.self_stream,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Endpoints} from '~/Endpoints';
|
||||
import http from '~/lib/HttpClient';
|
||||
import {Logger} from '~/lib/Logger';
|
||||
import type {Webhook} from '~/records/WebhookRecord';
|
||||
import WebhookStore from '~/stores/WebhookStore';
|
||||
import {Endpoints} from '@app/Endpoints';
|
||||
import http from '@app/lib/HttpClient';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import WebhookStore from '@app/stores/WebhookStore';
|
||||
import type {Webhook} from '@fluxer/schema/src/domains/webhook/WebhookSchemas';
|
||||
|
||||
const logger = new Logger('WebhookActionCreators');
|
||||
|
||||
@@ -37,7 +37,7 @@ export interface UpdateWebhookParams {
|
||||
avatar?: string | null;
|
||||
}
|
||||
|
||||
export const fetchGuildWebhooks = async (guildId: string): Promise<Array<Webhook>> => {
|
||||
export async function fetchGuildWebhooks(guildId: string): Promise<Array<Webhook>> {
|
||||
WebhookStore.handleGuildWebhooksFetchPending(guildId);
|
||||
|
||||
try {
|
||||
@@ -52,15 +52,15 @@ export const fetchGuildWebhooks = async (guildId: string): Promise<Array<Webhook
|
||||
WebhookStore.handleGuildWebhooksFetchError(guildId);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const fetchChannelWebhooks = async ({
|
||||
export async function fetchChannelWebhooks({
|
||||
guildId,
|
||||
channelId,
|
||||
}: {
|
||||
guildId: string;
|
||||
channelId: string;
|
||||
}): Promise<Array<Webhook>> => {
|
||||
}): Promise<Array<Webhook>> {
|
||||
WebhookStore.handleChannelWebhooksFetchPending(channelId);
|
||||
|
||||
try {
|
||||
@@ -75,9 +75,9 @@ export const fetchChannelWebhooks = async ({
|
||||
WebhookStore.handleChannelWebhooksFetchError(channelId);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const createWebhook = async ({channelId, name, avatar}: CreateWebhookParams): Promise<Webhook> => {
|
||||
export async function createWebhook({channelId, name, avatar}: CreateWebhookParams): Promise<Webhook> {
|
||||
try {
|
||||
const response = await http.post<Webhook>(Endpoints.CHANNEL_WEBHOOKS(channelId), {name, avatar: avatar ?? null});
|
||||
const data = response.body;
|
||||
@@ -89,9 +89,9 @@ export const createWebhook = async ({channelId, name, avatar}: CreateWebhookPara
|
||||
logger.error(`Failed to create webhook for channel ${channelId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const deleteWebhook = async (webhookId: string): Promise<void> => {
|
||||
export async function deleteWebhook(webhookId: string): Promise<void> {
|
||||
const existing = WebhookStore.getWebhook(webhookId);
|
||||
|
||||
try {
|
||||
@@ -104,9 +104,9 @@ export const deleteWebhook = async (webhookId: string): Promise<void> => {
|
||||
logger.error(`Failed to delete webhook ${webhookId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const moveWebhook = async (webhookId: string, newChannelId: string): Promise<Webhook> => {
|
||||
export async function moveWebhook(webhookId: string, newChannelId: string): Promise<Webhook> {
|
||||
const existing = WebhookStore.getWebhook(webhookId);
|
||||
if (!existing) {
|
||||
throw new Error(`Webhook ${webhookId} not found`);
|
||||
@@ -124,7 +124,7 @@ export const moveWebhook = async (webhookId: string, newChannelId: string): Prom
|
||||
logger.error(`Failed to move webhook ${webhookId} to channel ${newChannelId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const updateWebhook = async ({webhookId, name, avatar}: UpdateWebhookParams): Promise<Webhook> => {
|
||||
try {
|
||||
@@ -140,7 +140,7 @@ const updateWebhook = async ({webhookId, name, avatar}: UpdateWebhookParams): Pr
|
||||
}
|
||||
};
|
||||
|
||||
export const updateWebhooks = async (updates: Array<UpdateWebhookParams>): Promise<Array<Webhook>> => {
|
||||
export async function updateWebhooks(updates: Array<UpdateWebhookParams>): Promise<Array<Webhook>> {
|
||||
const results: Array<Webhook> = [];
|
||||
|
||||
for (const update of updates) {
|
||||
@@ -153,4 +153,4 @@ export const updateWebhooks = async (updates: Array<UpdateWebhookParams>): Promi
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import GuildReadStateStore from '~/stores/GuildReadStateStore';
|
||||
import IdleStore from '~/stores/IdleStore';
|
||||
import NotificationStore from '~/stores/NotificationStore';
|
||||
import WindowStore from '~/stores/WindowStore';
|
||||
import GuildReadStateStore from '@app/stores/GuildReadStateStore';
|
||||
import IdleStore from '@app/stores/IdleStore';
|
||||
import NotificationStore from '@app/stores/NotificationStore';
|
||||
import WindowStore from '@app/stores/WindowStore';
|
||||
|
||||
export const focus = (focused: boolean): void => {
|
||||
export function focused(focused: boolean): void {
|
||||
WindowStore.setFocused(focused);
|
||||
GuildReadStateStore.handleWindowFocus();
|
||||
NotificationStore.handleWindowFocus({focused});
|
||||
@@ -30,12 +30,12 @@ export const focus = (focused: boolean): void => {
|
||||
if (focused) {
|
||||
IdleStore.recordActivity();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const resized = (): void => {
|
||||
export function resized(): void {
|
||||
WindowStore.updateWindowSize();
|
||||
};
|
||||
}
|
||||
|
||||
export const visibilityChanged = (visible: boolean): void => {
|
||||
export function visibilityChanged(visible: boolean): void {
|
||||
WindowStore.setVisible(visible);
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user