(null);
5 |
--------------------------------------------------------------------------------
/apps/server/sql/spaces/get_token.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | invite_token
3 | FROM
4 | spaces
5 | WHERE
6 | id = $1
7 | AND deleted = FALSE
8 | LIMIT 1;
9 |
10 |
--------------------------------------------------------------------------------
/apps/server/sql/users/get_users_extension.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | users_extension AS "user_ext!: UserExt"
3 | FROM
4 | users_extension
5 | WHERE
6 | user_id = $1;
7 |
8 |
--------------------------------------------------------------------------------
/apps/server/text/reset-password/content.zh-CN.html:
--------------------------------------------------------------------------------
1 |
2 | 你请求了重置密码。
3 | 点这里进行重置。
4 |
5 |
6 | 如果并没有请求重置,请忽略这封邮件
7 |
--------------------------------------------------------------------------------
/apps/server/text/reset-password/content.zh-TW.html:
--------------------------------------------------------------------------------
1 |
2 | 你請求了重設密碼。
3 | 點這裡進行重設。
4 |
5 |
6 | 如果並沒有請求重設,請忽略這封郵件
7 |
--------------------------------------------------------------------------------
/apps/spa/eslint.config.js:
--------------------------------------------------------------------------------
1 | import { nextJsConfig } from '@boluo/eslint-config/next-js';
2 |
3 | /** @type {import("eslint").Linter.Config} */
4 | export default nextJsConfig;
5 |
--------------------------------------------------------------------------------
/apps/storybook/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/packages/api-browser/eslint.config.js:
--------------------------------------------------------------------------------
1 | import { config } from '@boluo/eslint-config/base';
2 |
3 | /** @type {import("eslint").Linter.Config} */
4 | export default config;
5 |
--------------------------------------------------------------------------------
/packages/interpreter/eslint.config.js:
--------------------------------------------------------------------------------
1 | import { config } from '@boluo/eslint-config/base';
2 |
3 | /** @type {import("eslint").Linter.Config} */
4 | export default config;
5 |
--------------------------------------------------------------------------------
/apps/server/sql/channels/get_by_id_list.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | ch AS "channel!: Channel"
3 | FROM
4 | channels ch
5 | WHERE
6 | ch.id = ANY($1)
7 | AND deleted = FALSE;
8 |
--------------------------------------------------------------------------------
/apps/server/src/messages.rs:
--------------------------------------------------------------------------------
1 | pub mod api;
2 | mod handlers;
3 | mod models;
4 |
5 | pub use handlers::router;
6 | pub use models::Entities;
7 | pub use models::{MaxPos, Message};
8 |
--------------------------------------------------------------------------------
/apps/site/eslint.config.js:
--------------------------------------------------------------------------------
1 | import { nextJsConfig } from '@boluo/eslint-config/next-js';
2 |
3 | /** @type {import("eslint").Linter.Config} */
4 | export default nextJsConfig;
5 |
--------------------------------------------------------------------------------
/apps/spa/pages/layout.tsx:
--------------------------------------------------------------------------------
1 | import { type ReactNode } from 'react';
2 |
3 | export default function Layout({ children }: { children: ReactNode }) {
4 | return children;
5 | }
6 |
--------------------------------------------------------------------------------
/apps/server/sql/channels/remove_user_from_channel.sql:
--------------------------------------------------------------------------------
1 | UPDATE
2 | channel_members
3 | SET
4 | is_joined = FALSE
5 | WHERE
6 | user_id = $1
7 | AND channel_id = $2;
8 |
9 |
--------------------------------------------------------------------------------
/apps/server/sql/spaces/get_by_id.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | s AS "space!: Space"
3 | FROM
4 | spaces s
5 | WHERE
6 | s.id = $1
7 | AND deleted = FALSE
8 | LIMIT 1;
9 |
10 |
--------------------------------------------------------------------------------
/apps/server/sql/spaces/recent.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | id AS "id!: Uuid"
3 | FROM
4 | spaces
5 | WHERE
6 | deleted = FALSE
7 | AND latest_activity > now() - interval '2 hours';
8 |
--------------------------------------------------------------------------------
/apps/server/sql/users/remove_avatar.sql:
--------------------------------------------------------------------------------
1 | UPDATE
2 | users
3 | SET
4 | avatar_id = NULL
5 | WHERE
6 | id = $1
7 | RETURNING
8 | users AS "users!: User";
9 |
10 |
--------------------------------------------------------------------------------
/apps/server/src/spaces.rs:
--------------------------------------------------------------------------------
1 | pub mod api;
2 | pub mod handlers;
3 | pub mod models;
4 |
5 | pub use handlers::router;
6 | pub use models::{Space, SpaceMember, SpaceSettings, UserSpaces};
7 |
--------------------------------------------------------------------------------
/docs/api/Info/Basic.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Basic
3 | type: http
4 | seq: 1
5 | }
6 |
7 | get {
8 | url: {{base_url}}/info
9 | body: none
10 | auth: inherit
11 | }
12 |
--------------------------------------------------------------------------------
/apps/server/sql/spaces/user_owned_spaces.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | spaces AS "space!: Space"
3 | FROM
4 | spaces
5 | WHERE
6 | spaces.owner_id = $1
7 | AND spaces.deleted = FALSE;
8 |
9 |
--------------------------------------------------------------------------------
/packages/interpreter/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './eval';
2 | export * from './entities';
3 | export * from './parse-result';
4 | export * from './parser';
5 | export * from './to-parsed';
6 |
--------------------------------------------------------------------------------
/packages/utils/src/errors.ts:
--------------------------------------------------------------------------------
1 | export const isChunkLoadError = (error: unknown): error is Error => {
2 | return error instanceof Error && error.message.includes('Loading chunk');
3 | };
4 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # https://github.com/github-linguist/linguist/blob/master/docs/overrides.md#vendored-code
2 | packages/api/src/bindings.ts linguist-generated
3 | **/.sqlx/* linguist-generated
4 |
--------------------------------------------------------------------------------
/apps/db/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM postgres:18-trixie
2 | COPY postgresql.conf /etc/postgresql/postgresql.conf
3 |
4 | CMD ["postgres", "-c", "config_file=/etc/postgresql/postgresql.conf"]
5 | EXPOSE 5432
6 |
--------------------------------------------------------------------------------
/apps/server/sql/spaces/get_by_channel.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | s AS "space!: Space"
3 | FROM
4 | channels ch
5 | INNER JOIN spaces s ON ch.space_id = s.id
6 | WHERE
7 | ch.id = $1;
8 |
9 |
--------------------------------------------------------------------------------
/apps/server/src/channels.rs:
--------------------------------------------------------------------------------
1 | pub mod api;
2 | pub mod handlers;
3 | pub mod models;
4 |
5 | pub use handlers::router;
6 | pub use models::{Channel, ChannelMember, ChannelMembers, ChannelType};
7 |
--------------------------------------------------------------------------------
/docs/api/Users/Logout.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Logout
3 | type: http
4 | seq: 6
5 | }
6 |
7 | get {
8 | url: {{base_url}}/users/logout
9 | body: none
10 | auth: inherit
11 | }
12 |
--------------------------------------------------------------------------------
/apps/server/sql/channels/fetch_channel.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | ch AS "channel!: Channel"
3 | FROM
4 | channels ch
5 | WHERE
6 | ch.id = $1
7 | AND deleted = FALSE
8 | LIMIT 1;
9 |
10 |
--------------------------------------------------------------------------------
/apps/server/sql/users/create.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO users (email, username, nickname, PASSWORD)
2 | VALUES ($1, $2, $3, crypt($4, gen_salt('bf')))
3 | RETURNING
4 | users AS "users!: User";
5 |
6 |
--------------------------------------------------------------------------------
/apps/server/sql/users/get_by_reset_token.sql:
--------------------------------------------------------------------------------
1 | SELECT users AS "users!: User"
2 | FROM users INNER JOIN reset_tokens ON users.id = reset_tokens.user_id
3 | WHERE reset_tokens.token = $1
4 | LIMIT 1;
5 |
--------------------------------------------------------------------------------
/scripts/generate-types.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | cargo run -p server -- --types
4 |
5 | npm exec prettier -- --write ./packages/types/bindings.ts
6 | cargo sqlx prepare --workspace -- --tests
7 |
--------------------------------------------------------------------------------
/apps/legacy/src/hooks/useIsLoggedIn.ts:
--------------------------------------------------------------------------------
1 | import { useSelector } from '../store';
2 |
3 | export function useIsLoggedIn(): boolean {
4 | return useSelector((state) => state.profile !== undefined);
5 | }
6 |
--------------------------------------------------------------------------------
/apps/storybook/wrangler.toml:
--------------------------------------------------------------------------------
1 | name = "boluo-storybook"
2 | compatibility_date = "2025-07-25"
3 |
4 | [assets]
5 | directory = "./storybook-static/"
6 | not_found_handling = "single-page-application"
7 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "editorconfig.editorconfig",
4 | "bradlc.vscode-tailwindcss",
5 | "dbaeumer.vscode-eslint",
6 | "esbenp.prettier-vscode"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/apps/server/sql/spaces/put_settings.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO spaces_extension (space_id, settings)
2 | VALUES ($1, $2)
3 | ON CONFLICT (space_id)
4 | DO UPDATE SET
5 | settings = EXCLUDED.settings;
6 |
7 |
--------------------------------------------------------------------------------
/docs/api/Info/Health Check.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Health Check
3 | type: http
4 | seq: 2
5 | }
6 |
7 | get {
8 | url: {{base_url}}/info/healthcheck
9 | body: none
10 | auth: inherit
11 | }
12 |
--------------------------------------------------------------------------------
/docs/api/Users/Get Settings.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Get Settings
3 | type: http
4 | seq: 7
5 | }
6 |
7 | get {
8 | url: {{base_url}}/users/settings
9 | body: none
10 | auth: inherit
11 | }
12 |
--------------------------------------------------------------------------------
/packages/tailwind-config/fonts/fusion-pixel/fusion-pixel-10px-monospaced-ja.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mythal/boluo/HEAD/packages/tailwind-config/fonts/fusion-pixel/fusion-pixel-10px-monospaced-ja.woff2
--------------------------------------------------------------------------------
/packages/tailwind-config/fonts/fusion-pixel/fusion-pixel-10px-monospaced-ko.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mythal/boluo/HEAD/packages/tailwind-config/fonts/fusion-pixel/fusion-pixel-10px-monospaced-ko.woff2
--------------------------------------------------------------------------------
/packages/typescript-config/react-library.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "extends": "./base.json",
4 | "compilerOptions": {
5 | "jsx": "react-jsx"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/apps/server/sql/messages/max_pos.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | pos_p,
3 | pos_q,
4 | id
5 | FROM
6 | messages msg
7 | WHERE
8 | channel_id = $1
9 | ORDER BY
10 | msg.pos DESC
11 | LIMIT 1;
12 |
13 |
--------------------------------------------------------------------------------
/packages/tailwind-config/fonts/fusion-pixel/fusion-pixel-10px-monospaced-latin.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mythal/boluo/HEAD/packages/tailwind-config/fonts/fusion-pixel/fusion-pixel-10px-monospaced-latin.woff2
--------------------------------------------------------------------------------
/apps/legacy/src/components/atoms/Label.tsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { block, pY } from '../../styles/atoms';
3 |
4 | export const Label = styled.label`
5 | ${pY(2)};
6 | ${block};
7 | `;
8 |
--------------------------------------------------------------------------------
/apps/server/sql/channels/is_master.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | is_master
3 | FROM
4 | channel_members cm
5 | WHERE
6 | cm.user_id = $1
7 | AND cm.channel_id = $2
8 | AND cm.is_joined
9 | LIMIT 1;
10 |
11 |
--------------------------------------------------------------------------------
/apps/server/sql/spaces/refresh_token.sql:
--------------------------------------------------------------------------------
1 | UPDATE
2 | spaces
3 | SET
4 | invite_token = gen_random_uuid ()
5 | WHERE
6 | id = $1
7 | AND deleted = FALSE
8 | RETURNING
9 | invite_token;
10 |
11 |
--------------------------------------------------------------------------------
/packages/tailwind-config/fonts/fusion-pixel/fusion-pixel-10px-monospaced-zh_hans.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mythal/boluo/HEAD/packages/tailwind-config/fonts/fusion-pixel/fusion-pixel-10px-monospaced-zh_hans.woff2
--------------------------------------------------------------------------------
/packages/tailwind-config/fonts/fusion-pixel/fusion-pixel-10px-monospaced-zh_hant.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mythal/boluo/HEAD/packages/tailwind-config/fonts/fusion-pixel/fusion-pixel-10px-monospaced-zh_hant.woff2
--------------------------------------------------------------------------------
/packages/ui/src/classes.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 |
3 | export const link = clsx(
4 | 'text-text-link hover:text-text-link-hover active:text-text-link-active underline decoration-text-link-decoration',
5 | );
6 |
--------------------------------------------------------------------------------
/prettier.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import("prettier").Config} */
2 | const config = {
3 | plugins: ['prettier-plugin-tailwindcss'],
4 | singleQuote: true,
5 | printWidth: 100,
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/apps/server/sql/spaces/get_space_member.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | member AS "space_member!: SpaceMember"
3 | FROM
4 | space_members member
5 | WHERE
6 | user_id = $1
7 | AND space_id = $2
8 | LIMIT 1;
9 |
10 |
--------------------------------------------------------------------------------
/apps/server/sql/users/reset_token_invalidate.sql:
--------------------------------------------------------------------------------
1 | UPDATE reset_tokens
2 | SET
3 | invalidated_at = now() at time zone 'utc'
4 | WHERE
5 | user_id = $1
6 | AND used_at IS NULL
7 | AND invalidated_at IS NULL;
8 |
--------------------------------------------------------------------------------
/apps/spa/hooks/useDefaultInGame.tsx:
--------------------------------------------------------------------------------
1 | import { useChannel } from './useChannel';
2 |
3 | export const useDefaultInGame = (): boolean => {
4 | const channel = useChannel();
5 | return channel?.type === 'IN_GAME';
6 | };
7 |
--------------------------------------------------------------------------------
/apps/server/sql/channels/get_color_list.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | user_id,
3 | text_color AS "color!"
4 | FROM
5 | channel_members cm
6 | WHERE
7 | cm.channel_id = $1
8 | AND cm.text_color IS NOT NULL;
9 |
10 |
--------------------------------------------------------------------------------
/apps/server/sql/media/create.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO media (id, mime_type, uploader_id, filename, original_filename, hash, size, source)
2 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
3 | RETURNING
4 | media AS "media!: Media";
5 |
6 |
--------------------------------------------------------------------------------
/apps/server/sql/messages/by_pos.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | msg AS "message!: Message"
3 | FROM
4 | messages msg
5 | WHERE
6 | msg.channel_id = $1
7 | AND msg.pos_p = $2
8 | AND msg.pos_q = $3
9 | LIMIT 1;
10 |
11 |
--------------------------------------------------------------------------------
/apps/server/sql/spaces/search.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | spaces AS "space!: Space"
3 | FROM
4 | spaces
5 | WHERE
6 | deleted = FALSE
7 | AND concat(name, ' ', description)
8 | LIKE ALL ($1)
9 | LIMIT 1024;
10 |
11 |
--------------------------------------------------------------------------------
/apps/server/text/error_serialize_error.json:
--------------------------------------------------------------------------------
1 | {
2 | "is_ok": false,
3 | "ok": null,
4 | "err": {
5 | "code": "UNEXPECTED",
6 | "message": "Unexpected serialize error",
7 | "table": null
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/apps/site/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from 'react';
2 | import '@boluo/tailwind-config';
3 |
4 | export default function RootLayout({ children }: { children: ReactNode }) {
5 | return <>{children}>;
6 | }
7 |
--------------------------------------------------------------------------------
/apps/spa/components/sidebar/SidebarGuestContent.tsx:
--------------------------------------------------------------------------------
1 | import { type FC } from 'react';
2 |
3 | export const SidebarGuestContent: FC = () => {
4 | return {/* Guest content can be added here in the future */}
;
5 | };
6 |
--------------------------------------------------------------------------------
/apps/spa/hooks/usePaneKey.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { PaneContext } from '../state/view.context';
3 |
4 | export const usePaneKey = (): number | null => {
5 | return useContext(PaneContext).key;
6 | };
7 |
--------------------------------------------------------------------------------
/packages/ui/src/chat/MessageHandleBox.tsx:
--------------------------------------------------------------------------------
1 | export const MessageHandleBox = ({ children }: { children: React.ReactNode }) => {
2 | return {children}
;
3 | };
4 |
--------------------------------------------------------------------------------
/apps/server/sql/channels/create_channel.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO channels (space_id, name, is_public, default_dice_type, "type")
2 | VALUES ($1, $2, $3, COALESCE($4, 'd20'), $5)
3 | RETURNING
4 | channels AS "channel!: Channel";
5 |
6 |
--------------------------------------------------------------------------------
/apps/server/sql/spaces/all.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | spaces AS "space!: Space"
3 | FROM
4 | spaces
5 | WHERE
6 | deleted = FALSE
7 | AND explorable = TRUE
8 | ORDER BY
9 | latest_activity DESC
10 | LIMIT 512;
11 |
12 |
--------------------------------------------------------------------------------
/apps/server/sql/users/reset_token_use.sql:
--------------------------------------------------------------------------------
1 | UPDATE reset_tokens
2 | SET
3 | used_at = now() at time zone 'utc'
4 | WHERE
5 | token = $1
6 | AND user_id = $2
7 | AND used_at IS NULL
8 | AND invalidated_at IS NULL;
9 |
--------------------------------------------------------------------------------
/apps/server/text/email-verification/content.zh-CN.html:
--------------------------------------------------------------------------------
1 | 您好!
2 | 感谢您注册 Boluo。请点击下面的链接来验证您的邮箱地址:
3 | 验证邮箱
4 | 此链接将在24小时后过期。
5 | 如果您没有注册 Boluo 账户,请忽略此邮件。
6 |
--------------------------------------------------------------------------------
/apps/spa/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import '@boluo/tailwind-config';
2 |
3 | import type { AppProps } from 'next/app';
4 |
5 | export default function SpaApp({ Component, pageProps }: AppProps) {
6 | return ;
7 | }
8 |
--------------------------------------------------------------------------------
/packages/typescript-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@boluo/typescript-config",
3 | "version": "0.0.0",
4 | "license": "MIT",
5 | "private": true,
6 | "publishConfig": {
7 | "access": "public"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/utils/src/id.ts:
--------------------------------------------------------------------------------
1 | import { v1 as makeId, validate } from 'uuid';
2 |
3 | export type Id = string;
4 |
5 | export const isUuid = (x: unknown): x is string => typeof x === 'string' && validate(x);
6 |
7 | export { makeId };
8 |
--------------------------------------------------------------------------------
/apps/legacy/src/hooks/useMyId.ts:
--------------------------------------------------------------------------------
1 | import { useSelector } from '../store';
2 | import { type Id } from '../utils/id';
3 |
4 | export const useMyId = (): Id | undefined => {
5 | return useSelector((state) => state.profile?.user.id);
6 | };
7 |
--------------------------------------------------------------------------------
/apps/server/sql/channels/delete_channel.sql:
--------------------------------------------------------------------------------
1 | UPDATE
2 | channels
3 | SET
4 | deleted = TRUE,
5 | old_name = name,
6 | name = uuid_generate_v4 ()::text
7 | WHERE
8 | id = $1
9 | AND deleted = FALSE;
10 |
11 |
--------------------------------------------------------------------------------
/apps/server/sql/messages/set_folded.sql:
--------------------------------------------------------------------------------
1 | UPDATE
2 | messages
3 | SET
4 | folded = $2,
5 | modified = (now() at time zone 'utc')
6 | WHERE
7 | id = $1
8 | RETURNING
9 | messages AS "message!: Message";
10 |
11 |
--------------------------------------------------------------------------------
/apps/server/sql/messages/update_space_latest_activity.sql:
--------------------------------------------------------------------------------
1 | UPDATE
2 | spaces
3 | SET
4 | latest_activity = $2
5 | FROM
6 | channels
7 | WHERE
8 | spaces.id = channels.space_id
9 | AND channels.id = $1;
10 |
11 |
--------------------------------------------------------------------------------
/apps/server/sql/spaces/create.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO spaces ("name", owner_id, "password", default_dice_type, "description")
2 | VALUES ($1, $2, COALESCE($3, ''), COALESCE($4, 'd20'), $5)
3 | RETURNING
4 | spaces AS "space!: Space";
5 |
6 |
--------------------------------------------------------------------------------
/apps/server/text/email-verification/content.zh-TW.html:
--------------------------------------------------------------------------------
1 | 您好!
2 | 感謝您註冊 Boluo。請點擊下面的連結來驗證您的電子郵件地址:
3 | 驗證電子郵件
4 | 此連結將在24小時後過期。
5 | 如果您沒有註冊 Boluo 帳戶,請忽略此郵件。
6 |
--------------------------------------------------------------------------------
/apps/spa/hooks/useDefaultRollCommand.tsx:
--------------------------------------------------------------------------------
1 | import { useChannel } from './useChannel';
2 |
3 | export const useDefaultRollCommand = (): string => {
4 | const channel = useChannel();
5 | return channel?.defaultRollCommand || 'd';
6 | };
7 |
--------------------------------------------------------------------------------
/apps/spa/hooks/useIsScrolling.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from 'react';
2 |
3 | export const IsScrollingContext = createContext(false);
4 |
5 | export const useIsScrolling = (): boolean => useContext(IsScrollingContext);
6 |
--------------------------------------------------------------------------------
/apps/spa/hooks/usePaneIsFocus.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { PaneContext } from '../state/view.context';
3 |
4 | export const usePaneIsFocus = (): boolean => {
5 | return useContext(PaneContext).focused;
6 | };
7 |
--------------------------------------------------------------------------------
/apps/spa/state/pane-size.ts:
--------------------------------------------------------------------------------
1 | import { atom } from 'jotai';
2 | import React from 'react';
3 |
4 | const sizeLevelAtom = atom(0);
5 |
6 | export const SizeLevelContext = React.createContext(sizeLevelAtom);
7 |
--------------------------------------------------------------------------------
/packages/store/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@boluo/typescript-config/base.json",
3 | "compilerOptions": {
4 | "outDir": "dist/"
5 | },
6 | "include": ["src/**/*.ts"],
7 | "exclude": ["dist", "build", "node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/apps/server/sql/channels/get_by_space.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | channel AS "channel!: Channel"
3 | FROM
4 | channels channel
5 | WHERE
6 | channel.space_id = $1
7 | AND deleted = FALSE
8 | ORDER BY
9 | channel.created;
10 |
11 |
--------------------------------------------------------------------------------
/apps/server/text/reset-password/content.ja.html:
--------------------------------------------------------------------------------
1 |
2 | パスワードのリセットをリクエストしました。 パスワードをリセットするには、ここをクリックしてください。
6 |
7 |
8 | パスワードのリセットをリクエストしていない場合は、このメールを無視してください。
9 |
--------------------------------------------------------------------------------
/apps/spa/state/unread.atoms.ts:
--------------------------------------------------------------------------------
1 | import { atomFamily, atomWithStorage } from 'jotai/utils';
2 |
3 | export const channelReadFamily = atomFamily((channelId: string) =>
4 | atomWithStorage(`channel:${channelId}:read`, Number.MIN_VALUE),
5 | );
6 |
--------------------------------------------------------------------------------
/packages/ui/src/entities/top-level.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const IsTopLevelContext = React.createContext(true);
4 |
5 | export function useIsTopLevel() {
6 | return React.useContext(IsTopLevelContext);
7 | }
8 |
--------------------------------------------------------------------------------
/apps/spa/components/pane-profile/ShowUsername.tsx:
--------------------------------------------------------------------------------
1 | import { type FC } from 'react';
2 |
3 | export const ShowUsername: FC<{ username: string }> = ({ username }) => {
4 | return {username}
;
5 | };
6 |
--------------------------------------------------------------------------------
/apps/spa/hooks/useIsChildPane.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from 'react';
2 |
3 | export const IsChildPaneContext = createContext(false);
4 |
5 | export const useIsChildPane = () => {
6 | return useContext(IsChildPaneContext);
7 | };
8 |
--------------------------------------------------------------------------------
/apps/theme-designer/README.md:
--------------------------------------------------------------------------------
1 | # Theme Designer
2 |
3 | A GUI tool to help design themes for the application.
4 |
5 | ## Prerequisites
6 |
7 | - ripgrep
8 |
9 | ## Running
10 |
11 | ```
12 | cargo run --package theme-designer
13 | ```
14 |
--------------------------------------------------------------------------------
/apps/server/text/email-change/content.zh-CN.html:
--------------------------------------------------------------------------------
1 | 您好!
2 | 您已请求更改您的 Boluo 账户邮箱地址。请点击下面的链接确认此更改:
3 | 确认邮箱更改
4 | 此链接将在24小时后失效。
5 | 如果您没有请求更改邮箱,请忽略此邮件,您当前的邮箱地址将保持不变。
6 |
--------------------------------------------------------------------------------
/apps/server/text/email-change/content.zh-TW.html:
--------------------------------------------------------------------------------
1 | 您好!
2 | 您已請求變更您的 Boluo 帳戶信箱地址。請點擊下面的連結確認此變更:
3 | 確認信箱變更
4 | 此連結將在24小時後失效。
5 | 如果您沒有請求變更信箱,請忽略此郵件,您目前的信箱地址將保持不變。
6 |
--------------------------------------------------------------------------------
/apps/spa/hooks/useIsOptimistic.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from 'react';
2 |
3 | export const IsOptimisticContext = createContext(false);
4 |
5 | export const useIsOptimistic = () => {
6 | return useContext(IsOptimisticContext);
7 | };
8 |
--------------------------------------------------------------------------------
/apps/spa/hooks/useIsReordering.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from 'react';
2 |
3 | export const IsReorderingContext = createContext(false);
4 |
5 | export const useIsReordering = () => {
6 | return useContext(IsReorderingContext);
7 | };
8 |
--------------------------------------------------------------------------------
/packages/hooks/useIsClient.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | export const useIsClient = () => {
4 | const [isClient, setIsClient] = useState(false);
5 | useEffect(() => setIsClient(true), []);
6 | return isClient;
7 | };
8 |
--------------------------------------------------------------------------------
/packages/icons/svgr.config.js:
--------------------------------------------------------------------------------
1 | // see https://react-svgr.com/docs/options/
2 | module.exports = {
3 | typescript: true,
4 | outDir: './src/',
5 | jsxRuntime: 'automatic',
6 | icon: '1em',
7 | memo: false,
8 | prettier: false,
9 | };
10 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 | [*]
3 | end_of_line = lf
4 | insert_final_newline = true
5 | trim_trailing_whitespace = true
6 | charset = utf-8
7 |
8 | [*.{js,jsx,mjs,cjs,ts,tsx,json,css,webmanifest}]
9 | indent_style = space
10 | indent_size = 2
11 |
--------------------------------------------------------------------------------
/apps/legacy/src/components/atoms/PanelTitle.tsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { fontNormal, mY, textXl } from '../../styles/atoms';
3 |
4 | export const PanelTitle = styled.h1`
5 | ${textXl};
6 | ${fontNormal};
7 | ${mY(2)};
8 | `;
9 |
--------------------------------------------------------------------------------
/docs/api/Users/Remove Avatar.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Remove Avatar
3 | type: http
4 | seq: 11
5 | }
6 |
7 | post {
8 | url: {{base_url}}/users/remove_avatar
9 | body: none
10 | auth: inherit
11 | }
12 |
13 | body:json {
14 | null
15 | }
16 |
--------------------------------------------------------------------------------
/packages/utils/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@boluo/typescript-config/base.json",
3 | "compilerOptions": {
4 | "outDir": "dist",
5 | "rootDir": "src"
6 | },
7 | "include": ["src"],
8 | "exclude": ["node_modules", "dist"]
9 | }
10 |
--------------------------------------------------------------------------------
/apps/server/sql/spaces/get_members_by_channel.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | sm AS "space_member!: SpaceMember"
3 | FROM
4 | space_members sm
5 | INNER JOIN channels c ON c.space_id = sm.space_id
6 | WHERE
7 | sm.user_id = $1
8 | AND c.id = $2;
9 |
10 |
--------------------------------------------------------------------------------
/apps/server/sql/spaces/set_space_member.sql:
--------------------------------------------------------------------------------
1 | UPDATE
2 | space_members
3 | SET
4 | is_admin = COALESCE($1, is_admin)
5 | WHERE
6 | user_id = $2
7 | AND space_id = $3
8 | RETURNING
9 | space_members AS "space_member!: SpaceMember";
10 |
11 |
--------------------------------------------------------------------------------
/apps/server/sql/users/set_settings.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO users_extension (user_id, settings)
2 | VALUES ($1, $2)
3 | ON CONFLICT (user_id)
4 | DO UPDATE SET
5 | settings = $2
6 | RETURNING
7 | users_extension AS "user_ext!: UserExt";
8 |
9 |
--------------------------------------------------------------------------------
/docs/api/Users/Check Email Verification Status.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Check Email Verification Status
3 | type: http
4 | seq: 16
5 | }
6 |
7 | get {
8 | url: {{base_url}}/users/email_verification_status
9 | body: none
10 | auth: inherit
11 | }
12 |
--------------------------------------------------------------------------------
/apps/server/migrations/20250930021446_remove-messages-foregin-keys.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE messages DROP CONSTRAINT IF EXISTS message_parent;
2 | ALTER TABLE messages DROP CONSTRAINT IF EXISTS message_channel;
3 | ALTER TABLE messages DROP CONSTRAINT IF EXISTS message_sender;
4 |
--------------------------------------------------------------------------------
/apps/server/sql/channels/get_channel_member_list_by_user.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | m AS "member!: ChannelMember"
3 | FROM
4 | channel_members m
5 | INNER JOIN channels c ON c.id = m.channel_id
6 | WHERE
7 | m.user_id = $1
8 | AND m.is_joined;
9 |
10 |
--------------------------------------------------------------------------------
/apps/server/sql/users/login.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | (PASSWORD = crypt($2, PASSWORD)) AS "password_match!",
3 | users AS "user!: User"
4 | FROM
5 | users
6 | WHERE (username = $1
7 | OR email = lower($1))
8 | AND deactivated = FALSE
9 | LIMIT 1;
10 |
11 |
--------------------------------------------------------------------------------
/apps/site/src/app/[lang]/[theme]/account/loading.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingText } from '@boluo/ui/LoadingText';
2 |
3 | export default function Loading() {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/apps/spa/state/actions.ts:
--------------------------------------------------------------------------------
1 | export type MakeAction<
2 | ActionMap extends Record,
3 | ActionName,
4 | > = ActionName extends keyof ActionMap
5 | ? {
6 | type: ActionName;
7 | payload: ActionMap[ActionName];
8 | }
9 | : never;
10 |
--------------------------------------------------------------------------------
/apps/theme-designer/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "theme-designer"
3 | version = "0.1.0"
4 | edition = "2024"
5 |
6 | [dependencies]
7 | anyhow = "1.0"
8 | eframe = { version = "0.33", features = ["default"] }
9 | egui_extras = "0.33"
10 | once_cell = "1.19"
11 |
--------------------------------------------------------------------------------
/docs/api/Spaces/Get Token.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Get Token
3 | type: http
4 | seq: 7
5 | }
6 |
7 | get {
8 | url: {{base_url}}/spaces/token?id={{space_id}}
9 | body: none
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | id: {{space_id}}
15 | }
16 |
--------------------------------------------------------------------------------
/docs/api/Spaces/Members.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Members
3 | type: http
4 | seq: 6
5 | }
6 |
7 | get {
8 | url: {{base_url}}/spaces/members?id={{space_id}}
9 | body: none
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | id: {{space_id}}
15 | }
16 |
--------------------------------------------------------------------------------
/docs/api/Users/Query User.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Query User
3 | type: http
4 | seq: 4
5 | }
6 |
7 | get {
8 | url: {{base_url}}/users/query?id={{user_id}}
9 | body: none
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | id: {{user_id}}
15 | }
16 |
--------------------------------------------------------------------------------
/packages/ui/src/HelpText.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from 'react';
2 | import { type ChildrenProps } from '@boluo/types';
3 |
4 | export const HelpText: FC = ({ children }) => (
5 | {children}
6 | );
7 |
--------------------------------------------------------------------------------
/.taplo.toml:
--------------------------------------------------------------------------------
1 | [formatting]
2 | align_entries = false
3 | indent_tables = true
4 | indent_entries = true
5 | indent_string = ' '
6 |
7 | [[rule]]
8 | include = ["**/Cargo.toml"]
9 | [rule.formatting]
10 | indent_tables = false
11 | indent_entries = false
12 |
--------------------------------------------------------------------------------
/apps/legacy/src/components/atoms/TextArea.tsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { inputStyle } from './Input';
3 |
4 | const TextArea = styled.textarea`
5 | ${inputStyle};
6 | resize: none;
7 | width: 100%;
8 | `;
9 |
10 | export default TextArea;
11 |
--------------------------------------------------------------------------------
/apps/server/sql/channels/get_channel_by_name.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | ch AS "channel!: Channel"
3 | FROM
4 | spaces s
5 | INNER JOIN channels ch ON ch.space_id = s.id
6 | WHERE
7 | s.id = $1
8 | AND ch.name = $2
9 | AND ch.deleted = FALSE
10 | LIMIT 1;
11 |
12 |
--------------------------------------------------------------------------------
/apps/server/sql/spaces/get_members_by_spaces.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | m AS "space!: SpaceMember",
3 | u AS "user!: User"
4 | FROM
5 | space_members m
6 | INNER JOIN users u ON u.id = m.user_id
7 | WHERE
8 | space_id = $1
9 | AND u.deactivated = FALSE;
10 |
11 |
--------------------------------------------------------------------------------
/apps/server/text/reset-password/content.en.html:
--------------------------------------------------------------------------------
1 |
2 | You have requested to reset your password.
3 | Click here to reset your password.
4 |
5 |
6 | If you did not request to reset your password, please ignore this email.
7 |
--------------------------------------------------------------------------------
/apps/spa/components/sidebar/ConnectionIndicatorConnecting.tsx:
--------------------------------------------------------------------------------
1 | import { type FC } from 'react';
2 | import { FormattedMessage } from 'react-intl';
3 |
4 | export const ConnectionIndicatorConnecting: FC = () => {
5 | return ;
6 | };
7 |
--------------------------------------------------------------------------------
/apps/spa/hooks/useIsDragging.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from 'react';
2 |
3 | export const IsDraggingContext = createContext(false);
4 |
5 | export const useIsDragging = () => {
6 | const isDragging = useContext(IsDraggingContext);
7 | return isDragging;
8 | };
9 |
--------------------------------------------------------------------------------
/docs/api/Channels/Members.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Members
3 | type: http
4 | seq: 3
5 | }
6 |
7 | get {
8 | url: {{base_url}}/channels/members?id={{channel_id}}
9 | body: none
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | id: {{channel_id}}
15 | }
16 |
--------------------------------------------------------------------------------
/docs/api/Spaces/Get Settings.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Get Settings
3 | type: http
4 | seq: 8
5 | }
6 |
7 | get {
8 | url: {{base_url}}/spaces/settings?id={{space_id}}
9 | body: none
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | id: {{space_id}}
15 | }
16 |
--------------------------------------------------------------------------------
/apps/legacy/src/components/atoms/Title.tsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { m, p, pB, textXl } from '../../styles/atoms';
3 |
4 | const Title = styled.h1`
5 | ${[textXl, p(0), pB(6), m(0)]};
6 | font-weight: normal;
7 | `;
8 |
9 | export default Title;
10 |
--------------------------------------------------------------------------------
/apps/server/src/ts.rs:
--------------------------------------------------------------------------------
1 | use specta_typescript::Typescript;
2 |
3 | pub fn export() {
4 | Typescript::default()
5 | .bigint(specta_typescript::BigIntExportBehavior::Number)
6 | .export_to("./packages/types/bindings.ts", &specta::export())
7 | .unwrap();
8 | }
9 |
--------------------------------------------------------------------------------
/apps/server/text/email-verification/content.ja.html:
--------------------------------------------------------------------------------
1 | こんにちは!
2 | Boluoにご登録いただき、ありがとうございます。以下のリンクをクリックしてメールアドレスを認証してください:
3 | メールアドレスを認証
4 | このリンクは24時間後に期限切れになります。
5 | もしBoluoアカウントにご登録されていない場合は、このメールを無視してください。
6 |
--------------------------------------------------------------------------------
/apps/spa/components/PaneEmpty.tsx:
--------------------------------------------------------------------------------
1 | import { PaneSimpleBox } from './PaneSimpleBox';
2 |
3 | export const PaneEmpty = () => {
4 | return (
5 |
6 | ∅
7 |
8 | );
9 | };
10 |
--------------------------------------------------------------------------------
/apps/spa/hooks/useSpace.tsx:
--------------------------------------------------------------------------------
1 | import { type Space } from '@boluo/api';
2 | import React from 'react';
3 |
4 | /** The current space. */
5 | export const SpaceContext = React.createContext(undefined);
6 |
7 | export const useSpace = () => React.useContext(SpaceContext);
8 |
--------------------------------------------------------------------------------
/apps/spa/public/owlbear-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Boluo",
3 | "version": "1.0.0",
4 | "manifest_version": 1,
5 | "action": {
6 | "title": "Chat",
7 | "icon": "/icon.png",
8 | "popover": "/en",
9 | "height": 800,
10 | "width": 500
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/docs/api/Users/Check Username.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Check Username
3 | type: http
4 | seq: 8
5 | }
6 |
7 | get {
8 | url: {{base_url}}/users/check_username?username=bruno
9 | body: none
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | username: bruno
15 | }
16 |
--------------------------------------------------------------------------------
/apps/legacy/src/assets/icons/unfold.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/legacy/src/components/atoms/Separator.tsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { lineColor } from '../../styles/colors';
3 |
4 | const Separator = styled.hr`
5 | border: none;
6 | border-bottom: 1px solid ${lineColor};
7 | `;
8 |
9 | export default Separator;
10 |
--------------------------------------------------------------------------------
/apps/legacy/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/apps/server/sql/channels/fetch_channel_with_space.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | ch AS "channel!: Channel",
3 | s AS "space!: Space"
4 | FROM
5 | channels ch
6 | INNER JOIN spaces s ON ch.space_id = s.id
7 | WHERE
8 | ch.id = $1
9 | AND ch.deleted = FALSE
10 | LIMIT 1;
11 |
12 |
--------------------------------------------------------------------------------
/apps/server/sql/messages/export.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | msg AS "message!: Message"
3 | FROM
4 | messages msg
5 | WHERE
6 | msg.channel_id = $1
7 | AND msg.deleted = FALSE
8 | AND msg.created > coalesce($2, to_timestamp(0)::timestamptz)
9 | ORDER BY
10 | msg.pos;
11 |
12 |
--------------------------------------------------------------------------------
/apps/server/sql/messages/get_after_pos.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | msg AS "message!: Message"
3 | FROM
4 | messages msg
5 | WHERE
6 | msg.channel_id = $1
7 | AND msg.deleted = FALSE
8 | AND ($2::float8 IS NULL OR msg.pos > $2)
9 | ORDER BY
10 | msg.pos ASC
11 | LIMIT $3;
12 |
--------------------------------------------------------------------------------
/docs/api/Channels/All Members.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: All Members
3 | type: http
4 | seq: 4
5 | }
6 |
7 | get {
8 | url: {{base_url}}/channels/all_members?id={{channel_id}}
9 | body: none
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | id: {{channel_id}}
15 | }
16 |
--------------------------------------------------------------------------------
/docs/api/Channels/Query Channel.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Query Channel
3 | type: http
4 | seq: 1
5 | }
6 |
7 | get {
8 | url: {{base_url}}/channels/query?id={{channel_id}}
9 | body: none
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | id: {{channel_id}}
15 | }
16 |
--------------------------------------------------------------------------------
/docs/api/Messages/Query Message.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Query Message
3 | type: http
4 | seq: 2
5 | }
6 |
7 | get {
8 | url: {{base_url}}/messages/query?id={{message_id}}
9 | body: none
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | id: {{message_id}}
15 | }
16 |
--------------------------------------------------------------------------------
/docs/api/Spaces/Query Space.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Query Space
3 | type: http
4 | seq: 2
5 | }
6 |
7 | get {
8 | url: {{base_url}}/spaces/query?id={{space_id}}
9 | body: none
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | id: {{space_id}}
15 | token:
16 | }
17 |
--------------------------------------------------------------------------------
/packages/api/src/patch.ts:
--------------------------------------------------------------------------------
1 | import type { EditMessage, Message } from '@boluo/types/bindings';
2 |
3 | export interface Patch {
4 | '/users/update_settings': { payload: object; query: null; result: object };
5 | '/messages/edit': { payload: EditMessage; query: null; result: Message };
6 | }
7 |
--------------------------------------------------------------------------------
/packages/ui/src/OopsKaomoji.tsx:
--------------------------------------------------------------------------------
1 | interface Props {
2 | className?: string;
3 | }
4 |
5 | export const OopsKaomoji = ({ className }: Props) => {
6 | return (
7 |
8 | (; ・`д・´)
9 |
10 | );
11 | };
12 |
--------------------------------------------------------------------------------
/apps/legacy/src/assets/icons/fold.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/legacy/src/hooks/useForceUpdate.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'react';
2 |
3 | export function useForceUpdate() {
4 | const [, setValue] = useState(0); // integer state
5 | return useCallback(() => setValue((value) => ++value), []); // update the state to force render
6 | }
7 |
--------------------------------------------------------------------------------
/apps/server/sql/channels/remove_user_by_space.sql:
--------------------------------------------------------------------------------
1 | UPDATE
2 | channel_members cm
3 | SET
4 | is_joined = FALSE
5 | FROM
6 | channels ch
7 | WHERE
8 | cm.user_id = $1
9 | AND ch.space_id = $2
10 | AND cm.channel_id = ch.id
11 | RETURNING
12 | cm.channel_id;
13 |
14 |
--------------------------------------------------------------------------------
/apps/server/sql/channels/set_color.sql:
--------------------------------------------------------------------------------
1 | UPDATE
2 | channel_members
3 | SET
4 | text_color = COALESCE($3, text_color)
5 | WHERE
6 | user_id = $1
7 | AND channel_id = $2
8 | AND is_joined
9 | RETURNING
10 | channel_members AS "channel_member!: ChannelMember";
11 |
12 |
--------------------------------------------------------------------------------
/apps/server/sql/channels/set_master.sql:
--------------------------------------------------------------------------------
1 | UPDATE
2 | channel_members
3 | SET
4 | is_master = COALESCE($3, is_master)
5 | WHERE
6 | user_id = $1
7 | AND channel_id = $2
8 | AND is_joined
9 | RETURNING
10 | channel_members AS "channel_member!: ChannelMember";
11 |
12 |
--------------------------------------------------------------------------------
/apps/server/sql/users/partial_set_settings.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO users_extension (user_id, settings)
2 | VALUES ($1, $2)
3 | ON CONFLICT (user_id)
4 | DO UPDATE SET
5 | settings = users_extension.settings || $2
6 | RETURNING
7 | users_extension AS "user_ext!: UserExt";
8 |
9 |
--------------------------------------------------------------------------------
/apps/site/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@boluo/typescript-config/nextjs.json",
3 | "compilerOptions": {
4 | "plugins": [{ "name": "next" }]
5 | },
6 | "include": ["next.config.ts", "next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/apps/spa/hooks/useComposeAtom.tsx:
--------------------------------------------------------------------------------
1 | import { type ChannelAtoms, useChannelAtoms } from './useChannelAtoms';
2 |
3 | export type ComposeAtom = ChannelAtoms['composeAtom'];
4 |
5 | export const useComposeAtom = (): ChannelAtoms['composeAtom'] => {
6 | return useChannelAtoms().composeAtom;
7 | };
8 |
--------------------------------------------------------------------------------
/apps/spa/hooks/useMaxPane.tsx:
--------------------------------------------------------------------------------
1 | import { useBreakpoint } from '../breakpoint';
2 |
3 | export const usePaneLimit = () => {
4 | const breakpoint = useBreakpoint();
5 | if (breakpoint === 'xs' || breakpoint === 'sm') {
6 | return 1;
7 | } else {
8 | return 10;
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/docs/api/Spaces/Get Users Status.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Get Users Status
3 | type: http
4 | seq: 1
5 | }
6 |
7 | get {
8 | url: {{base_url}}/spaces/users_status?id={{space_id}}
9 | body: none
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | id: {{space_id}}
15 | }
16 |
--------------------------------------------------------------------------------
/docs/api/Spaces/My Space Member.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: My Space Member
3 | type: http
4 | seq: 5
5 | }
6 |
7 | get {
8 | url: {{base_url}}/spaces/my_space_member?id={{space_id}}
9 | body: none
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | id: {{space_id}}
15 | }
16 |
--------------------------------------------------------------------------------
/docs/api/Users/Check Email.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Check Email
3 | type: http
4 | seq: 9
5 | }
6 |
7 | get {
8 | url: {{base_url}}/users/check_email?email=bruno@example.com
9 | body: none
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | email: bruno@example.com
15 | }
16 |
--------------------------------------------------------------------------------
/apps/legacy/src/components/atoms/SpaceGrid.tsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { grid, spacingN } from '../../styles/atoms';
3 |
4 | export const SpaceGrid = styled.div`
5 | ${grid};
6 | grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
7 | gap: ${spacingN(2)};
8 | `;
9 |
--------------------------------------------------------------------------------
/apps/legacy/src/hooks/useToggle.ts:
--------------------------------------------------------------------------------
1 | import { useReducer } from 'react';
2 |
3 | const reducer = (prevState: boolean) => {
4 | return !prevState;
5 | };
6 |
7 | export function useToggle(initialValue: boolean): [boolean, () => void] {
8 | return useReducer(reducer, initialValue);
9 | }
10 |
--------------------------------------------------------------------------------
/apps/server/sql/channels/get_channel_member_list.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | m AS "member!: ChannelMember",
3 | u AS "user!: User"
4 | FROM
5 | channel_members m
6 | INNER JOIN users u ON u.id = m.user_id
7 | WHERE
8 | channel_id = $1
9 | AND ($2
10 | OR is_joined);
11 |
12 |
--------------------------------------------------------------------------------
/apps/server/sql/channels/set_name.sql:
--------------------------------------------------------------------------------
1 | UPDATE
2 | channel_members
3 | SET
4 | character_name = COALESCE($3, character_name)
5 | WHERE
6 | user_id = $1
7 | AND channel_id = $2
8 | AND is_joined
9 | RETURNING
10 | channel_members AS "channel_member!: ChannelMember";
11 |
12 |
--------------------------------------------------------------------------------
/packages/utils/src/async.ts:
--------------------------------------------------------------------------------
1 | export function sleep(ms: number): Promise {
2 | return new Promise((resolve) => setTimeout(resolve, ms));
3 | }
4 |
5 | export function timeout(ms: number): Promise<'TIMEOUT'> {
6 | return new Promise((resolve) => setTimeout(() => resolve('TIMEOUT'), ms));
7 | }
8 |
--------------------------------------------------------------------------------
/apps/server/sql/channels/get_channel_member_list_by_user_and_space.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | m AS "member!: ChannelMember"
3 | FROM
4 | channel_members m
5 | INNER JOIN channels c ON c.id = m.channel_id
6 | WHERE
7 | m.user_id = $1
8 | AND c.space_id = $2
9 | AND m.is_joined;
10 |
11 |
--------------------------------------------------------------------------------
/apps/spa/components/pane-channel/ChatContentLoading.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from 'react';
2 | import { Loading } from '@boluo/ui/Loading';
3 |
4 | export const ChatListLoading: FC = () => {
5 | return (
6 |
7 |
8 |
9 | );
10 | };
11 |
--------------------------------------------------------------------------------
/apps/spa/state/ui.atoms.ts:
--------------------------------------------------------------------------------
1 | import { atom } from 'jotai';
2 | import { atomWithStorage } from 'jotai/utils';
3 |
4 | export const isSidebarExpandedAtom = atomWithStorage('boluo-sidebar-expanded', true);
5 |
6 | export const sidebarContentStateAtom = atom<'CHANNELS' | 'SPACES' | 'CONNECTIONS'>('CHANNELS');
7 |
--------------------------------------------------------------------------------
/docs/api/Channels/Export.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Export
3 | type: http
4 | seq: 6
5 | }
6 |
7 | get {
8 | url: {{base_url}}/channels/export?channelId={{channel_id}}
9 | body: none
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | channelId: {{channel_id}}
15 | after:
16 | }
17 |
--------------------------------------------------------------------------------
/docs/api/Events/Get Token.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Get Token
3 | type: http
4 | seq: 1
5 | }
6 |
7 | get {
8 | url: {{base_url}}/events/token?spaceId={{space_id}}&userId
9 | body: none
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | spaceId: {{space_id}}
15 | userId:
16 | }
17 |
--------------------------------------------------------------------------------
/docs/api/Spaces/Query With Related.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Query With Related
3 | type: http
4 | seq: 4
5 | }
6 |
7 | get {
8 | url: {{base_url}}/spaces/query_with_related?id={{space_id}}
9 | body: none
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | id: {{space_id}}
15 | }
16 |
--------------------------------------------------------------------------------
/packages/backend-proxy/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@boluo/backend-proxy",
3 | "version": "0.0.0",
4 | "devDependencies": {
5 | "@cloudflare/workers-types": "4",
6 | "typescript": "^5.9.3"
7 | },
8 | "license": "AGPL-3.0",
9 | "scripts": {
10 | "build": "tsc"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/packages/ui/src/entities/EntityUnknown.tsx:
--------------------------------------------------------------------------------
1 | import { type FC } from 'react';
2 | import { FormattedMessage } from 'react-intl';
3 |
4 | export const EntityUnknown: FC = () => (
5 |
6 | []
7 |
8 | );
9 |
--------------------------------------------------------------------------------
/apps/legacy/src/hooks/useBaseUrlDelay.ts:
--------------------------------------------------------------------------------
1 | export {
2 | useBaseUrlDelay,
3 | getRouteScore,
4 | getRouteMovingAverage,
5 | getRouteSuccessRate,
6 | getAllRouteStats,
7 | resetRouteStats,
8 | } from './useBaseUrlMovingAverage';
9 | export type { MeasureResult } from './useBaseUrlMovingAverage';
10 |
--------------------------------------------------------------------------------
/apps/legacy/src/utils/profile.ts:
--------------------------------------------------------------------------------
1 | import { ONLINE_TIMEOUT } from '../settings';
2 |
3 | export const isOnline = (timestamp?: number, now?: number) => {
4 | if (!timestamp) {
5 | return false;
6 | }
7 | now = now || new Date().getTime();
8 | return now - timestamp < ONLINE_TIMEOUT;
9 | };
10 |
--------------------------------------------------------------------------------
/apps/server/text/email-change/content.ja.html:
--------------------------------------------------------------------------------
1 | こんにちは!
2 | Boluoアカウントのメールアドレス変更をリクエストされました。以下のリンクをクリックして変更を確認してください:
3 | メールアドレス変更を確認
4 | このリンクは24時間後に有効期限が切れます。
5 | メールアドレス変更をリクエストしていない場合は、このメールを無視してください。現在のメールアドレスは変更されません。
6 |
--------------------------------------------------------------------------------
/apps/spa/hooks/useBannerNode.tsx:
--------------------------------------------------------------------------------
1 | import React, { type RefObject } from 'react';
2 |
3 | export const BannerContext = React.createContext>({
4 | current: null,
5 | });
6 |
7 | export const useBannerNode = (): HTMLDivElement | null => React.useContext(BannerContext).current;
8 |
--------------------------------------------------------------------------------
/docs/api/Channels/Check Name.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Check Name
3 | type: http
4 | seq: 5
5 | }
6 |
7 | get {
8 | url: {{base_url}}/channels/check_name?name&spaceId={{space_id}}
9 | body: none
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | name:
15 | spaceId: {{space_id}}
16 | }
17 |
--------------------------------------------------------------------------------
/packages/api/src/origin-map.ts:
--------------------------------------------------------------------------------
1 | export const originMap = {
2 | 'boluochat.com': 'https://production.boluochat.com',
3 | 'boluo.chat': 'https://production.boluo.chat',
4 | 'boluo-staging.mythal.net': 'https://server.boluo-staging.mythal.net',
5 | '.kagangtuya.top': 'https://boluo-net.kagangtuya.top',
6 | };
7 |
--------------------------------------------------------------------------------
/packages/icons/icons/play.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@boluo/typescript-config/react-library.json",
3 | "compilerOptions": {
4 | "moduleResolution": "bundler",
5 | "module": "esnext",
6 | "rootDir": "src"
7 | },
8 | "include": ["src"],
9 | "exclude": ["dist", "build", "node_modules"]
10 | }
11 |
--------------------------------------------------------------------------------
/apps/spa/hooks/useMember.tsx:
--------------------------------------------------------------------------------
1 | import { type MemberWithUser } from '@boluo/api';
2 | import React from 'react';
3 |
4 | export const MemberContext = React.createContext(null);
5 |
6 | export const useMember = (): MemberWithUser | null => {
7 | return React.useContext(MemberContext);
8 | };
9 |
--------------------------------------------------------------------------------
/packages/icons/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@boluo/typescript-config/react-library.json",
3 | "compilerOptions": {
4 | "target": "ESNext",
5 | "outDir": "./dist/"
6 | },
7 | "include": ["src/**/*.ts", "src/**/*.tsx", "build.ts"],
8 | "exclude": ["dist", "build", "node_modules"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/settings/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@boluo/typescript-config/base.json",
3 | "compilerOptions": {
4 | "outDir": "./dist",
5 | "module": "Preserve",
6 | "moduleResolution": "bundler"
7 | },
8 | "include": ["**/*.ts"],
9 | "exclude": ["dist", "build", "node_modules"]
10 | }
11 |
--------------------------------------------------------------------------------
/packages/types/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@boluo/typescript-config/base.json",
3 | "compilerOptions": {
4 | "outDir": "./dist",
5 | "module": "Preserve",
6 | "moduleResolution": "bundler"
7 | },
8 | "include": ["**/*.ts"],
9 | "exclude": ["dist", "build", "node_modules"]
10 | }
11 |
--------------------------------------------------------------------------------
/apps/legacy/src/information.ts:
--------------------------------------------------------------------------------
1 | import { type ReactNode } from 'react';
2 | import { type Id } from './utils/id';
3 |
4 | export type InformationLevel = 'INFO' | 'SUCCESS' | 'WARNING' | 'ERROR';
5 |
6 | export interface Information {
7 | id: Id;
8 | level: InformationLevel;
9 | content: ReactNode;
10 | }
11 |
--------------------------------------------------------------------------------
/apps/server/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | dotenvy::from_filename(".env.local").ok();
3 | dotenvy::dotenv().ok();
4 |
5 | if let Ok(url) = std::env::var("DATABASE_URL") {
6 | println!("cargo::rustc-env=DATABASE_URL={url}");
7 | println!("cargo:rerun-if-changed=migrations");
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/apps/server/sql/messages/create.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO messages (id, sender_id, channel_id, name, text, entities, in_game, is_action, is_master, whisper_to_users, media_id, pos_p, pos_q, color)
2 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
3 | RETURNING
4 | messages AS "message!: Message";
5 |
6 |
--------------------------------------------------------------------------------
/apps/server/sql/messages/get_by_channel.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | msg AS "message!: Message"
3 | FROM
4 | messages msg
5 | WHERE
6 | msg.channel_id = $1
7 | AND msg.deleted = FALSE
8 | AND ($2::float8 IS NULL
9 | OR msg.pos < $2) -- before
10 | ORDER BY
11 | msg.pos DESC
12 | LIMIT $3;
13 |
14 |
--------------------------------------------------------------------------------
/apps/server/sql/users/edit.sql:
--------------------------------------------------------------------------------
1 | UPDATE
2 | users
3 | SET
4 | nickname = COALESCE($2, nickname),
5 | bio = COALESCE($3, bio),
6 | avatar_id = COALESCE($4, avatar_id),
7 | default_color = COALESCE($5, default_color)
8 | WHERE
9 | id = $1
10 | RETURNING
11 | users AS "users!: User";
12 |
13 |
--------------------------------------------------------------------------------
/apps/site/src/server.ts:
--------------------------------------------------------------------------------
1 | import { type IntlShape } from 'react-intl';
2 |
3 | export interface Params {
4 | lang: string;
5 | theme: string;
6 | }
7 |
8 | export const title = (intl: IntlShape, prefix: string): string => {
9 | return prefix + ' - ' + intl.formatMessage({ defaultMessage: 'Boluo' });
10 | };
11 |
--------------------------------------------------------------------------------
/docs/api/Spaces/Delete Space.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Delete Space
3 | type: http
4 | seq: 11
5 | }
6 |
7 | post {
8 | url: {{base_url}}/spaces/delete?id={{space_id}}
9 | body: json
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | id: {{space_id}}
15 | }
16 |
17 | body:json {
18 | {}
19 | }
20 |
--------------------------------------------------------------------------------
/docs/api/Spaces/Leave Space.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Leave Space
3 | type: http
4 | seq: 14
5 | }
6 |
7 | post {
8 | url: {{base_url}}/spaces/leave?id={{space_id}}
9 | body: json
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | id: {{space_id}}
15 | }
16 |
17 | body:json {
18 | {}
19 | }
20 |
--------------------------------------------------------------------------------
/docs/api/Users/Query Self.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Query Self
3 | type: http
4 | seq: 5
5 | }
6 |
7 | get {
8 | url: {{base_url}}/users/query_self
9 | body: none
10 | auth: inherit
11 | }
12 |
13 | script:post-response {
14 | let data = res.getBody();
15 | bru.setVar("user_id", data.ok.id);
16 | }
17 |
--------------------------------------------------------------------------------
/apps/legacy/src/utils/helper.ts:
--------------------------------------------------------------------------------
1 | export function compare(a: number, b: number): number {
2 | return b - a;
3 | }
4 |
5 | export function compareRev(a: number, b: number): number {
6 | return a - b;
7 | }
8 |
9 | export function parseDateString(dateString: string): Date {
10 | return new Date(dateString);
11 | }
12 |
--------------------------------------------------------------------------------
/apps/legacy/src/utils/path.ts:
--------------------------------------------------------------------------------
1 | import { encodeUuid, type Id } from './id';
2 |
3 | export function chatPath(spaceId: Id, channelId?: Id): string {
4 | if (channelId) {
5 | return `/chat/${encodeUuid(spaceId)}/${encodeUuid(channelId)}`;
6 | } else {
7 | return `/chat/${encodeUuid(spaceId)}`;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/docs/api/Users/Get Me.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Get Me
3 | type: http
4 | seq: 1
5 | }
6 |
7 | get {
8 | url: {{base_url}}/users/get_me
9 | body: none
10 | auth: inherit
11 | }
12 |
13 | script:post-response {
14 | let data = res.getBody();
15 | let token = bru.setVar("user_id", data.ok.user.id);
16 | }
17 |
--------------------------------------------------------------------------------
/docs/api/Users/Resend Email Verification.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Resend Email Verification
3 | type: http
4 | seq: 17
5 | }
6 |
7 | post {
8 | url: {{base_url}}/users/resend_email_verification
9 | body: json
10 | auth: inherit
11 | }
12 |
13 | body:json {
14 | {
15 | "lang": "zh-CN"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/color/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@boluo/typescript-config/base.json",
3 | "compilerOptions": {
4 | "module": "Preserve",
5 | "moduleResolution": "bundler",
6 | "rootDir": "src",
7 | "outDir": "dist"
8 | },
9 | "include": ["src"],
10 | "exclude": ["node_modules", "dist"]
11 | }
12 |
--------------------------------------------------------------------------------
/docs/api/Channels/Join Channel.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Join Channel
3 | type: http
4 | seq: 8
5 | }
6 |
7 | post {
8 | url: {{base_url}}/channels/join
9 | body: json
10 | auth: inherit
11 | }
12 |
13 | body:json {
14 | {
15 | "channelId": "{{channel_id}}",
16 | "characterName": "Link"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/docs/api/Channels/Leave Channel.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Leave Channel
3 | type: http
4 | seq: 9
5 | }
6 |
7 | post {
8 | url: {{base_url}}/channels/leave?id={{channel_id}}
9 | body: json
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | id: {{channel_id}}
15 | }
16 |
17 | body:json {
18 | {}
19 | }
20 |
--------------------------------------------------------------------------------
/docs/api/Messages/Delete Message.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Delete Message
3 | type: http
4 | seq: 5
5 | }
6 |
7 | post {
8 | url: {{base_url}}/messages/delete?id={{message_id}}
9 | body: json
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | id: {{message_id}}
15 | }
16 |
17 | body:json {
18 | {}
19 | }
20 |
--------------------------------------------------------------------------------
/docs/api/Messages/Toggle Fold.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Toggle Fold
3 | type: http
4 | seq: 6
5 | }
6 |
7 | post {
8 | url: {{base_url}}/messages/toggle_fold?id={{message_id}}
9 | body: json
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | id: {{message_id}}
15 | }
16 |
17 | body:json {
18 | {}
19 | }
20 |
--------------------------------------------------------------------------------
/docs/api/Spaces/Refresh Token.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Refresh Token
3 | type: http
4 | seq: 12
5 | }
6 |
7 | post {
8 | url: {{base_url}}/spaces/refresh_token?id={{space_id}}
9 | body: json
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | id: {{space_id}}
15 | }
16 |
17 | body:json {
18 | {}
19 | }
20 |
--------------------------------------------------------------------------------
/docs/api/Users/Confirm Email Change.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Confirm Email Change
3 | type: http
4 | seq: 15
5 | }
6 |
7 | post {
8 | url: {{base_url}}/users/confirm_email_change
9 | body: json
10 | auth: inherit
11 | }
12 |
13 | body:json {
14 | {
15 | "token": "email_change_token_here"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/docs/api/Users/Reset Password.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Reset Password
3 | type: http
4 | seq: 12
5 | }
6 |
7 | post {
8 | url: {{base_url}}/users/reset_password
9 | body: json
10 | auth: inherit
11 | }
12 |
13 | body:json {
14 | {
15 | "email": "user@example.com",
16 | "lang": "zh-CN"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/packages/icons/icons/check.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/docs/api/Channels/Delete Channel.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Delete Channel
3 | type: http
4 | seq: 12
5 | }
6 |
7 | post {
8 | url: {{base_url}}/channels/delete?id={{channel_id}}
9 | body: json
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | id: {{channel_id}}
15 | }
16 |
17 | body:json {
18 | {}
19 | }
20 |
--------------------------------------------------------------------------------
/packages/common/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@boluo/typescript-config/react-library.json",
3 | "compilerOptions": {
4 | "outDir": "./dist",
5 | "moduleResolution": "bundler",
6 | "module": "Preserve"
7 | },
8 | "include": ["**/*.ts", "**/*.tsx"],
9 | "exclude": ["dist", "build", "node_modules"]
10 | }
11 |
--------------------------------------------------------------------------------
/packages/hooks/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@boluo/typescript-config/react-library.json",
3 | "compilerOptions": {
4 | "outDir": "./dist",
5 | "moduleResolution": "bundler",
6 | "module": "Preserve"
7 | },
8 | "include": ["**/*.ts", "**/*.tsx"],
9 | "exclude": ["dist", "build", "node_modules"]
10 | }
11 |
--------------------------------------------------------------------------------
/packages/hooks/useQueryMySpaces.tsx:
--------------------------------------------------------------------------------
1 | import { type ApiError, type SpaceWithMember } from '@boluo/api';
2 | import { type SWRResponse } from 'swr';
3 | import { useGetQuery } from './useGetQuery';
4 |
5 | export const useQueryMySpaces = (): SWRResponse => {
6 | return useGetQuery('/spaces/my', null);
7 | };
8 |
--------------------------------------------------------------------------------
/packages/icons/icons/chevron-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/chevron-up.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/x.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/theme/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@boluo/typescript-config/react-library.json",
3 | "compilerOptions": {
4 | "outDir": "./dist",
5 | "module": "Preserve",
6 | "moduleResolution": "bundler"
7 | },
8 | "include": ["**/*.ts", "**/*.tsx"],
9 | "exclude": ["dist", "build", "node_modules"]
10 | }
11 |
--------------------------------------------------------------------------------
/apps/legacy/src/components/chat/ListItemPlaceholder.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react';
2 | import * as React from 'react';
3 |
4 | const style = css`
5 | width: 100%;
6 | height: 100%;
7 | `;
8 |
9 | function ListItemPlaceholder() {
10 | return ;
11 | }
12 |
13 | export default ListItemPlaceholder;
14 |
--------------------------------------------------------------------------------
/docs/api/Spaces/Join Space.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Join Space
3 | type: http
4 | seq: 13
5 | }
6 |
7 | post {
8 | url: {{base_url}}/spaces/join?spaceId={{space_id}}
9 | body: json
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | spaceId: {{space_id}}
15 | token:
16 | }
17 |
18 | body:json {
19 | {}
20 | }
21 |
--------------------------------------------------------------------------------
/packages/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@boluo/typescript-config/base.json",
3 | "compilerOptions": {
4 | "rootDir": "src",
5 | "outDir": "dist/",
6 | "module": "Preserve",
7 | "moduleResolution": "bundler"
8 | },
9 | "include": ["src/**/*.ts"],
10 | "exclude": ["dist", "build", "node_modules"]
11 | }
12 |
--------------------------------------------------------------------------------
/packages/icons/icons/square.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/sort/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@boluo/typescript-config/base.json",
3 | "compilerOptions": {
4 | "rootDir": "src",
5 | "outDir": "dist",
6 | "module": "Preserve",
7 | "moduleResolution": "bundler"
8 | },
9 | "include": ["src/**/*.ts"],
10 | "exclude": ["dist", "node_modules"]
11 | }
12 |
13 |
--------------------------------------------------------------------------------
/apps/legacy/src/assets/icons/x.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/apps/legacy/src/components/molecules/SpinnerIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Fan from '../../fan.svg';
3 | import { spin } from '../../styles/atoms';
4 | import Icon from '../atoms/Icon';
5 |
6 | function SpinnerIcon() {
7 | return ;
8 | }
9 |
10 | export default React.memo(SpinnerIcon);
11 |
--------------------------------------------------------------------------------
/apps/server/sql/channels/edit_member.sql:
--------------------------------------------------------------------------------
1 | UPDATE
2 | channel_members
3 | SET
4 | character_name = COALESCE($3, character_name),
5 | text_color = COALESCE($4, text_color)
6 | WHERE
7 | user_id = $1
8 | AND channel_id = $2
9 | AND is_joined
10 | RETURNING
11 | channel_members AS "member!: ChannelMember";
12 |
13 |
--------------------------------------------------------------------------------
/apps/spa/components/pane-channel-settings/form.ts:
--------------------------------------------------------------------------------
1 | import { type ChannelType } from '@boluo/api';
2 |
3 | export interface ChannelSettingsForm {
4 | name: string;
5 | topic: string;
6 | defaultDiceType: string;
7 | defaultRollCommand: string;
8 | isSecret: boolean;
9 | type: ChannelType;
10 | isArchived: boolean;
11 | }
12 |
--------------------------------------------------------------------------------
/packages/icons/icons/chevron-left.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/paper-plane.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/interpreter/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@boluo/typescript-config/base.json",
3 | "compilerOptions": {
4 | "rootDir": "src",
5 | "outDir": "dist",
6 | "module": "Preserve",
7 | "moduleResolution": "bundler"
8 | },
9 | "include": ["src/**/*.ts"],
10 | "exclude": ["dist", "node_modules"]
11 | }
12 |
13 |
--------------------------------------------------------------------------------
/packages/locale/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@boluo/typescript-config/base.json",
3 | "compilerOptions": {
4 | "rootDir": "src",
5 | "outDir": "dist/",
6 | "module": "Preserve",
7 | "moduleResolution": "bundler"
8 | },
9 | "include": ["src/**/*.ts"],
10 | "exclude": ["dist", "build", "node_modules"]
11 | }
12 |
--------------------------------------------------------------------------------
/packages/utils/src/function.ts:
--------------------------------------------------------------------------------
1 | export const not = (x: unknown): boolean => !x;
2 | export const toggle = not;
3 |
4 | // identity
5 | export type SelfMapper = (x: T) => T;
6 | export const identity = (x: T): T => x;
7 |
8 | export type EmptyFunction = () => void;
9 | export const empty = () => {
10 | // empty function
11 | };
12 |
--------------------------------------------------------------------------------
/apps/server/sql/users/get.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | users AS "users!: User"
3 | FROM
4 | users
5 | WHERE
6 | CASE WHEN $1 IS NOT NULL THEN
7 | id = $1
8 | WHEN $2 IS NOT NULL THEN
9 | email = $2
10 | WHEN $3 IS NOT NULL THEN
11 | username = $3
12 | END
13 | AND deactivated = FALSE
14 | LIMIT 1;
15 |
16 |
--------------------------------------------------------------------------------
/apps/spa/components/sidebar/SidebarContentLoading.tsx:
--------------------------------------------------------------------------------
1 | import { type FC } from 'react';
2 | import { LoadingText } from '@boluo/ui/LoadingText';
3 |
4 | export const SidebarContentLoading: FC = () => {
5 | return (
6 |
7 |
8 |
9 | );
10 | };
11 |
--------------------------------------------------------------------------------
/docs/api/Spaces/Kick Member.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Kick Member
3 | type: http
4 | seq: 15
5 | }
6 |
7 | post {
8 | url: {{base_url}}/spaces/kick?spaceId={{space_id}}&userId
9 | body: json
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | spaceId: {{space_id}}
15 | userId:
16 | }
17 |
18 | body:json {
19 | {}
20 | }
21 |
--------------------------------------------------------------------------------
/docs/api/Users/Request Email Change.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Request Email Change
3 | type: http
4 | seq: 14
5 | }
6 |
7 | post {
8 | url: {{base_url}}/users/request_email_change
9 | body: json
10 | auth: inherit
11 | }
12 |
13 | body:json {
14 | {
15 | "newEmail": "new@example.com",
16 | "lang": "zh-CN"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/packages/icons/icons/chevron-right.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/cloud.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/utils/src/files.ts:
--------------------------------------------------------------------------------
1 | export const showFileSize = (size: number): string => {
2 | if (size < 1024) return `${size} Byte`;
3 | if (size < 1024 * 1024) return `${Math.round(size / 1024)} KiB`;
4 | if (size < 1024 * 1024 * 1024) return `${Math.round(size / 1024 / 1024)} MiB`;
5 | return `${Math.round(size / 1024 / 1024 / 1024)} GB`;
6 | };
7 |
--------------------------------------------------------------------------------
/apps/server/sql/messages/move_between.sql:
--------------------------------------------------------------------------------
1 | UPDATE
2 | messages
3 | SET
4 | (pos_p,
5 | pos_q) = (
6 | SELECT
7 | p AS pos_p,
8 | q AS pos_q
9 | FROM
10 | find_intermediate ($2, $3, $4, $5))
11 | WHERE
12 | id = $1
13 | RETURNING
14 | messages AS "message!: Message";
15 |
16 |
--------------------------------------------------------------------------------
/apps/server/sql/spaces/get_space_member_list_by_user.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | sm AS "member!: SpaceMember"
3 | FROM
4 | space_members sm
5 | INNER JOIN spaces s ON sm.space_id = s.id
6 | AND s.deleted = FALSE
7 | INNER JOIN users u ON u.id = $1
8 | WHERE
9 | sm.user_id = $1
10 | ORDER BY
11 | s.latest_activity DESC;
12 |
13 |
--------------------------------------------------------------------------------
/packages/icons/icons/moon.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/packages/icons/icons/pause.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/apps/legacy/src/assets/icons/columns.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/apps/legacy/src/components/atoms/News.tsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { p, roundedSm, textSm } from '../../styles/atoms';
3 | import { blue } from '../../styles/colors';
4 |
5 | export const News = styled.div`
6 | ${[roundedSm, p(2), textSm]};
7 | background-color: ${blue['900']};
8 | border: 1px solid ${blue['800']};
9 | `;
10 |
--------------------------------------------------------------------------------
/apps/legacy/src/components/atoms/Portal.tsx:
--------------------------------------------------------------------------------
1 | import React, { type ReactNode } from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | const root = document.getElementById('portal') as HTMLDivElement;
5 |
6 | export const Portal: React.FC<{ children: ReactNode }> = React.memo(({ children }) => {
7 | return ReactDOM.createPortal(children, root);
8 | });
9 |
--------------------------------------------------------------------------------
/apps/server/sql/messages/edit.sql:
--------------------------------------------------------------------------------
1 | UPDATE
2 | messages
3 | SET
4 | name = $2,
5 | text = $3,
6 | entities = $4,
7 | in_game = $5,
8 | is_action = $6,
9 | media_id = $7,
10 | modified = (now() at time zone 'utc'),
11 | color = $8
12 | WHERE
13 | id = $1
14 | RETURNING
15 | messages AS "message!: Message";
16 |
17 |
--------------------------------------------------------------------------------
/docs/api/Channels/Add Member.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Add Member
3 | type: http
4 | seq: 14
5 | }
6 |
7 | post {
8 | url: {{base_url}}/channels/add_member
9 | body: json
10 | auth: inherit
11 | }
12 |
13 | body:json {
14 | {
15 | "channelId": "{{channel_id}}",
16 | "userId": "",
17 | "characterName": "路过的魔法少女"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/docs/api/Channels/Edit Master.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Edit Master
3 | type: http
4 | seq: 15
5 | }
6 |
7 | post {
8 | url: {{base_url}}/channels/edit_master
9 | body: json
10 | auth: inherit
11 | }
12 |
13 | body:json {
14 | {
15 | "channelId": "{{channel_id}}",
16 | "userId": "",
17 | "grantOrRevoke": "GRANT"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/api/src/events.ts:
--------------------------------------------------------------------------------
1 | export type * from '@boluo/types/bindings';
2 | import type { Update } from '@boluo/types/bindings';
3 |
4 | export function isServerUpdate(object: unknown): object is Update {
5 | if (typeof object !== 'object' || object == null) {
6 | return false;
7 | }
8 | return 'mailbox' in object && 'body' in object;
9 | }
10 |
--------------------------------------------------------------------------------
/packages/icons/icons/bold.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/filter.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/apps/interpreter-cli/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@boluo/typescript-config/base.json",
3 | "compilerOptions": {
4 | "rootDir": "src",
5 | "outDir": "dist",
6 | "types": ["node"],
7 | "module": "ESNext",
8 | "moduleResolution": "Bundler"
9 | },
10 | "include": ["src/**/*.ts"],
11 | "exclude": ["dist", "node_modules"]
12 | }
13 |
--------------------------------------------------------------------------------
/apps/site/.vercel/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "projectId": "_",
3 | "orgId": "_",
4 | "settings": {
5 | "framework": "nextjs",
6 | "rootDirectory": null,
7 | "installCommand": "npm install --include-workspace-root",
8 | "outputDirectory": null,
9 | "buildCommand": "npm run --include-workspace-root build:site --if-present"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/apps/spa/state/notification.atoms.ts:
--------------------------------------------------------------------------------
1 | import { atomWithStorage } from 'jotai/utils';
2 |
3 | export const isNotificationSupported = typeof window !== 'undefined' && 'Notification' in window;
4 | export const notificationEnableAtom = atomWithStorage(
5 | 'boluo-notification-v0',
6 | isNotificationSupported && Notification.permission === 'granted',
7 | );
8 |
--------------------------------------------------------------------------------
/docs/api/Users/Reset Password Confirm.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Reset Password Confirm
3 | type: http
4 | seq: 13
5 | }
6 |
7 | post {
8 | url: {{base_url}}/users/reset_password_confirm
9 | body: json
10 | auth: inherit
11 | }
12 |
13 | body:json {
14 | {
15 | "token": "reset_token_here",
16 | "password": "new_password"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/apps/legacy/src/components/atoms/ErrorMessage.tsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { lighten } from 'polished';
3 | import { textSm } from '../../styles/atoms';
4 | import { errorColor } from '../../styles/colors';
5 |
6 | export const ErrorMessage = styled.p`
7 | margin: 0;
8 | ${textSm};
9 | color: ${lighten(0.5, errorColor)};
10 | `;
11 |
--------------------------------------------------------------------------------
/apps/legacy/src/utils/errors.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { showFlash } from '../actions';
3 | import { type AppError, errorText } from '../api/error';
4 | import { type Dispatch } from '../store';
5 |
6 | export const throwErr = (dispatch: Dispatch) => (err: AppError) => {
7 | dispatch(showFlash('ERROR', {errorText(err).description}));
8 | };
9 |
--------------------------------------------------------------------------------
/apps/server/src/events.rs:
--------------------------------------------------------------------------------
1 | mod api;
2 | mod broadcast;
3 | pub mod context;
4 | mod handlers;
5 | pub mod models;
6 | pub mod preview;
7 | mod status;
8 | mod token;
9 | mod types;
10 |
11 | pub use broadcast::{get_broadcast_table, get_mailbox_broadcast_rx};
12 | pub use handlers::router;
13 | pub use status::StatusMap;
14 | pub use types::{Update, startup_id};
15 |
--------------------------------------------------------------------------------
/apps/server/text/email-verification/content.en.html:
--------------------------------------------------------------------------------
1 | Hello!
2 | Thank you for registering with Boluo. Please click the link below to verify your email address:
3 | Verify Email
4 | This link will expire in 24 hours.
5 | If you didn't register for a Boluo account, please ignore this email.
6 |
--------------------------------------------------------------------------------
/apps/spa/components/PaneFooterBox.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from 'react';
2 | import { type ChildrenProps } from '@boluo/types';
3 |
4 | export const PaneFooterBox: FC = ({ children }) => (
5 |
6 | {children}
7 |
8 | );
9 |
--------------------------------------------------------------------------------
/apps/spa/hooks/useComposeError.tsx:
--------------------------------------------------------------------------------
1 | import { useAtomValue } from 'jotai';
2 | import { type ComposeError } from '../state/compose.reducer';
3 | import { useChannelAtoms } from './useChannelAtoms';
4 |
5 | export const useComposeError = (): ComposeError | null => {
6 | const { checkComposeAtom } = useChannelAtoms();
7 | return useAtomValue(checkComposeAtom);
8 | };
9 |
--------------------------------------------------------------------------------
/apps/spa/sentry.client.config.ts:
--------------------------------------------------------------------------------
1 | import * as Sentry from '@sentry/nextjs';
2 | import { SENTRY_CONFIG } from './const';
3 |
4 | if (SENTRY_CONFIG.enabled) {
5 | Sentry.init({
6 | dsn: SENTRY_CONFIG.dsn,
7 | environment: 'development',
8 | tunnel: SENTRY_CONFIG.tunnel,
9 | attachStacktrace: true,
10 | integrations: [],
11 | });
12 | }
13 |
--------------------------------------------------------------------------------
/docs/api/Channels/Edit Member.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Edit Member
3 | type: http
4 | seq: 13
5 | }
6 |
7 | post {
8 | url: {{base_url}}/channels/edit_member
9 | body: json
10 | auth: inherit
11 | }
12 |
13 | body:json {
14 | {
15 | "channelId": "{{channel_id}}",
16 | "characterName": "阿拉拉圾",
17 | "textColor": "#ff0000"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/icons/icons/eye.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/search.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/types/utils.ts:
--------------------------------------------------------------------------------
1 | import type React from 'react';
2 |
3 | export type Empty = Record;
4 |
5 | export interface StyleProps {
6 | className?: string | undefined;
7 | }
8 |
9 | export type DataAttr = { [P in keyof T & string as `data-${P}`]?: T[P] };
10 |
11 | export interface ChildrenProps {
12 | children: React.ReactNode;
13 | }
14 |
--------------------------------------------------------------------------------
/apps/legacy/src/assets/icons/rotate-cw.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/apps/spa/hooks/useScrollerRef.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { createContext, type RefObject } from 'react';
3 |
4 | export const ScrollerRefContext = createContext>({
5 | current: null,
6 | });
7 |
8 | export const useScrollerRef = (): RefObject =>
9 | useContext(ScrollerRefContext);
10 |
--------------------------------------------------------------------------------
/docs/api/Messages/Move Between.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Move Between
3 | type: http
4 | seq: 4
5 | }
6 |
7 | post {
8 | url: {{base_url}}/messages/move_between
9 | body: json
10 | auth: inherit
11 | }
12 |
13 | body:json {
14 | {
15 | "messageId": "{{message_id}}",
16 | "range": [null, null],
17 | "channelId": "{{channel_id}}"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/docs/api/Spaces/Update Settings.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Update Settings
3 | type: http
4 | seq: 16
5 | }
6 |
7 | post {
8 | url: {{base_url}}/spaces/update_settings?id={{space_id}}
9 | body: json
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | id: {{space_id}}
15 | }
16 |
17 | body:json {
18 | {
19 | "nanigasuki": "チョコミント"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/apps/legacy/src/assets/icons/circle.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/apps/spa/components/sidebar/SidebarSkeletonItem.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingText } from '@boluo/ui/LoadingText';
2 | import { SidebarItem } from './SidebarItem';
3 |
4 | const nop = () => {
5 | // no-op
6 | };
7 | export const SidebarSkeletonItem = () => {
8 | return (
9 |
10 |
11 |
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/apps/spa/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import { Html, Head, Main, NextScript } from 'next/document';
2 |
3 | export default function Document() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/docs/api/Spaces/My Spaces.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: My Spaces
3 | type: http
4 | seq: 3
5 | }
6 |
7 | get {
8 | url: {{base_url}}/spaces/my
9 | body: none
10 | auth: inherit
11 | }
12 |
13 | script:post-response {
14 | let data = res.getBody();
15 | if (data.isOk && data.ok.length > 0) {
16 | bru.setVar("space_id", data.ok[0].space.id);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/docs/api/Users/Register.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Register
3 | type: http
4 | seq: 3
5 | }
6 |
7 | post {
8 | url: {{base_url}}/users/register
9 | body: json
10 | auth: inherit
11 | }
12 |
13 | body:json {
14 | {
15 | "email": "bruno@example.com",
16 | "username": "bruno",
17 | "nickname": "Bruno",
18 | "password": "BrunoBruno"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/api-browser/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@boluo/typescript-config/base.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "lib": ["ESNext", "DOM"],
6 | "outDir": "dist/",
7 | "module": "Preserve",
8 | "moduleResolution": "bundler"
9 | },
10 | "include": ["src/**/*.ts"],
11 | "exclude": ["dist", "build", "node_modules"]
12 | }
13 |
--------------------------------------------------------------------------------
/packages/hooks/useMounted.tsx:
--------------------------------------------------------------------------------
1 | import { type RefObject, useEffect, useRef } from 'react';
2 |
3 | export const useMountedRef = (): RefObject => {
4 | const mountedRef = useRef(true);
5 | useEffect(() => {
6 | mountedRef.current = true;
7 | return () => {
8 | mountedRef.current = false;
9 | };
10 | }, []);
11 | return mountedRef;
12 | };
13 |
--------------------------------------------------------------------------------
/packages/icons/icons/plus.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/apps/spa/hooks/useSettings.tsx:
--------------------------------------------------------------------------------
1 | import type { Settings } from '@boluo/settings';
2 | import React from 'react';
3 |
4 | const emptySettings: Settings = {};
5 |
6 | export const SettingsContext = React.createContext(emptySettings);
7 |
8 | export const useSettings = () => {
9 | return React.useContext(SettingsContext) || emptySettings;
10 | };
11 |
--------------------------------------------------------------------------------
/packages/icons/icons/moon-star.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/apps/legacy/src/assets/icons/uncheck.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/apps/legacy/src/components/chat/PrivateChat.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react';
2 | import React from 'react';
3 |
4 | const style = css`
5 | width: 100%;
6 | height: 100%;
7 | display: flex;
8 | justify-content: center;
9 | align-items: center;
10 | `;
11 |
12 | export const PrivateChat = () => {
13 | return 这是私有频道,你没有加入或者被邀请。
;
14 | };
15 |
--------------------------------------------------------------------------------
/packages/api/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface LoginData {
2 | username: string;
3 | password: string;
4 | withToken?: boolean;
5 | }
6 |
7 | export interface SpaceIdWithToken {
8 | spaceId: string;
9 | token?: string;
10 | }
11 | export interface IdQuery {
12 | id: string;
13 | }
14 |
15 | export interface IdWithToken {
16 | id: string;
17 | token?: string;
18 | }
19 |
--------------------------------------------------------------------------------
/packages/icons/icons/archive.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/packages/icons/icons/bell.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/filter-x.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/lock.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/user.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/apps/spa/hooks/useChannelId.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from 'react';
2 |
3 | export const ChannelIdContext = createContext(null);
4 |
5 | export const useChannelId = (): string => {
6 | const id = useContext(ChannelIdContext);
7 | if (!id) {
8 | throw new Error('[Unexpected] Access channel id outside the channel');
9 | }
10 | return id;
11 | };
12 |
--------------------------------------------------------------------------------
/apps/spa/production/wrangler.toml:
--------------------------------------------------------------------------------
1 | name = "boluo-app"
2 | compatibility_date = "2025-10-19"
3 | main = "../../../packages/backend-proxy/dist/worker.js"
4 |
5 | [assets]
6 | directory = "../out/"
7 | html_handling = "auto-trailing-slash"
8 | not_found_handling = "404-page"
9 | run_worker_first = ["/api/*"]
10 |
11 | [vars]
12 | BACKEND_URL = "https://production.boluo.chat"
13 |
--------------------------------------------------------------------------------
/apps/spa/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@boluo/typescript-config/nextjs.json",
3 | "compilerOptions": {
4 | "plugins": [{ "name": "next" }]
5 | },
6 | "include": [
7 | "next-env.d.ts",
8 | "additional.d.ts",
9 | "next.config.ts",
10 | "**/*.ts",
11 | "**/*.tsx",
12 | ".next/types/**/*.ts"
13 | ],
14 | "exclude": ["node_modules", "out"]
15 | }
16 |
--------------------------------------------------------------------------------
/apps/spa/wrangler.toml:
--------------------------------------------------------------------------------
1 | name = "boluo-app-staging"
2 | compatibility_date = "2025-10-19"
3 | main = "../../packages/backend-proxy/dist/worker.js"
4 |
5 | [assets]
6 | directory = "./out/"
7 | html_handling = "auto-trailing-slash"
8 | not_found_handling = "404-page"
9 | run_worker_first = ["/api/*"]
10 |
11 | [vars]
12 | BACKEND_URL = "https://boluo-server-staging.fly.dev"
13 |
--------------------------------------------------------------------------------
/docs/api/Media/Presigned.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Presigned
3 | type: http
4 | seq: 1
5 | }
6 |
7 | post {
8 | url: {{base_url}}/media/presigned?filename=malice.png&mimeType=image/png&size=1024
9 | body: json
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | filename: malice.png
15 | mimeType: image/png
16 | size: 1024
17 | }
18 |
19 | body:json {
20 | {}
21 | }
22 |
--------------------------------------------------------------------------------
/packages/icons/icons/chevrons-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/apps/legacy/src/assets/icons/x-circle.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/apps/legacy/src/components/pages/NotFound.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { useTitle } from '../../hooks/useTitle';
3 | import Title from '../atoms/Title';
4 |
5 | function NotFound() {
6 | useTitle('页面没有找到');
7 | return (
8 | <>
9 | 没有找到
10 | 所请求的资源不存在、已删除或者你没有权限访问。
11 | >
12 | );
13 | }
14 |
15 | export default NotFound;
16 |
--------------------------------------------------------------------------------
/apps/storybook/src/Spinner.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react-vite';
2 |
3 | import { Spinner } from '@boluo/ui/Spinner';
4 |
5 | const meta: Meta = {
6 | title: 'Feedback/Spinner',
7 | component: Spinner,
8 | };
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const Basic: Story = { args: {} };
14 |
--------------------------------------------------------------------------------
/packages/icons/icons/history.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/typescript-config/nextjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "extends": "./base.json",
4 | "compilerOptions": {
5 | "plugins": [{ "name": "next" }],
6 | "module": "ESNext",
7 | "moduleResolution": "Bundler",
8 | "allowJs": true,
9 | "jsx": "preserve",
10 | "noEmit": true,
11 | "resolveJsonModule": true
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/ui/src/BackToHomepage.tsx:
--------------------------------------------------------------------------------
1 | import { FormattedMessage } from 'react-intl';
2 | import * as classes from './classes';
3 |
4 | export const BackToHomepage = ({ url }: { url: string }) => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/packages/ui/src/entities/EntityText.tsx:
--------------------------------------------------------------------------------
1 | import { type EntityOf } from '@boluo/api';
2 | import type { FC } from 'react';
3 |
4 | interface Props {
5 | source: string;
6 | entity: EntityOf<'Text'>;
7 | }
8 |
9 | export const EntityText: FC = ({ source, entity: { start, len } }) => {
10 | return {source.substring(start, start + len)};
11 | };
12 |
--------------------------------------------------------------------------------
/packages/ui/src/users/UserCardError.tsx:
--------------------------------------------------------------------------------
1 | import { FormattedMessage } from 'react-intl';
2 | import { FloatingBox } from '../FloatingBox';
3 |
4 | export const UserCardError = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/packages/ui/src/users/UserCardLoading.tsx:
--------------------------------------------------------------------------------
1 | import { FormattedMessage } from 'react-intl';
2 | import { FloatingBox } from '../FloatingBox';
3 |
4 | export const UserCardLoading = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/apps/legacy/src/assets/icons/lock.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/apps/legacy/src/components/atoms/HelpText.tsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { block, spacingN, textSm } from '../../styles/atoms';
3 | import { minorTextColor } from '../../styles/colors';
4 |
5 | export const HelpText = styled.small`
6 | ${[textSm, block]};
7 | margin: 0;
8 | padding: ${spacingN(1)} 0;
9 | line-height: 1.5em;
10 | color: ${minorTextColor};
11 | `;
12 |
--------------------------------------------------------------------------------
/packages/icons/icons/sidebar.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/apps/legacy/src/assets/icons/dot-circle.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/apps/legacy/src/states/connection.ts:
--------------------------------------------------------------------------------
1 | import { atom } from 'jotai';
2 | import { atomWithStorage } from 'jotai/utils';
3 | import { isCrossOrigin } from '../settings';
4 |
5 | export type ConnectState = 'CONNECTING' | 'OPEN' | 'CLOSED';
6 | export const connectionStateAtom = atom('CLOSED');
7 |
8 | export const autoSelectAtom = atomWithStorage('BOLUO_AUTO_SELECT_PROXY', !isCrossOrigin);
9 |
--------------------------------------------------------------------------------
/apps/server/sql/spaces/get.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | s AS "space!: Space",
3 | u AS "user!: User"
4 | FROM
5 | spaces s
6 | LEFT JOIN users u ON CASE WHEN $3 THEN
7 | s.owner_id = u.id
8 | END
9 | WHERE
10 | CASE WHEN $1 IS NOT NULL THEN
11 | s.id = $1
12 | WHEN $2 IS NOT NULL THEN
13 | s.name = $2
14 | END
15 | AND deleted = FALSE
16 | LIMIT 1;
17 |
18 |
--------------------------------------------------------------------------------
/apps/spa/hooks/useReadObserve.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { recordWarn } from '../error';
3 |
4 | export const ReadObserverContext = React.createContext<(node: Element) => () => void>(() => {
5 | recordWarn('ReadObserverContext is not provided');
6 | return () => {};
7 | });
8 |
9 | export const useReadObserve = () => {
10 | return React.useContext(ReadObserverContext);
11 | };
12 |
--------------------------------------------------------------------------------
/apps/storybook/src/LoadFailed.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react-vite';
2 | import { LoadFailed } from '@boluo/ui/LoadFailed';
3 |
4 | const meta: Meta = { title: 'Feedback/LoadFailed', component: LoadFailed };
5 |
6 | export default meta;
7 | type Story = StoryObj;
8 |
9 | export const Default: Story = {
10 | args: {},
11 | };
12 |
--------------------------------------------------------------------------------
/docker-compose.override.example.yml:
--------------------------------------------------------------------------------
1 | # Example override file for development environment
2 | # Copy this file to docker-compose.override.yml and edit it to your needs
3 |
4 | services:
5 | db:
6 | ports:
7 | - 127.0.0.1:5432:5432
8 | redis:
9 | ports:
10 | - 127.0.0.1:6379:6379
11 | storage:
12 | ports:
13 | - 127.0.0.1:9000:9000
14 | - 127.0.0.1:9090:9090
15 |
--------------------------------------------------------------------------------
/docs/api/Channels/Kick Member.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Kick Member
3 | type: http
4 | seq: 10
5 | }
6 |
7 | post {
8 | url: {{base_url}}/channels/kick?spaceId={{space_id}}&channelId={{channel_id}}&userId
9 | body: json
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | spaceId: {{space_id}}
15 | channelId: {{channel_id}}
16 | userId:
17 | }
18 |
19 | body:json {
20 | {}
21 | }
22 |
--------------------------------------------------------------------------------
/packages/icons/icons/panel-left-open.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/interpreter/src/to-parsed.ts:
--------------------------------------------------------------------------------
1 | import { type ParseResult, emptyParseResult } from './parse-result';
2 | import { type Entities } from '@boluo/api';
3 |
4 | export const messageToParsed = (text: string, entities: Entities): ParseResult => {
5 | if (!Array.isArray(entities) || text == null) {
6 | return emptyParseResult;
7 | }
8 | return { ...emptyParseResult, text, entities };
9 | };
10 |
--------------------------------------------------------------------------------
/packages/theme/react.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { type Theme } from '@boluo/types';
3 | import { getThemeFromDom, observeTheme } from './index';
4 |
5 | export const useTheme = (): Theme => {
6 | const [theme, setTheme] = useState(getThemeFromDom());
7 |
8 | useEffect(() => {
9 | return observeTheme(setTheme);
10 | }, []);
11 |
12 | return theme;
13 | };
14 |
--------------------------------------------------------------------------------
/apps/legacy/src/components/atoms/Text.tsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { spacingN, textBase } from '../../styles/atoms';
3 | import { textColor } from '../../styles/colors';
4 |
5 | export const Text = styled.p`
6 | color: ${textColor};
7 | font-size: ${textBase};
8 | margin: 0;
9 | line-height: 1.75rem;
10 | padding: ${spacingN(1)} 0;
11 | `;
12 |
13 | export default Text;
14 |
--------------------------------------------------------------------------------
/apps/spa/components/ChatNotFound.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import { PaneList } from './PaneList';
3 |
4 | export const ChatNotFound = () => {
5 | const defaultPane = useMemo(() => {
6 | return (
7 | Not found
8 | );
9 | }, []);
10 | return ;
11 | };
12 |
--------------------------------------------------------------------------------
/docs/api/Users/Edit User.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Edit User
3 | type: http
4 | seq: 10
5 | }
6 |
7 | post {
8 | url: {{base_url}}/users/edit
9 | body: json
10 | auth: inherit
11 | }
12 |
13 | body:json {
14 | {
15 | "nickname": "石破天惊lovelove",
16 | "bio": "God is in his heaven, all is right with the world",
17 | "avatar": null,
18 | "defaultColor": "preset:blue"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/hooks/useWebSocketUrl.tsx:
--------------------------------------------------------------------------------
1 | import { apiUrlAtom } from '@boluo/api-browser';
2 | import { type Atom, atom, useAtomValue } from 'jotai';
3 |
4 | export const webSocketUrlAtom: Atom = atom((get) => {
5 | const baseUrl = get(apiUrlAtom);
6 | return baseUrl.replace(/^http/, 'ws');
7 | });
8 |
9 | export const useWebSocketUrl = () => {
10 | return useAtomValue(webSocketUrlAtom);
11 | };
12 |
--------------------------------------------------------------------------------
/packages/icons/icons/close.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/corner-down-right.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/ellipsis-vertical.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/menu.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/more-vertical.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/panel-left-close.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/ui/src/entities/EntityExpr.tsx:
--------------------------------------------------------------------------------
1 | import { type FC } from 'react';
2 | import { type ExprEntity } from '@boluo/api';
3 | import { EntityExprNode } from './EntityExprNode';
4 |
5 | interface Props {
6 | source: string;
7 | entity: ExprEntity;
8 | level?: number;
9 | }
10 |
11 | export const EntityExpr: FC = ({ entity }) => {
12 | return ;
13 | };
14 |
--------------------------------------------------------------------------------
/packages/utils/src/env.ts:
--------------------------------------------------------------------------------
1 | export const parseBool = (env: string | null | undefined): boolean => {
2 | env = (env ?? 'false').trim().toLowerCase();
3 | if (env === 'true' || env === '1' || env === 'on') {
4 | return true;
5 | } else if (env === 'false' || env === '0' || env === 'off') {
6 | return false;
7 | } else {
8 | throw new Error(`Invalid boolean value: ${env}`);
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/apps/legacy/src/assets/icons/ellipsis.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/apps/storybook/src/LoadingText.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react-vite';
2 | import { LoadingText } from '@boluo/ui/LoadingText';
3 |
4 | const meta: Meta = {
5 | title: 'Feedback/LoadingText',
6 | component: LoadingText,
7 | };
8 |
9 | export default meta;
10 | type Story = StoryObj;
11 |
12 | export const Basic: Story = { args: {} };
13 |
--------------------------------------------------------------------------------
/apps/storybook/src/OopsKaomoji.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react-vite';
2 | import { OopsKaomoji } from '@boluo/ui/OopsKaomoji';
3 |
4 | const meta: Meta = {
5 | title: 'Feedback/OopsKaomoji',
6 | component: OopsKaomoji,
7 | };
8 |
9 | export default meta;
10 | type Story = StoryObj;
11 |
12 | export const Basic: Story = { args: {} };
13 |
--------------------------------------------------------------------------------
/packages/icons/icons/key.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/move-vertical.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/paperclip.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/apps/storybook/src/HelpText.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react-vite';
2 | import { HelpText } from '@boluo/ui/HelpText';
3 |
4 | const meta: Meta = {
5 | title: 'Base/HelpText',
6 | component: HelpText,
7 | };
8 |
9 | export default meta;
10 | type Story = StoryObj;
11 |
12 | export const Basic: Story = { args: { children: 'This is a help text' } };
13 |
--------------------------------------------------------------------------------
/packages/icons/icons/alert-circle.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/link.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/user-x.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/icons/icons/x-circle.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/tailwind-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@boluo/tailwind-config",
3 | "version": "0.0.0",
4 | "devDependencies": {
5 | "@boluo/typescript-config": "0.0.0",
6 | "@tailwindcss/postcss": "^4.1.14",
7 | "tailwindcss": "^4.1.14"
8 | },
9 | "exports": {
10 | ".": "./tailwind.css",
11 | "./postcss": "./postcss.config.js"
12 | },
13 | "private": true,
14 | "type": "module"
15 | }
16 |
--------------------------------------------------------------------------------
/packages/utils/src/random.ts:
--------------------------------------------------------------------------------
1 | export const shuffle = (array: Array): void => {
2 | for (let i = array.length - 1; i > 0; i--) {
3 | const j = Math.floor(Math.random() * (i + 1));
4 | const temp = array[i];
5 | array[i] = array[j]!;
6 | array[j] = temp!;
7 | }
8 | };
9 | export const selectRandom = (array: Array): T => {
10 | return array[Math.floor(Math.random() * array.length)]!;
11 | };
12 |
--------------------------------------------------------------------------------
/apps/legacy/src/assets/icons/filter.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/apps/server/sql/channels/get_channels_by_user.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | c AS "channel!: Channel",
3 | cm AS "member!: ChannelMember"
4 | FROM
5 | channel_members cm
6 | INNER JOIN channels c ON cm.channel_id = c.id
7 | AND c.deleted = FALSE
8 | INNER JOIN space_members sm ON cm.user_id = sm.user_id
9 | AND c.space_id = sm.space_id
10 | WHERE
11 | cm.user_id = $1
12 | AND cm.is_joined;
13 |
14 |
--------------------------------------------------------------------------------
/apps/spa/hooks/useVirtuosoRef.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { createContext, type RefObject } from 'react';
3 | import type { VirtuosoHandle } from 'react-virtuoso';
4 |
5 | export const VirtuosoRefContext = createContext>({
6 | current: null,
7 | });
8 |
9 | export const useVirtuosoRef = (): RefObject =>
10 | useContext(VirtuosoRefContext);
11 |
--------------------------------------------------------------------------------
/packages/icons/icons/arrow-up-wide-short.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/save.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/apps/storybook/src/entities/EntityUnknown.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react-vite';
2 | import { EntityUnknown } from '@boluo/ui/entities/EntityUnknown';
3 |
4 | const meta: Meta = {
5 | title: 'Entities/Unknown',
6 | component: EntityUnknown,
7 | };
8 |
9 | export default meta;
10 | type Story = StoryObj;
11 |
12 | export const Basic: Story = {};
13 |
--------------------------------------------------------------------------------
/packages/icons/icons/arrow-down-wide-short.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/triangle-alert.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/ui/src/entities/EntityEvaluatedExpr.tsx:
--------------------------------------------------------------------------------
1 | import { type FC } from 'react';
2 | import { EntityExprNode } from './EntityExprNode';
3 | import { type EvaluatedExpr } from '@boluo/api';
4 |
5 | interface Props {
6 | source: string;
7 | entity: EvaluatedExpr;
8 | level?: number;
9 | }
10 |
11 | export const EntityEvaluatedExpr: FC = ({ entity }) => {
12 | return ;
13 | };
14 |
--------------------------------------------------------------------------------
/apps/server/sql/channels/get_channel_member.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | cm AS "member!: ChannelMember"
3 | FROM
4 | channel_members cm
5 | INNER JOIN channels ch ON cm.channel_id = ch.id
6 | AND ch.deleted = FALSE
7 | INNER JOIN space_members sm ON ch.space_id = sm.space_id
8 | AND cm.user_id = sm.user_id
9 | WHERE
10 | cm.user_id = $1
11 | AND cm.channel_id = $2
12 | AND cm.is_joined
13 | LIMIT 1;
14 |
15 |
--------------------------------------------------------------------------------
/apps/server/sql/channels/get_member_by_channel.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | cm AS "channel!: ChannelMember",
3 | sm AS "space!: SpaceMember"
4 | FROM
5 | channel_members cm
6 | INNER JOIN channels ch ON cm.channel_id = ch.id
7 | AND ch.deleted = FALSE
8 | INNER JOIN space_members sm ON sm.space_id = ch.space_id
9 | AND sm.user_id = cm.user_id
10 | WHERE
11 | cm.channel_id = $1
12 | AND cm.is_joined;
13 |
14 |
--------------------------------------------------------------------------------
/apps/server/sql/spaces/edit.sql:
--------------------------------------------------------------------------------
1 | UPDATE
2 | spaces
3 | SET
4 | name = COALESCE($2, name),
5 | description = COALESCE($3, description),
6 | default_dice_type = COALESCE($4, default_dice_type),
7 | explorable = COALESCE($5, explorable),
8 | is_public = COALESCE($6, is_public),
9 | allow_spectator = COALESCE($7, allow_spectator)
10 | WHERE
11 | id = $1
12 | RETURNING
13 | spaces AS "space!: Space";
14 |
15 |
--------------------------------------------------------------------------------
/packages/icons/icons/clipboard.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/cloud-off.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/edit.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/satellite-dish.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/upload.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/locale/src/server.ts:
--------------------------------------------------------------------------------
1 | import { createIntl, type IntlShape } from '@formatjs/intl';
2 | import { onIntlError, toLocale } from '.';
3 | import { loadMessages } from './dynamic';
4 |
5 | export const getIntl = async ({ lang }: { lang: string }): Promise => {
6 | const locale = toLocale(lang);
7 | const messages = await loadMessages(locale);
8 | return createIntl({ locale, messages, onError: onIntlError });
9 | };
10 |
--------------------------------------------------------------------------------
/packages/ui/src/chat/IsActionIndicator.tsx:
--------------------------------------------------------------------------------
1 | import { PersonRunning } from '@boluo/icons';
2 | import { Delay } from '../Delay';
3 |
4 | export const IsActionIndicator = () => {
5 | return (
6 |
7 |
8 |
9 |
10 | Action
11 |
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/.sqlx/query-ff1f5b18915188e185f0deef5242e09653b3b2bc8623422e549b529b7887cee3.json:
--------------------------------------------------------------------------------
1 | {
2 | "db_name": "PostgreSQL",
3 | "query": "UPDATE user_sessions\nSET active = FALSE\nWHERE id = $1;\n",
4 | "describe": {
5 | "columns": [],
6 | "parameters": {
7 | "Left": [
8 | "Uuid"
9 | ]
10 | },
11 | "nullable": []
12 | },
13 | "hash": "ff1f5b18915188e185f0deef5242e09653b3b2bc8623422e549b529b7887cee3"
14 | }
15 |
--------------------------------------------------------------------------------
/packages/icons/icons/a-arrow-up.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/log-in.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/log-out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/scaling.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/apps/legacy/src/assets/icons/paper-plane.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/apps/legacy/src/components/chat/compose/BroadcastAreClosed.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react';
2 | import React from 'react';
3 |
4 | interface Props {
5 | className?: string;
6 | }
7 |
8 | const style = css`
9 | font-style: italic;
10 | `;
11 |
12 | export const BroadcastAreClosed = ({ className }: Props) => {
13 | return (
14 |
15 | [预览广播已关闭]
16 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/apps/server/text/email-change/content.en.html:
--------------------------------------------------------------------------------
1 | Hello!
2 | You have requested to change your email address for your Boluo account. Please click the link below to confirm this change:
3 | Confirm Email Change
4 | This link will expire in 24 hours.
5 | If you didn't request this email change, please ignore this email and your current email address will remain unchanged.
6 |
--------------------------------------------------------------------------------
/apps/site/src/app/[lang]/[theme]/account/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from 'react';
2 | import { BackLink } from '../../../../components/BackLink';
3 |
4 | export default function Layout({ children }: { children: ReactNode }) {
5 | return (
6 |
7 |
8 | {children}
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/apps/spa/state/view.utils.ts:
--------------------------------------------------------------------------------
1 | import { type Pane, type PaneData } from './view.types';
2 |
3 | export const findPane = (panes: Pane[], predicate: (pane: PaneData) => boolean) => {
4 | for (const pane of panes) {
5 | if (predicate(pane)) {
6 | return pane;
7 | }
8 | const childPane = pane.child?.pane;
9 | if (childPane && predicate(childPane)) {
10 | return childPane;
11 | }
12 | }
13 | return null;
14 | };
15 |
--------------------------------------------------------------------------------
/packages/icons/icons/alert-triangle.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/ui/src/ErrorMessageBox.tsx:
--------------------------------------------------------------------------------
1 | import { type ReactNode } from 'react';
2 |
3 | interface Props {
4 | children: ReactNode;
5 | }
6 |
7 | export const ErrorMessageBox = ({ children }: Props) => {
8 | return (
9 |
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/.sqlx/query-d3323ec6958d941643ea3121a8e35ab44420baf50708e62a3f920e807ec07c1a.json:
--------------------------------------------------------------------------------
1 | {
2 | "db_name": "PostgreSQL",
3 | "query": "UPDATE\n spaces\nSET\n deleted = TRUE\nWHERE\n id = $1;\n\n",
4 | "describe": {
5 | "columns": [],
6 | "parameters": {
7 | "Left": [
8 | "Uuid"
9 | ]
10 | },
11 | "nullable": []
12 | },
13 | "hash": "d3323ec6958d941643ea3121a8e35ab44420baf50708e62a3f920e807ec07c1a"
14 | }
15 |
--------------------------------------------------------------------------------
/apps/interpreter-cli/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "interpreter-cli",
3 | "version": "0.0.0",
4 | "dependencies": {
5 | "@boluo/interpreter": "0.0.0"
6 | },
7 | "devDependencies": {
8 | "@types/node": "^24.0.12",
9 | "tsx": "^4.21.0",
10 | "typescript": "^5.9.3"
11 | },
12 | "private": true,
13 | "scripts": {
14 | "check": "tsc --noEmit",
15 | "start": "tsx src/index.ts"
16 | },
17 | "type": "module"
18 | }
19 |
--------------------------------------------------------------------------------
/packages/icons/icons/help-circle.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/shared-types/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "shared-types"
3 | version = "0.1.0"
4 | edition = "2024"
5 |
6 | [lib]
7 | name = "shared_types"
8 |
9 | [dependencies]
10 | serde = { version = "1", features = ["derive"] }
11 | serde_json = "1"
12 | specta = { version = "2.0.0-rc.22", features = [
13 | "chrono",
14 | "derive",
15 | "export",
16 | "serde",
17 | "serde_json",
18 | "uuid",
19 | ] }
20 | specta-typescript = "0.0.9"
21 |
--------------------------------------------------------------------------------
/packages/ui/src/entities/EntityStrong.tsx:
--------------------------------------------------------------------------------
1 | import { type EntityOf } from '@boluo/api';
2 | import type { FC } from 'react';
3 |
4 | interface Props {
5 | source: string;
6 | entity: EntityOf<'Strong'>;
7 | }
8 |
9 | export const EntityStrong: FC = ({
10 | source,
11 | entity: {
12 | child: { start, len },
13 | },
14 | }) => {
15 | return {source.substring(start, start + len)};
16 | };
17 |
--------------------------------------------------------------------------------
/.sqlx/query-0e33a532c76682fe608925c12632c97a3b7362910f02bba15b778f900d650cc0.json:
--------------------------------------------------------------------------------
1 | {
2 | "db_name": "PostgreSQL",
3 | "query": "UPDATE\n messages\nSET\n deleted = TRUE\nWHERE\n id = $1;\n\n",
4 | "describe": {
5 | "columns": [],
6 | "parameters": {
7 | "Left": [
8 | "Uuid"
9 | ]
10 | },
11 | "nullable": []
12 | },
13 | "hash": "0e33a532c76682fe608925c12632c97a3b7362910f02bba15b778f900d650cc0"
14 | }
15 |
--------------------------------------------------------------------------------
/.sqlx/query-d30e7b47ad997e47163b89e21fcf7a5ae0a159fb74f492c72d5fbdfa27e344d6.json:
--------------------------------------------------------------------------------
1 | {
2 | "db_name": "PostgreSQL",
3 | "query": "UPDATE\n users\nSET\n deactivated = TRUE\nWHERE\n id = $1;\n\n",
4 | "describe": {
5 | "columns": [],
6 | "parameters": {
7 | "Left": [
8 | "Uuid"
9 | ]
10 | },
11 | "nullable": []
12 | },
13 | "hash": "d30e7b47ad997e47163b89e21fcf7a5ae0a159fb74f492c72d5fbdfa27e344d6"
14 | }
15 |
--------------------------------------------------------------------------------
/docs/api/Channels/By Space.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: By Space
3 | type: http
4 | seq: 2
5 | }
6 |
7 | get {
8 | url: {{base_url}}/channels/by_space?id={{space_id}}
9 | body: none
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | id: {{space_id}}
15 | }
16 |
17 | script:post-response {
18 | let data = res.getBody();
19 | if (data.isOk && data.ok.length > 0) {
20 | bru.setVar("channel_id", data.ok[0].channel.id);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/packages/icons/icons/external-link.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/shrink.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/tool.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/ui/src/entities/RollBox.tsx:
--------------------------------------------------------------------------------
1 | import { type FC, type ReactNode } from 'react';
2 |
3 | interface Props {
4 | children: ReactNode;
5 | }
6 |
7 | export const RollBox: FC = ({ children }) => {
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/.env.local.example:
--------------------------------------------------------------------------------
1 | DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:5432/boluo?sslmode=disable
2 | REDIS_URL=redis://127.0.0.1:6379
3 | BACKEND_URL=http://127.0.0.1:3033
4 | APP_URL=http://127.0.0.1:3200
5 | SITE_URL=http://127.0.0.1:3100
6 | PUBLIC_MEDIA_URL=http://127.0.0.1:9000/boluo
7 |
8 | SECRET=SOME_SECRET
9 | S3_ACCESS_KEY_ID=boluo
10 | S3_SECRET_ACCESS_KEY=boluo-development
11 | S3_ENDPOINT_URL=http://127.0.0.1:9000
12 | S3_BUCKET_NAME=boluo
13 |
--------------------------------------------------------------------------------
/apps/spa/hooks/useChannel.tsx:
--------------------------------------------------------------------------------
1 | import { type Channel } from '@boluo/api';
2 | import React from 'react';
3 | import { recordWarn } from '../error';
4 |
5 | export const ChannelContext = React.createContext(null);
6 |
7 | export const useChannel = () => {
8 | const channel = React.useContext(ChannelContext);
9 | if (!channel) {
10 | recordWarn('useChannel must be used within a channel context');
11 | }
12 | return channel;
13 | };
14 |
--------------------------------------------------------------------------------
/apps/storybook/src/Kbd.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react-vite';
2 | import { Kbd } from '@boluo/ui/Kbd';
3 |
4 | const meta: Meta = {
5 | title: 'Base/Kbd',
6 | component: Kbd,
7 | };
8 |
9 | export default meta;
10 | type Story = StoryObj;
11 |
12 | export const Basic: Story = { args: { children: 'Ctrl' } };
13 |
14 | export const Small: Story = { args: { children: 'Ctrl', variant: 'small' } };
15 |
--------------------------------------------------------------------------------
/apps/storybook/src/SomethingWentWrong.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react-vite';
2 | import { SomethingWentWrong } from '@boluo/ui/SomethingWentWrong';
3 |
4 | const meta: Meta = {
5 | title: 'Feedback/SomethingWentWrong',
6 | component: SomethingWentWrong,
7 | };
8 |
9 | export default meta;
10 | type Story = StoryObj;
11 |
12 | export const Basic: Story = { args: {} };
13 |
--------------------------------------------------------------------------------
/apps/storybook/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 | import tailwindcss from '@tailwindcss/vite';
4 |
5 | export default defineConfig({
6 | plugins: [
7 | tailwindcss(),
8 | react({
9 | babel: {
10 | plugins: [
11 | ['formatjs', { idInterpolationPattern: '[sha512:contenthash:base64:6]', ast: true }],
12 | ],
13 | },
14 | }),
15 | ],
16 | });
17 |
--------------------------------------------------------------------------------
/docs/api/Users/Login.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: Login
3 | type: http
4 | seq: 2
5 | }
6 |
7 | post {
8 | url: {{base_url}}/users/login
9 | body: json
10 | auth: inherit
11 | }
12 |
13 | body:json {
14 | {
15 | "username": "bruno",
16 | "password": "BrunoBruno",
17 | "withToken": true
18 | }
19 | }
20 |
21 | script:post-response {
22 | let data = res.getBody();
23 | let token = bru.setEnvVar("access_token", data.ok.token);
24 | }
25 |
--------------------------------------------------------------------------------
/packages/icons/icons/hash.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/split-horizontal.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/utils/src/flags.ts:
--------------------------------------------------------------------------------
1 | import { parseBool } from './env.js';
2 |
3 | const getBoolFlag = (key: string, defaultValue = false): boolean => {
4 | if (globalThis.window?.localStorage) {
5 | const storage = globalThis.window.localStorage;
6 | const value = storage.getItem(key.toLowerCase());
7 | return parseBool(value);
8 | } else {
9 | return defaultValue;
10 | }
11 | };
12 |
13 | export const IS_DEBUG = getBoolFlag('BOLUO_DEBUG');
14 |
--------------------------------------------------------------------------------
/apps/legacy/src/components/atoms/Delay.tsx:
--------------------------------------------------------------------------------
1 | import { type MeasureResult } from '../../hooks/useBaseUrlDelay';
2 |
3 | export const Delay = ({ delay }: { delay: MeasureResult | 'LOADING' }) => {
4 | if (delay === 'LOADING') {
5 | return ...;
6 | } else if (delay === 'ERROR') {
7 | return 出错;
8 | } else if (delay === 'TIMEOUT') {
9 | return 超时;
10 | }
11 | return {delay.toFixed(2)}ms;
12 | };
13 |
--------------------------------------------------------------------------------
/apps/server/src/events/api.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 | use uuid::Uuid;
3 |
4 | #[derive(Deserialize, Serialize, specta::Type)]
5 | pub struct Token {
6 | pub token: Uuid,
7 | }
8 |
9 | #[derive(Deserialize, Serialize, Default, specta::Type)]
10 | #[serde(rename_all = "camelCase")]
11 | pub struct MakeToken {
12 | #[serde(default)]
13 | pub space_id: Option,
14 | #[serde(default)]
15 | pub user_id: Option,
16 | }
17 |
--------------------------------------------------------------------------------
/packages/icons/icons/globe.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/apps/db/README.md:
--------------------------------------------------------------------------------
1 | # Database Configuration
2 |
3 | ## Schema
4 |
5 | ```
6 | pg_dump --schema-only -U postgres --no-owner --no-privileges boluo > db/schema.sql
7 | ```
8 |
9 | ## Dump
10 |
11 | ```
12 | pg_dump --username ... --host ... --dbname boluo --no-owner --no-privileges --schema public --file boluo-$(date +%Y%m%d).dump
13 | ```
14 |
15 | ## Restore
16 |
17 | ```
18 | psql --username ... --host ... --dbname boluo --file boluo-$(date +%Y%m%d).dump
19 | ```
20 |
--------------------------------------------------------------------------------
/apps/storybook/src/Loading.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react-vite';
2 | import { Loading } from '@boluo/ui/Loading';
3 |
4 | const meta: Meta = {
5 | title: 'Feedback/Loading',
6 | component: Loading,
7 | };
8 |
9 | export default meta;
10 | type Story = StoryObj;
11 |
12 | export const Inline: Story = { args: { type: 'inline' } };
13 |
14 | export const Block: Story = { args: { type: 'block' } };
15 |
--------------------------------------------------------------------------------
/packages/hooks/useDetectBrowserSupport.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | export const useDetectBrowserSupport = () => {
4 | const [isSupported, setIsSupported] = useState(true);
5 | useEffect(() => {
6 | // setIsSupported(false);
7 | setIsSupported(
8 | CSS.supports('container-type', 'inline-size') &&
9 | CSS.supports('grid-template-rows', 'subgrid'),
10 | );
11 | }, []);
12 | return isSupported;
13 | };
14 |
--------------------------------------------------------------------------------
/packages/icons/icons/refresh.svg:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/packages/icons/icons/user-plus.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/apps/legacy/src/assets/icons/plus-circle.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/apps/server/sql/messages/get.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | msg AS "message!: Message",
3 | (msg.whisper_to_users IS NOT NULL
4 | AND cm.is_master IS NOT TRUE
5 | AND ($2 IS NULL
6 | OR $2 <> ALL (msg.whisper_to_users))) AS "should_hide!"
7 | FROM
8 | messages msg
9 | LEFT JOIN channel_members cm ON cm.channel_id = msg.channel_id
10 | AND cm.user_id = $2
11 | WHERE
12 | msg.id = $1
13 | AND msg.deleted = FALSE
14 | LIMIT 1;
15 |
16 |
--------------------------------------------------------------------------------
/apps/storybook/src/DiceSelect.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react-vite';
2 | import { DiceSelect } from '@boluo/ui/DiceSelect';
3 | import { fn } from 'storybook/test';
4 |
5 | const meta: Meta = {
6 | title: 'Derived/DiceSelect',
7 | component: DiceSelect,
8 | };
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const Basic: Story = { args: { value: 'd20', onChange: fn() } };
14 |
--------------------------------------------------------------------------------
/apps/storybook/src/entities/EntityExprUnknown.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react-vite';
2 | import { EntityExprNodeUnknown } from '@boluo/ui/entities/EntityExprUnknown';
3 |
4 | const meta: Meta = {
5 | title: 'Entities/Expr/Unknown',
6 | component: EntityExprNodeUnknown,
7 | };
8 |
9 | export default meta;
10 | type Story = StoryObj;
11 |
12 | export const Basic: Story = {};
13 |
--------------------------------------------------------------------------------
/packages/icons/icons/thumbs-up.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/users.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.sqlx/query-f79ae2c952ff6125556afa20b2f55d44e304fe66877f45d2a61a9f77b1d86b0a.json:
--------------------------------------------------------------------------------
1 | {
2 | "db_name": "PostgreSQL",
3 | "query": "DELETE FROM space_members\nWHERE user_id = $1\n AND space_id = $2;\n\n",
4 | "describe": {
5 | "columns": [],
6 | "parameters": {
7 | "Left": [
8 | "Uuid",
9 | "Uuid"
10 | ]
11 | },
12 | "nullable": []
13 | },
14 | "hash": "f79ae2c952ff6125556afa20b2f55d44e304fe66877f45d2a61a9f77b1d86b0a"
15 | }
16 |
--------------------------------------------------------------------------------
/apps/server/sql/channels/get_with_space_member.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | cm AS "channel!: ChannelMember",
3 | sm AS "space!: SpaceMember"
4 | FROM
5 | channel_members cm
6 | INNER JOIN channels ch ON cm.channel_id = ch.id
7 | AND ch.deleted = FALSE
8 | INNER JOIN space_members sm ON sm.space_id = ch.space_id
9 | AND sm.user_id = cm.user_id
10 | WHERE
11 | cm.user_id = $1
12 | AND cm.channel_id = $2
13 | AND cm.is_joined
14 | LIMIT 1;
15 |
16 |
--------------------------------------------------------------------------------
/apps/storybook/src/ErrorMessageBox.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react-vite';
2 | import { ErrorMessageBox } from '@boluo/ui/ErrorMessageBox';
3 |
4 | const meta: Meta = {
5 | title: 'Feedback/ErrorMessageBox',
6 | component: ErrorMessageBox,
7 | };
8 |
9 | export default meta;
10 | type Story = StoryObj;
11 |
12 | export const Basic: Story = { args: { children: 'This is an error message' } };
13 |
--------------------------------------------------------------------------------
/packages/icons/icons/thumbs-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/apps/legacy/src/assets/icons/chevron-down.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/apps/legacy/src/assets/icons/chevron-up.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/apps/legacy/src/assets/icons/comment-solid.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/icons/icons/scroll-text.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.sqlx/query-da12d4699b27ab86418abc9143bd0eb319f127c73112a0cbe4349213db435632.json:
--------------------------------------------------------------------------------
1 | {
2 | "db_name": "PostgreSQL",
3 | "query": "UPDATE\n users\nSET\n PASSWORD = crypt($2, gen_salt('bf'))\nWHERE\n id = $1;\n\n",
4 | "describe": {
5 | "columns": [],
6 | "parameters": {
7 | "Left": [
8 | "Uuid",
9 | "Text"
10 | ]
11 | },
12 | "nullable": []
13 | },
14 | "hash": "da12d4699b27ab86418abc9143bd0eb319f127c73112a0cbe4349213db435632"
15 | }
16 |
--------------------------------------------------------------------------------
/packages/icons/icons/file-plus.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/mask.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/ui/src/FallbackIcon.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from 'react';
2 |
3 | interface Props {
4 | placeholder?: string;
5 | className?: string;
6 | }
7 |
8 | export const FallbackIcon = memo(
9 | ({ placeholder = '⍰', className = 'inline-block w-[1em] h-[1em] text-text-subtle' }: Props) => {
10 | return (
11 |
12 | {placeholder}
13 |
14 | );
15 | },
16 | );
17 | FallbackIcon.displayName = 'FallbackIcon';
18 |
--------------------------------------------------------------------------------
/packages/ui/src/LampSwitch.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 |
3 | export const LampSwitch = ({ isOn }: { isOn: boolean }) => {
4 | return (
5 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/packages/ui/src/Spinner.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { CircleNotch } from '@boluo/icons';
3 | import React from 'react';
4 | import Icon from './Icon';
5 |
6 | interface Props {
7 | className?: string;
8 | label?: string;
9 | }
10 |
11 | export const Spinner: React.FC = ({ label, className }) => {
12 | return (
13 |
14 | );
15 | };
16 | Spinner.displayName = 'Spinner';
17 |
--------------------------------------------------------------------------------
/apps/storybook/src/FloatingBox.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react-vite';
2 |
3 | import { FloatingBox } from '@boluo/ui/FloatingBox';
4 |
5 | const meta: Meta = {
6 | title: 'Base/FloatingBox',
7 | component: FloatingBox,
8 | args: {
9 | className: 'p-4',
10 | },
11 | };
12 |
13 | export default meta;
14 | type Story = StoryObj;
15 |
16 | export const Basic: Story = { args: { children: 'This is a floating box' } };
17 |
--------------------------------------------------------------------------------
/packages/hooks/useQueryChannel.tsx:
--------------------------------------------------------------------------------
1 | import type { ApiError, Channel } from '@boluo/api';
2 | import { get } from '@boluo/api-browser';
3 | import useSWR, { type SWRResponse } from 'swr';
4 | import { unwrap } from '@boluo/utils/result';
5 |
6 | export const useQueryChannel = (channelId: string): SWRResponse => {
7 | const key = ['/channels/query', channelId] as const;
8 | return useSWR(key, ([path, id]) => get(path, { id }).then(unwrap));
9 | };
10 |
--------------------------------------------------------------------------------
/packages/icons/icons/book-copy.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/dice.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/packages/icons/icons/shuffle.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/apps/legacy/src/api/media.ts:
--------------------------------------------------------------------------------
1 | import { type Id } from '../utils/id';
2 |
3 | export interface Media {
4 | id: Id;
5 | mimeType: string;
6 | uploaderId: Id;
7 | filename: string;
8 | originalFilename: string;
9 | hash: string;
10 | description: string;
11 | created: number;
12 | }
13 |
14 | export interface PreSign {
15 | filename: string;
16 | mimeType: string;
17 | size: number;
18 | }
19 |
20 | export interface PreSignResult {
21 | url: string;
22 | mediaId: string;
23 | }
24 |
--------------------------------------------------------------------------------
/apps/site/src/app/[lang]/[theme]/account/sign-up/Footer.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import ChevronLeft from '@boluo/icons/ChevronLeft';
3 | import { FormattedMessage } from 'react-intl';
4 | import { ButtonLink } from '../../../../../components/ButtonLink';
5 |
6 | export function Footer() {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/apps/spa/hooks/useChatContainerClassnames.tsx:
--------------------------------------------------------------------------------
1 | import { type Settings } from '@boluo/settings';
2 | import { useSettings } from './useSettings';
3 | import clsx from 'clsx';
4 |
5 | export const settingsToClassnames = (settings: Partial) => {
6 | return clsx(settings.layout ?? 'irc-layout', settings.messageSize, settings.inGameFont);
7 | };
8 |
9 | export const useChatContainerClassnames = () => {
10 | const settings = useSettings();
11 | return settingsToClassnames(settings);
12 | };
13 |
--------------------------------------------------------------------------------
/apps/storybook/src/BroadcastTurnedOff.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react-vite';
2 | import { BroadcastTurnedOff } from '@boluo/ui/chat/BroadcastTurnedOff';
3 |
4 | const meta: Meta = {
5 | title: 'Chat/BroadcastTurnedOff',
6 | component: BroadcastTurnedOff,
7 | };
8 |
9 | export default meta;
10 | type Story = StoryObj;
11 |
12 | export const Basic: Story = {
13 | args: {
14 | count: 0,
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/apps/storybook/src/IsActionIndicator.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react-vite';
2 | import { IsActionIndicator } from '@boluo/ui/chat/IsActionIndicator';
3 |
4 | const meta: Meta = {
5 | title: 'Chat/IsActionIndicator',
6 | component: IsActionIndicator,
7 | parameters: {
8 | layout: 'centered',
9 | },
10 | };
11 |
12 | export default meta;
13 | type Story = StoryObj;
14 |
15 | export const Basic: Story = {};
16 |
--------------------------------------------------------------------------------
/docs/api/Messages/By Channel.bru:
--------------------------------------------------------------------------------
1 | meta {
2 | name: By Channel
3 | type: http
4 | seq: 1
5 | }
6 |
7 | get {
8 | url: {{base_url}}/messages/by_channel?channelId={{channel_id}}
9 | body: none
10 | auth: inherit
11 | }
12 |
13 | params:query {
14 | channelId: {{channel_id}}
15 | before:
16 | limit: 50
17 | }
18 |
19 | script:post-response {
20 | let data = res.getBody();
21 | if (data.isOk && data.ok.length > 0) {
22 | bru.setVar("message_id", data.ok[0].id);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/icons/icons/trash.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------