Frontend: Removed channels
ci/woodpecker/push/ociImagePush Pipeline is pending
Details
ci/woodpecker/push/ociImagePush Pipeline is pending
Details
This commit is contained in:
parent
c09188fa9c
commit
e88e46508d
|
@ -1,2 +0,0 @@
|
||||||
codecov:
|
|
||||||
token: d55e1270-f20a-4727-aa05-2bd57793315a
|
|
|
@ -1,195 +0,0 @@
|
||||||
/*
|
|
||||||
* For a detailed explanation regarding each configuration property and type check, visit:
|
|
||||||
* https://jestjs.io/docs/en/configuration.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default {
|
|
||||||
// All imported modules in your tests should be mocked automatically
|
|
||||||
// automock: false,
|
|
||||||
|
|
||||||
// Stop running tests after `n` failures
|
|
||||||
// bail: 0,
|
|
||||||
|
|
||||||
// The directory where Jest should store its cached dependency information
|
|
||||||
// cacheDirectory: "C:\\Users\\ai\\AppData\\Local\\Temp\\jest",
|
|
||||||
|
|
||||||
// Automatically clear mock calls and instances between every test
|
|
||||||
// clearMocks: false,
|
|
||||||
|
|
||||||
// Indicates whether the coverage information should be collected while executing the test
|
|
||||||
// collectCoverage: false,
|
|
||||||
|
|
||||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
|
||||||
// collectCoverageFrom: undefined,
|
|
||||||
|
|
||||||
// The directory where Jest should output its coverage files
|
|
||||||
coverageDirectory: "coverage",
|
|
||||||
|
|
||||||
// An array of regexp pattern strings used to skip coverage collection
|
|
||||||
// coveragePathIgnorePatterns: [
|
|
||||||
// "\\\\node_modules\\\\"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// Indicates which provider should be used to instrument code for coverage
|
|
||||||
coverageProvider: "v8",
|
|
||||||
|
|
||||||
// A list of reporter names that Jest uses when writing coverage reports
|
|
||||||
// coverageReporters: [
|
|
||||||
// "json",
|
|
||||||
// "text",
|
|
||||||
// "lcov",
|
|
||||||
// "clover"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// An object that configures minimum threshold enforcement for coverage results
|
|
||||||
// coverageThreshold: undefined,
|
|
||||||
|
|
||||||
// A path to a custom dependency extractor
|
|
||||||
// dependencyExtractor: undefined,
|
|
||||||
|
|
||||||
// Make calling deprecated APIs throw helpful error messages
|
|
||||||
// errorOnDeprecated: false,
|
|
||||||
|
|
||||||
// Force coverage collection from ignored files using an array of glob patterns
|
|
||||||
// forceCoverageMatch: [],
|
|
||||||
|
|
||||||
// A path to a module which exports an async function that is triggered once before all test suites
|
|
||||||
// globalSetup: undefined,
|
|
||||||
|
|
||||||
// A path to a module which exports an async function that is triggered once after all test suites
|
|
||||||
// globalTeardown: undefined,
|
|
||||||
|
|
||||||
// A set of global variables that need to be available in all test environments
|
|
||||||
// globals: {},
|
|
||||||
|
|
||||||
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
|
||||||
// maxWorkers: "50%",
|
|
||||||
|
|
||||||
// An array of directory names to be searched recursively up from the requiring module's location
|
|
||||||
// moduleDirectories: [
|
|
||||||
// "node_modules"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// An array of file extensions your modules use
|
|
||||||
// moduleFileExtensions: [
|
|
||||||
// "js",
|
|
||||||
// "json",
|
|
||||||
// "jsx",
|
|
||||||
// "ts",
|
|
||||||
// "tsx",
|
|
||||||
// "node"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
|
||||||
// moduleNameMapper: {},
|
|
||||||
|
|
||||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
|
||||||
// modulePathIgnorePatterns: [],
|
|
||||||
|
|
||||||
// Activates notifications for test results
|
|
||||||
// notify: false,
|
|
||||||
|
|
||||||
// An enum that specifies notification mode. Requires { notify: true }
|
|
||||||
// notifyMode: "failure-change",
|
|
||||||
|
|
||||||
// A preset that is used as a base for Jest's configuration
|
|
||||||
// preset: undefined,
|
|
||||||
|
|
||||||
// Run tests from one or more projects
|
|
||||||
// projects: undefined,
|
|
||||||
|
|
||||||
// Use this configuration option to add custom reporters to Jest
|
|
||||||
// reporters: undefined,
|
|
||||||
|
|
||||||
// Automatically reset mock state between every test
|
|
||||||
// resetMocks: false,
|
|
||||||
|
|
||||||
// Reset the module registry before running each individual test
|
|
||||||
// resetModules: false,
|
|
||||||
|
|
||||||
// A path to a custom resolver
|
|
||||||
// resolver: undefined,
|
|
||||||
|
|
||||||
// Automatically restore mock state between every test
|
|
||||||
// restoreMocks: false,
|
|
||||||
|
|
||||||
// The root directory that Jest should scan for tests and modules within
|
|
||||||
// rootDir: undefined,
|
|
||||||
|
|
||||||
// A list of paths to directories that Jest should use to search for files in
|
|
||||||
roots: ["<rootDir>"],
|
|
||||||
|
|
||||||
// Allows you to use a custom runner instead of Jest's default test runner
|
|
||||||
// runner: "jest-runner",
|
|
||||||
|
|
||||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
|
||||||
// setupFiles: [],
|
|
||||||
|
|
||||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
|
||||||
// setupFilesAfterEnv: [],
|
|
||||||
|
|
||||||
// The number of seconds after which a test is considered as slow and reported as such in the results.
|
|
||||||
// slowTestThreshold: 5,
|
|
||||||
|
|
||||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
|
||||||
// snapshotSerializers: [],
|
|
||||||
|
|
||||||
// The test environment that will be used for testing
|
|
||||||
testEnvironment: "node",
|
|
||||||
|
|
||||||
// Options that will be passed to the testEnvironment
|
|
||||||
// testEnvironmentOptions: {},
|
|
||||||
|
|
||||||
// Adds a location field to test results
|
|
||||||
// testLocationInResults: false,
|
|
||||||
|
|
||||||
// The glob patterns Jest uses to detect test files
|
|
||||||
testMatch: [
|
|
||||||
"**/__tests__/**/*.[jt]s?(x)",
|
|
||||||
"**/?(*.)+(spec|test).[tj]s?(x)",
|
|
||||||
"<rootDir>/test/**/*",
|
|
||||||
],
|
|
||||||
|
|
||||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
|
||||||
// testPathIgnorePatterns: [
|
|
||||||
// "\\\\node_modules\\\\"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
|
||||||
// testRegex: [],
|
|
||||||
|
|
||||||
// This option allows the use of a custom results processor
|
|
||||||
// testResultsProcessor: undefined,
|
|
||||||
|
|
||||||
// This option allows use of a custom test runner
|
|
||||||
// testRunner: "jasmine2",
|
|
||||||
|
|
||||||
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
|
|
||||||
// testURL: "http://localhost",
|
|
||||||
|
|
||||||
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
|
|
||||||
// timers: "real",
|
|
||||||
|
|
||||||
// A map from regular expressions to paths to transformers
|
|
||||||
transform: {
|
|
||||||
"^.+\\.(ts|tsx)$": "ts-jest",
|
|
||||||
},
|
|
||||||
|
|
||||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
|
||||||
// transformIgnorePatterns: [
|
|
||||||
// "\\\\node_modules\\\\",
|
|
||||||
// "\\.pnp\\.[^\\\\]+$"
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
|
||||||
// unmockedModulePathPatterns: undefined,
|
|
||||||
|
|
||||||
// Indicates whether each individual test should be reported during the run
|
|
||||||
// verbose: undefined,
|
|
||||||
|
|
||||||
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
|
||||||
// watchPathIgnorePatterns: [],
|
|
||||||
|
|
||||||
// Whether to use watchman for file crawling
|
|
||||||
// watchman: true,
|
|
||||||
};
|
|
|
@ -10,11 +10,7 @@
|
||||||
"tsd": "tsc && tsd",
|
"tsd": "tsc && tsd",
|
||||||
"api": "pnpm api-extractor run --local --verbose",
|
"api": "pnpm api-extractor run --local --verbose",
|
||||||
"api-prod": "pnpm api-extractor run --verbose",
|
"api-prod": "pnpm api-extractor run --verbose",
|
||||||
"api-doc": "pnpm api-documenter markdown -i ./etc/",
|
"api-doc": "pnpm api-documenter markdown -i ./etc/"
|
||||||
"lint": "pnpm rome check --apply *.ts",
|
|
||||||
"format": "pnpm rome format --write *.ts",
|
|
||||||
"jest": "jest --coverage --detectOpenHandles",
|
|
||||||
"test": "pnpm jest && pnpm tsd"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -25,13 +21,7 @@
|
||||||
"@microsoft/api-documenter": "^7.22.21",
|
"@microsoft/api-documenter": "^7.22.21",
|
||||||
"@swc/cli": "^0.1.62",
|
"@swc/cli": "^0.1.62",
|
||||||
"@swc/core": "^1.3.62",
|
"@swc/core": "^1.3.62",
|
||||||
"@types/jest": "^27.4.0",
|
|
||||||
"@types/node": "20.3.1",
|
"@types/node": "20.3.1",
|
||||||
"jest": "^27.4.5",
|
|
||||||
"jest-fetch-mock": "^3.0.3",
|
|
||||||
"jest-websocket-mock": "^2.2.1",
|
|
||||||
"mock-socket": "^9.0.8",
|
|
||||||
"ts-jest": "^27.1.2",
|
|
||||||
"ts-node": "10.4.0",
|
"ts-node": "10.4.0",
|
||||||
"tsd": "^0.28.1",
|
"tsd": "^0.28.1",
|
||||||
"typescript": "5.1.3"
|
"typescript": "5.1.3"
|
||||||
|
|
|
@ -54,7 +54,6 @@ type NoteSubmitReq = {
|
||||||
fileIds?: DriveFile["id"][];
|
fileIds?: DriveFile["id"][];
|
||||||
replyId?: null | Note["id"];
|
replyId?: null | Note["id"];
|
||||||
renoteId?: null | Note["id"];
|
renoteId?: null | Note["id"];
|
||||||
channelId?: null | Channel["id"];
|
|
||||||
poll?: null | {
|
poll?: null | {
|
||||||
choices: string[];
|
choices: string[];
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
|
@ -192,18 +191,6 @@ export type Endpoints = {
|
||||||
res: Blocking[];
|
res: Blocking[];
|
||||||
};
|
};
|
||||||
|
|
||||||
// channels
|
|
||||||
"channels/create": { req: TODO; res: TODO };
|
|
||||||
"channels/featured": { req: TODO; res: TODO };
|
|
||||||
"channels/follow": { req: TODO; res: TODO };
|
|
||||||
"channels/followed": { req: TODO; res: TODO };
|
|
||||||
"channels/owned": { req: TODO; res: TODO };
|
|
||||||
"channels/pin-note": { req: TODO; res: TODO };
|
|
||||||
"channels/show": { req: TODO; res: TODO };
|
|
||||||
"channels/timeline": { req: TODO; res: TODO };
|
|
||||||
"channels/unfollow": { req: TODO; res: TODO };
|
|
||||||
"channels/update": { req: TODO; res: TODO };
|
|
||||||
|
|
||||||
// charts
|
// charts
|
||||||
"charts/active-users": {
|
"charts/active-users": {
|
||||||
req: { span: "day" | "hour"; limit?: number; offset?: number | null };
|
req: { span: "day" | "hour"; limit?: number; offset?: number | null };
|
||||||
|
|
|
@ -51,8 +51,6 @@ export const permissions = [
|
||||||
"read:page-likes",
|
"read:page-likes",
|
||||||
"read:user-groups",
|
"read:user-groups",
|
||||||
"write:user-groups",
|
"write:user-groups",
|
||||||
"read:channels",
|
|
||||||
"write:channels",
|
|
||||||
"read:gallery",
|
"read:gallery",
|
||||||
"write:gallery",
|
"write:gallery",
|
||||||
"read:gallery-likes",
|
"read:gallery-likes",
|
||||||
|
|
|
@ -91,7 +91,6 @@ export type MeDetailed = UserDetailed & {
|
||||||
hasPendingReceivedFollowRequest: boolean;
|
hasPendingReceivedFollowRequest: boolean;
|
||||||
hasUnreadAnnouncement: boolean;
|
hasUnreadAnnouncement: boolean;
|
||||||
hasUnreadAntenna: boolean;
|
hasUnreadAntenna: boolean;
|
||||||
hasUnreadChannel: boolean;
|
|
||||||
hasUnreadMentions: boolean;
|
hasUnreadMentions: boolean;
|
||||||
hasUnreadMessagingMessage: boolean;
|
hasUnreadMessagingMessage: boolean;
|
||||||
hasUnreadNotification: boolean;
|
hasUnreadNotification: boolean;
|
||||||
|
@ -145,7 +144,6 @@ export type Note = {
|
||||||
visibility: "public" | "home" | "followers" | "specified";
|
visibility: "public" | "home" | "followers" | "specified";
|
||||||
visibleUserIds?: User["id"][];
|
visibleUserIds?: User["id"][];
|
||||||
localOnly?: boolean;
|
localOnly?: boolean;
|
||||||
channel?: Channel["id"];
|
|
||||||
myReaction?: string;
|
myReaction?: string;
|
||||||
reactions: Record<string, number>;
|
reactions: Record<string, number>;
|
||||||
renoteCount: number;
|
renoteCount: number;
|
||||||
|
@ -420,11 +418,6 @@ export type FollowRequest = {
|
||||||
followee: User;
|
followee: User;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Channel = {
|
|
||||||
id: ID;
|
|
||||||
// TODO
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Following = {
|
export type Following = {
|
||||||
id: ID;
|
id: ID;
|
||||||
createdAt: DateString;
|
createdAt: DateString;
|
||||||
|
|
|
@ -39,8 +39,6 @@ export type Channels = {
|
||||||
readAllAntennas: () => void;
|
readAllAntennas: () => void;
|
||||||
unreadAntenna: (payload: Antenna) => void;
|
unreadAntenna: (payload: Antenna) => void;
|
||||||
readAllAnnouncements: () => void;
|
readAllAnnouncements: () => void;
|
||||||
readAllChannels: () => void;
|
|
||||||
unreadChannel: (payload: Note["id"]) => void;
|
|
||||||
myTokenRegenerated: () => void;
|
myTokenRegenerated: () => void;
|
||||||
reversiNoInvites: () => void;
|
reversiNoInvites: () => void;
|
||||||
reversiInvited: (payload: FIXME) => void;
|
reversiInvited: (payload: FIXME) => void;
|
||||||
|
|
|
@ -1,222 +0,0 @@
|
||||||
import { APIClient, isAPIError } from "../src/api";
|
|
||||||
import { enableFetchMocks } from "jest-fetch-mock";
|
|
||||||
|
|
||||||
enableFetchMocks();
|
|
||||||
|
|
||||||
function getFetchCall(call: any[]) {
|
|
||||||
const { body, method } = call[1];
|
|
||||||
if (body != null && typeof body !== "string") {
|
|
||||||
throw new Error("invalid body");
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
url: call[0],
|
|
||||||
method: method,
|
|
||||||
body: JSON.parse(body as any),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("API", () => {
|
|
||||||
test("success", async () => {
|
|
||||||
fetchMock.resetMocks();
|
|
||||||
fetchMock.mockResponse(async (req) => {
|
|
||||||
const body = await req.json();
|
|
||||||
if (req.method === "POST" && req.url === "https://calckey.test/api/i") {
|
|
||||||
if (body.i === "TOKEN") {
|
|
||||||
return JSON.stringify({ id: "foo" });
|
|
||||||
} else {
|
|
||||||
return { status: 400 };
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return { status: 404 };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const cli = new APIClient({
|
|
||||||
origin: "https://calckey.test",
|
|
||||||
credential: "TOKEN",
|
|
||||||
});
|
|
||||||
|
|
||||||
const res = await cli.request("i");
|
|
||||||
|
|
||||||
expect(res).toEqual({
|
|
||||||
id: "foo",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({
|
|
||||||
url: "https://calckey.test/api/i",
|
|
||||||
method: "POST",
|
|
||||||
body: { i: "TOKEN" },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("with params", async () => {
|
|
||||||
fetchMock.resetMocks();
|
|
||||||
fetchMock.mockResponse(async (req) => {
|
|
||||||
const body = await req.json();
|
|
||||||
if (
|
|
||||||
req.method === "POST" &&
|
|
||||||
req.url === "https://calckey.test/api/notes/show"
|
|
||||||
) {
|
|
||||||
if (body.i === "TOKEN" && body.noteId === "aaaaa") {
|
|
||||||
return JSON.stringify({ id: "foo" });
|
|
||||||
} else {
|
|
||||||
return { status: 400 };
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return { status: 404 };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const cli = new APIClient({
|
|
||||||
origin: "https://calckey.test",
|
|
||||||
credential: "TOKEN",
|
|
||||||
});
|
|
||||||
|
|
||||||
const res = await cli.request("notes/show", { noteId: "aaaaa" });
|
|
||||||
|
|
||||||
expect(res).toEqual({
|
|
||||||
id: "foo",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({
|
|
||||||
url: "https://calckey.test/api/notes/show",
|
|
||||||
method: "POST",
|
|
||||||
body: { i: "TOKEN", noteId: "aaaaa" },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("204 No Content で null が返る", async () => {
|
|
||||||
fetchMock.resetMocks();
|
|
||||||
fetchMock.mockResponse(async (req) => {
|
|
||||||
if (
|
|
||||||
req.method === "POST" &&
|
|
||||||
req.url === "https://calckey.test/api/reset-password"
|
|
||||||
) {
|
|
||||||
return { status: 204 };
|
|
||||||
} else {
|
|
||||||
return { status: 404 };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const cli = new APIClient({
|
|
||||||
origin: "https://calckey.test",
|
|
||||||
credential: "TOKEN",
|
|
||||||
});
|
|
||||||
|
|
||||||
const res = await cli.request("reset-password", {
|
|
||||||
token: "aaa",
|
|
||||||
password: "aaa",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res).toEqual(null);
|
|
||||||
|
|
||||||
expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({
|
|
||||||
url: "https://calckey.test/api/reset-password",
|
|
||||||
method: "POST",
|
|
||||||
body: { i: "TOKEN", token: "aaa", password: "aaa" },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("インスタンスの credential が指定されていても引数で credential が null ならば null としてリクエストされる", async () => {
|
|
||||||
fetchMock.resetMocks();
|
|
||||||
fetchMock.mockResponse(async (req) => {
|
|
||||||
const body = await req.json();
|
|
||||||
if (req.method === "POST" && req.url === "https://calckey.test/api/i") {
|
|
||||||
if (typeof body.i === "string") {
|
|
||||||
return JSON.stringify({ id: "foo" });
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
status: 401,
|
|
||||||
body: JSON.stringify({
|
|
||||||
error: {
|
|
||||||
message: "Credential required.",
|
|
||||||
code: "CREDENTIAL_REQUIRED",
|
|
||||||
id: "1384574d-a912-4b81-8601-c7b1c4085df1",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return { status: 404 };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const cli = new APIClient({
|
|
||||||
origin: "https://calckey.test",
|
|
||||||
credential: "TOKEN",
|
|
||||||
});
|
|
||||||
|
|
||||||
await cli.request("i", {}, null);
|
|
||||||
} catch (e) {
|
|
||||||
expect(isAPIError(e)).toEqual(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("api error", async () => {
|
|
||||||
fetchMock.resetMocks();
|
|
||||||
fetchMock.mockResponse(async (req) => {
|
|
||||||
return {
|
|
||||||
status: 500,
|
|
||||||
body: JSON.stringify({
|
|
||||||
error: {
|
|
||||||
message:
|
|
||||||
"Internal error occurred. Please contact us if the error persists.",
|
|
||||||
code: "INTERNAL_ERROR",
|
|
||||||
id: "5d37dbcb-891e-41ca-a3d6-e690c97775ac",
|
|
||||||
kind: "server",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const cli = new APIClient({
|
|
||||||
origin: "https://calckey.test",
|
|
||||||
credential: "TOKEN",
|
|
||||||
});
|
|
||||||
|
|
||||||
await cli.request("i");
|
|
||||||
} catch (e: any) {
|
|
||||||
expect(isAPIError(e)).toEqual(true);
|
|
||||||
expect(e.id).toEqual("5d37dbcb-891e-41ca-a3d6-e690c97775ac");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("network error", async () => {
|
|
||||||
fetchMock.resetMocks();
|
|
||||||
fetchMock.mockAbort();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const cli = new APIClient({
|
|
||||||
origin: "https://calckey.test",
|
|
||||||
credential: "TOKEN",
|
|
||||||
});
|
|
||||||
|
|
||||||
await cli.request("i");
|
|
||||||
} catch (e) {
|
|
||||||
expect(isAPIError(e)).toEqual(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("json parse error", async () => {
|
|
||||||
fetchMock.resetMocks();
|
|
||||||
fetchMock.mockResponse(async (req) => {
|
|
||||||
return {
|
|
||||||
status: 500,
|
|
||||||
body: "<html>I AM NOT JSON</html>",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const cli = new APIClient({
|
|
||||||
origin: "https://calckey.test",
|
|
||||||
credential: "TOKEN",
|
|
||||||
});
|
|
||||||
|
|
||||||
await cli.request("i");
|
|
||||||
} catch (e) {
|
|
||||||
expect(isAPIError(e)).toEqual(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,181 +0,0 @@
|
||||||
import WS from "jest-websocket-mock";
|
|
||||||
import Stream from "../src/streaming";
|
|
||||||
|
|
||||||
describe("Streaming", () => {
|
|
||||||
test("useChannel", async () => {
|
|
||||||
const server = new WS("wss://calckey.test/streaming");
|
|
||||||
const stream = new Stream("https://calckey.test", { token: "TOKEN" });
|
|
||||||
const mainChannelReceived: any[] = [];
|
|
||||||
const main = stream.useChannel("main");
|
|
||||||
main.on("meUpdated", (payload) => {
|
|
||||||
mainChannelReceived.push(payload);
|
|
||||||
});
|
|
||||||
|
|
||||||
const ws = await server.connected;
|
|
||||||
expect(new URLSearchParams(new URL(ws.url).search).get("i")).toEqual(
|
|
||||||
"TOKEN",
|
|
||||||
);
|
|
||||||
|
|
||||||
const msg = JSON.parse((await server.nextMessage) as string);
|
|
||||||
const mainChannelId = msg.body.id;
|
|
||||||
expect(msg.type).toEqual("connect");
|
|
||||||
expect(msg.body.channel).toEqual("main");
|
|
||||||
expect(mainChannelId != null).toEqual(true);
|
|
||||||
|
|
||||||
server.send(
|
|
||||||
JSON.stringify({
|
|
||||||
type: "channel",
|
|
||||||
body: {
|
|
||||||
id: mainChannelId,
|
|
||||||
type: "meUpdated",
|
|
||||||
body: {
|
|
||||||
id: "foo",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(mainChannelReceived[0]).toEqual({
|
|
||||||
id: "foo",
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.close();
|
|
||||||
server.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("useChannel with parameters", async () => {
|
|
||||||
const server = new WS("wss://calckey.test/streaming");
|
|
||||||
const stream = new Stream("https://calckey.test", { token: "TOKEN" });
|
|
||||||
const messagingChannelReceived: any[] = [];
|
|
||||||
const messaging = stream.useChannel("messaging", { otherparty: "aaa" });
|
|
||||||
messaging.on("message", (payload) => {
|
|
||||||
messagingChannelReceived.push(payload);
|
|
||||||
});
|
|
||||||
|
|
||||||
const ws = await server.connected;
|
|
||||||
expect(new URLSearchParams(new URL(ws.url).search).get("i")).toEqual(
|
|
||||||
"TOKEN",
|
|
||||||
);
|
|
||||||
|
|
||||||
const msg = JSON.parse((await server.nextMessage) as string);
|
|
||||||
const messagingChannelId = msg.body.id;
|
|
||||||
expect(msg.type).toEqual("connect");
|
|
||||||
expect(msg.body.channel).toEqual("messaging");
|
|
||||||
expect(msg.body.params).toEqual({ otherparty: "aaa" });
|
|
||||||
expect(messagingChannelId != null).toEqual(true);
|
|
||||||
|
|
||||||
server.send(
|
|
||||||
JSON.stringify({
|
|
||||||
type: "channel",
|
|
||||||
body: {
|
|
||||||
id: messagingChannelId,
|
|
||||||
type: "message",
|
|
||||||
body: {
|
|
||||||
id: "foo",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(messagingChannelReceived[0]).toEqual({
|
|
||||||
id: "foo",
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.close();
|
|
||||||
server.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ちゃんとチャンネルごとにidが異なる", async () => {
|
|
||||||
const server = new WS("wss://calckey.test/streaming");
|
|
||||||
const stream = new Stream("https://calckey.test", { token: "TOKEN" });
|
|
||||||
|
|
||||||
stream.useChannel("messaging", { otherparty: "aaa" });
|
|
||||||
stream.useChannel("messaging", { otherparty: "bbb" });
|
|
||||||
|
|
||||||
const ws = await server.connected;
|
|
||||||
expect(new URLSearchParams(new URL(ws.url).search).get("i")).toEqual(
|
|
||||||
"TOKEN",
|
|
||||||
);
|
|
||||||
|
|
||||||
const msg = JSON.parse((await server.nextMessage) as string);
|
|
||||||
const messagingChannelId = msg.body.id;
|
|
||||||
const msg2 = JSON.parse((await server.nextMessage) as string);
|
|
||||||
const messagingChannelId2 = msg2.body.id;
|
|
||||||
|
|
||||||
expect(messagingChannelId != null).toEqual(true);
|
|
||||||
expect(messagingChannelId2 != null).toEqual(true);
|
|
||||||
expect(messagingChannelId).not.toEqual(messagingChannelId2);
|
|
||||||
|
|
||||||
stream.close();
|
|
||||||
server.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Connection#send", async () => {
|
|
||||||
const server = new WS("wss://calckey.test/streaming");
|
|
||||||
const stream = new Stream("https://calckey.test", { token: "TOKEN" });
|
|
||||||
|
|
||||||
const messaging = stream.useChannel("messaging", { otherparty: "aaa" });
|
|
||||||
messaging.send("read", { id: "aaa" });
|
|
||||||
|
|
||||||
const ws = await server.connected;
|
|
||||||
expect(new URLSearchParams(new URL(ws.url).search).get("i")).toEqual(
|
|
||||||
"TOKEN",
|
|
||||||
);
|
|
||||||
|
|
||||||
const connectMsg = JSON.parse((await server.nextMessage) as string);
|
|
||||||
const channelId = connectMsg.body.id;
|
|
||||||
const msg = JSON.parse((await server.nextMessage) as string);
|
|
||||||
|
|
||||||
expect(msg.type).toEqual("ch");
|
|
||||||
expect(msg.body.id).toEqual(channelId);
|
|
||||||
expect(msg.body.type).toEqual("read");
|
|
||||||
expect(msg.body.body).toEqual({ id: "aaa" });
|
|
||||||
|
|
||||||
stream.close();
|
|
||||||
server.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Connection#dispose", async () => {
|
|
||||||
const server = new WS("wss://calckey.test/streaming");
|
|
||||||
const stream = new Stream("https://calckey.test", { token: "TOKEN" });
|
|
||||||
const mainChannelReceived: any[] = [];
|
|
||||||
const main = stream.useChannel("main");
|
|
||||||
main.on("meUpdated", (payload) => {
|
|
||||||
mainChannelReceived.push(payload);
|
|
||||||
});
|
|
||||||
|
|
||||||
const ws = await server.connected;
|
|
||||||
expect(new URLSearchParams(new URL(ws.url).search).get("i")).toEqual(
|
|
||||||
"TOKEN",
|
|
||||||
);
|
|
||||||
|
|
||||||
const msg = JSON.parse((await server.nextMessage) as string);
|
|
||||||
const mainChannelId = msg.body.id;
|
|
||||||
expect(msg.type).toEqual("connect");
|
|
||||||
expect(msg.body.channel).toEqual("main");
|
|
||||||
expect(mainChannelId != null).toEqual(true);
|
|
||||||
main.dispose();
|
|
||||||
|
|
||||||
server.send(
|
|
||||||
JSON.stringify({
|
|
||||||
type: "channel",
|
|
||||||
body: {
|
|
||||||
id: mainChannelId,
|
|
||||||
type: "meUpdated",
|
|
||||||
body: {
|
|
||||||
id: "foo",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(mainChannelReceived.length).toEqual(0);
|
|
||||||
|
|
||||||
stream.close();
|
|
||||||
server.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: SharedConnection#dispose して一定時間経ったら disconnect メッセージがサーバーに送られてくるかのテスト
|
|
||||||
|
|
||||||
// TODO: チャンネル接続が使いまわされるかのテスト
|
|
||||||
});
|
|
|
@ -16,5 +16,5 @@
|
||||||
"esModuleInterop": true
|
"esModuleInterop": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules", "test/**/*"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"watch": "pnpm vite build --watch --mode development",
|
"watch": "pnpm vite build --watch --mode development",
|
||||||
"build": "pnpm vite build",
|
"build": "pnpm vite build",
|
||||||
"lint": "pnpm rome check \"src/**/*.{ts,vue}\"",
|
|
||||||
"format": "pnpm prettier --write '**/*.vue'"
|
"format": "pnpm prettier --write '**/*.vue'"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -1,135 +0,0 @@
|
||||||
<template>
|
|
||||||
<button
|
|
||||||
class="hdcaacmi _button"
|
|
||||||
:class="{ wait, active: isFollowing, full }"
|
|
||||||
:disabled="wait"
|
|
||||||
@click="onClick"
|
|
||||||
>
|
|
||||||
<template v-if="!wait">
|
|
||||||
<template v-if="isFollowing">
|
|
||||||
<span v-if="full">{{ i18n.ts.unfollow }}</span
|
|
||||||
><i class="ph-minus ph-bold ph-lg"></i>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<span v-if="full">{{ i18n.ts.follow }}</span
|
|
||||||
><i class="ph-plus ph-bold ph-lg"></i>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<span v-if="full">{{ i18n.ts.processing }}</span
|
|
||||||
><i class="ph-circle-notch ph-bold ph-lg fa-pulse ph-fw ph-lg"></i>
|
|
||||||
</template>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { ref } from "vue";
|
|
||||||
import * as os from "@/os";
|
|
||||||
import { i18n } from "@/i18n";
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
channel: Record<string, any>;
|
|
||||||
full?: boolean;
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
full: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const isFollowing = ref<boolean>(props.channel.isFollowing);
|
|
||||||
const wait = ref(false);
|
|
||||||
|
|
||||||
async function onClick() {
|
|
||||||
wait.value = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (isFollowing.value) {
|
|
||||||
await os.api("channels/unfollow", {
|
|
||||||
channelId: props.channel.id,
|
|
||||||
});
|
|
||||||
isFollowing.value = false;
|
|
||||||
} else {
|
|
||||||
await os.api("channels/follow", {
|
|
||||||
channelId: props.channel.id,
|
|
||||||
});
|
|
||||||
isFollowing.value = true;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
} finally {
|
|
||||||
wait.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.hdcaacmi {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--accent);
|
|
||||||
background: transparent;
|
|
||||||
border: solid 1px var(--accent);
|
|
||||||
padding: 0;
|
|
||||||
height: 31px;
|
|
||||||
font-size: 16px;
|
|
||||||
border-radius: 32px;
|
|
||||||
background: #fff;
|
|
||||||
|
|
||||||
&.full {
|
|
||||||
padding: 0 8px 0 12px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.full) {
|
|
||||||
width: 31px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
&:after {
|
|
||||||
content: "";
|
|
||||||
pointer-events: none;
|
|
||||||
position: absolute;
|
|
||||||
top: -5px;
|
|
||||||
right: -5px;
|
|
||||||
bottom: -5px;
|
|
||||||
left: -5px;
|
|
||||||
border: 2px solid var(--focus);
|
|
||||||
border-radius: 32px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
//background: mix($primary, #fff, 20);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
//background: mix($primary, #fff, 40);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
color: #fff;
|
|
||||||
background: var(--accent);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--accentLighten);
|
|
||||||
border-color: var(--accentLighten);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background: var(--accentDarken);
|
|
||||||
border-color: var(--accentDarken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.wait {
|
|
||||||
cursor: wait !important;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
> span {
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,42 +0,0 @@
|
||||||
<template>
|
|
||||||
<MkPagination :pagination="pagination">
|
|
||||||
<template #empty>
|
|
||||||
<div class="_fullinfo">
|
|
||||||
<img
|
|
||||||
src="/static-assets/badges/not-found.png"
|
|
||||||
class="_ghost"
|
|
||||||
:alt="i18n.ts.notFound"
|
|
||||||
/>
|
|
||||||
<div>{{ i18n.ts.notFound }}</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #default="{ items }">
|
|
||||||
<MkChannelPreview
|
|
||||||
v-for="item in items"
|
|
||||||
:key="item.id"
|
|
||||||
class="_margin"
|
|
||||||
:channel="extractor(item)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</MkPagination>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import MkChannelPreview from "@/components/MkChannelPreview.vue";
|
|
||||||
import MkPagination, { Paging } from "@/components/MkPagination.vue";
|
|
||||||
import { i18n } from "@/i18n";
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
pagination: Paging;
|
|
||||||
noGap?: boolean;
|
|
||||||
extractor?: (item: any) => any;
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
extractor: (item) => item,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
|
|
@ -1,174 +0,0 @@
|
||||||
<template>
|
|
||||||
<MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1">
|
|
||||||
<div class="banner" :style="bannerStyle">
|
|
||||||
<div class="fade"></div>
|
|
||||||
<div class="name">
|
|
||||||
<i class="ph-television ph-bold ph-lg"></i> {{ channel.name }}
|
|
||||||
</div>
|
|
||||||
<div class="status">
|
|
||||||
<div>
|
|
||||||
<i class="ph-users ph-bold ph-lg ph-fw ph-lg"></i>
|
|
||||||
<I18n
|
|
||||||
:src="i18n.ts._channel.usersCount"
|
|
||||||
tag="span"
|
|
||||||
style="margin-left: 4px"
|
|
||||||
>
|
|
||||||
<template #n>
|
|
||||||
<b>{{ channel.usersCount }}</b>
|
|
||||||
</template>
|
|
||||||
</I18n>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<i class="ph-pencil ph-bold ph-lg ph-fw ph-lg"></i>
|
|
||||||
<I18n
|
|
||||||
:src="i18n.ts._channel.notesCount"
|
|
||||||
tag="span"
|
|
||||||
style="margin-left: 4px"
|
|
||||||
>
|
|
||||||
<template #n>
|
|
||||||
<b>{{ channel.notesCount }}</b>
|
|
||||||
</template>
|
|
||||||
</I18n>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<article v-if="channel.description">
|
|
||||||
<p :title="channel.description">
|
|
||||||
{{
|
|
||||||
channel.description.length > 85
|
|
||||||
? channel.description.slice(0, 85) + "…"
|
|
||||||
: channel.description
|
|
||||||
}}
|
|
||||||
</p>
|
|
||||||
</article>
|
|
||||||
<footer>
|
|
||||||
<span v-if="channel.lastNotedAt">
|
|
||||||
{{ i18n.ts.updatedAt }}: <MkTime :time="channel.lastNotedAt" />
|
|
||||||
</span>
|
|
||||||
</footer>
|
|
||||||
</MkA>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed } from "vue";
|
|
||||||
import { i18n } from "@/i18n";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
channel: Record<string, any>;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const bannerStyle = computed(() => {
|
|
||||||
if (props.channel.bannerUrl) {
|
|
||||||
return { backgroundImage: `url(${props.channel.bannerUrl})` };
|
|
||||||
} else {
|
|
||||||
return { backgroundColor: "#4c5e6d" };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.eftoefju {
|
|
||||||
display: block;
|
|
||||||
overflow: hidden;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .banner {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 200px;
|
|
||||||
background-position: center;
|
|
||||||
background-size: cover;
|
|
||||||
|
|
||||||
> .fade {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 64px;
|
|
||||||
background: linear-gradient(0deg, var(--panel), var(--X15));
|
|
||||||
}
|
|
||||||
|
|
||||||
> .name {
|
|
||||||
position: absolute;
|
|
||||||
top: 16px;
|
|
||||||
left: 16px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
-webkit-backdrop-filter: var(--blur, blur(8px));
|
|
||||||
backdrop-filter: var(--blur, blur(8px));
|
|
||||||
background: rgba(0, 0, 0, 0.2);
|
|
||||||
color: #fff;
|
|
||||||
font-size: 1.2em;
|
|
||||||
border-radius: 999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .status {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
bottom: 16px;
|
|
||||||
right: 16px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-size: 80%;
|
|
||||||
-webkit-backdrop-filter: var(--blur, blur(8px));
|
|
||||||
backdrop-filter: var(--blur, blur(8px));
|
|
||||||
background: rgba(0, 0, 0, 0.2);
|
|
||||||
border-radius: 6px;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> article {
|
|
||||||
padding: 16px;
|
|
||||||
|
|
||||||
> p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> footer {
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-top: solid 0.5px var(--divider);
|
|
||||||
|
|
||||||
> span {
|
|
||||||
opacity: 0.7;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 550px) {
|
|
||||||
font-size: 0.9em;
|
|
||||||
|
|
||||||
> .banner {
|
|
||||||
height: 80px;
|
|
||||||
|
|
||||||
> .status {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> article {
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> footer {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
|
||||||
font-size: 0.8em;
|
|
||||||
|
|
||||||
> .banner {
|
|
||||||
height: 70px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> article {
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -132,14 +132,6 @@
|
||||||
<MkA class="created-at" :to="notePage(appearNote)">
|
<MkA class="created-at" :to="notePage(appearNote)">
|
||||||
<MkTime :time="appearNote.createdAt" mode="absolute" />
|
<MkTime :time="appearNote.createdAt" mode="absolute" />
|
||||||
</MkA>
|
</MkA>
|
||||||
<MkA
|
|
||||||
v-if="appearNote.channel && !inChannel"
|
|
||||||
class="channel"
|
|
||||||
:to="`/channels/${appearNote.channel.id}`"
|
|
||||||
@click.stop
|
|
||||||
><i class="ph-television ph-bold ph-lg"></i>
|
|
||||||
{{ appearNote.channel.name }}</MkA
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<footer ref="footerEl" class="footer" @click.stop tabindex="-1">
|
<footer ref="footerEl" class="footer" @click.stop tabindex="-1">
|
||||||
<XReactionsViewer
|
<XReactionsViewer
|
||||||
|
@ -288,8 +280,6 @@ const props = defineProps<{
|
||||||
collapsedReply?: boolean;
|
collapsedReply?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const inChannel = inject("inChannel", null);
|
|
||||||
|
|
||||||
let note = $ref(deepClone(props.note));
|
let note = $ref(deepClone(props.note));
|
||||||
|
|
||||||
const softMuteReasonI18nSrc = (what?: string) => {
|
const softMuteReasonI18nSrc = (what?: string) => {
|
||||||
|
|
|
@ -35,7 +35,6 @@
|
||||||
ref="visibilityButton"
|
ref="visibilityButton"
|
||||||
v-tooltip="i18n.ts.visibility"
|
v-tooltip="i18n.ts.visibility"
|
||||||
class="_button visibility"
|
class="_button visibility"
|
||||||
:disabled="channel != null"
|
|
||||||
@click="setVisibility"
|
@click="setVisibility"
|
||||||
>
|
>
|
||||||
<span v-if="visibility === 'public'"
|
<span v-if="visibility === 'public'"
|
||||||
|
@ -265,7 +264,6 @@ const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
reply?: misskey.entities.Note;
|
reply?: misskey.entities.Note;
|
||||||
renote?: misskey.entities.Note;
|
renote?: misskey.entities.Note;
|
||||||
channel?: any; // TODO
|
|
||||||
mention?: misskey.entities.User;
|
mention?: misskey.entities.User;
|
||||||
specified?: misskey.entities.User;
|
specified?: misskey.entities.User;
|
||||||
initialText?: string;
|
initialText?: string;
|
||||||
|
@ -333,18 +331,12 @@ let hasNotSpecifiedMentions = $ref(false);
|
||||||
let recentHashtags = $ref(JSON.parse(localStorage.getItem("hashtags") || "[]"));
|
let recentHashtags = $ref(JSON.parse(localStorage.getItem("hashtags") || "[]"));
|
||||||
let imeText = $ref("");
|
let imeText = $ref("");
|
||||||
|
|
||||||
const typing = throttle(3000, () => {
|
|
||||||
if (props.channel) {
|
|
||||||
stream.send("typingOnChannel", { channel: props.channel.id });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const draftKey = $computed((): string => {
|
const draftKey = $computed((): string => {
|
||||||
if (props.editId) {
|
if (props.editId) {
|
||||||
return `edit:${props.editId}`;
|
return `edit:${props.editId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let key = props.channel ? `channel:${props.channel.id}` : "";
|
let key = "";
|
||||||
|
|
||||||
if (props.renote) {
|
if (props.renote) {
|
||||||
key += `renote:${props.renote.id}`;
|
key += `renote:${props.renote.id}`;
|
||||||
|
@ -362,8 +354,6 @@ const placeholder = $computed((): string => {
|
||||||
return i18n.ts._postForm.quotePlaceholder;
|
return i18n.ts._postForm.quotePlaceholder;
|
||||||
} else if (props.reply) {
|
} else if (props.reply) {
|
||||||
return i18n.ts._postForm.replyPlaceholder;
|
return i18n.ts._postForm.replyPlaceholder;
|
||||||
} else if (props.channel) {
|
|
||||||
return i18n.ts._postForm.channelPlaceholder;
|
|
||||||
} else {
|
} else {
|
||||||
const xs = [
|
const xs = [
|
||||||
i18n.ts._postForm._placeholders.a,
|
i18n.ts._postForm._placeholders.a,
|
||||||
|
@ -464,11 +454,6 @@ if (props.reply && props.reply.text != null) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.channel) {
|
|
||||||
visibility = "public";
|
|
||||||
localOnly = true; // TODO: チャンネルが連合するようになった折には消す
|
|
||||||
}
|
|
||||||
|
|
||||||
// 公開以外へのリプライ時は元の公開範囲を引き継ぐ
|
// 公開以外へのリプライ時は元の公開範囲を引き継ぐ
|
||||||
if (
|
if (
|
||||||
props.reply &&
|
props.reply &&
|
||||||
|
@ -622,11 +607,6 @@ function upload(file: File, name?: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setVisibility() {
|
function setVisibility() {
|
||||||
if (props.channel) {
|
|
||||||
// TODO: information dialog
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
os.popup(
|
os.popup(
|
||||||
defineAsyncComponent(
|
defineAsyncComponent(
|
||||||
() => import("@/components/MkVisibilityPicker.vue")
|
() => import("@/components/MkVisibilityPicker.vue")
|
||||||
|
@ -830,7 +810,6 @@ async function post() {
|
||||||
: quoteId
|
: quoteId
|
||||||
? quoteId
|
? quoteId
|
||||||
: undefined,
|
: undefined,
|
||||||
channelId: props.channel ? props.channel.id : undefined,
|
|
||||||
poll: poll,
|
poll: poll,
|
||||||
cw: useCw ? cw || "" : undefined,
|
cw: useCw ? cw || "" : undefined,
|
||||||
localOnly: localOnly,
|
localOnly: localOnly,
|
||||||
|
|
|
@ -27,7 +27,6 @@ import MkPostForm from "@/components/MkPostForm.vue";
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
reply?: misskey.entities.Note;
|
reply?: misskey.entities.Note;
|
||||||
renote?: misskey.entities.Note;
|
renote?: misskey.entities.Note;
|
||||||
channel?: any; // TODO
|
|
||||||
mention?: misskey.entities.User;
|
mention?: misskey.entities.User;
|
||||||
specified?: misskey.entities.User;
|
specified?: misskey.entities.User;
|
||||||
initialText?: string;
|
initialText?: string;
|
||||||
|
|
|
@ -32,7 +32,6 @@ const props = defineProps<{
|
||||||
src: string;
|
src: string;
|
||||||
list?: string;
|
list?: string;
|
||||||
antenna?: string;
|
antenna?: string;
|
||||||
channel?: string;
|
|
||||||
sound?: boolean;
|
sound?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -41,11 +40,6 @@ const emit = defineEmits<{
|
||||||
(ev: "queue", count: number): void;
|
(ev: "queue", count: number): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
provide(
|
|
||||||
"inChannel",
|
|
||||||
computed(() => props.src === "channel")
|
|
||||||
);
|
|
||||||
|
|
||||||
const tlComponent: InstanceType<typeof XNotes> = $ref();
|
const tlComponent: InstanceType<typeof XNotes> = $ref();
|
||||||
|
|
||||||
const prepend = (note) => {
|
const prepend = (note) => {
|
||||||
|
@ -180,15 +174,6 @@ if (props.src === "antenna") {
|
||||||
connection.on("note", prepend);
|
connection.on("note", prepend);
|
||||||
connection.on("userAdded", onUserAdded);
|
connection.on("userAdded", onUserAdded);
|
||||||
connection.on("userRemoved", onUserRemoved);
|
connection.on("userRemoved", onUserRemoved);
|
||||||
} else if (props.src === "channel") {
|
|
||||||
endpoint = "channels/timeline";
|
|
||||||
query = {
|
|
||||||
channelId: props.channel,
|
|
||||||
};
|
|
||||||
connection = stream.useChannel("channel", {
|
|
||||||
channelId: props.channel,
|
|
||||||
});
|
|
||||||
connection.on("note", prepend);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeHint() {
|
function closeHint() {
|
||||||
|
|
|
@ -465,15 +465,6 @@ function checkForSplash() {
|
||||||
updateAccount({ hasUnreadAnnouncement: false });
|
updateAccount({ hasUnreadAnnouncement: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
main.on("readAllChannels", () => {
|
|
||||||
updateAccount({ hasUnreadChannel: false });
|
|
||||||
});
|
|
||||||
|
|
||||||
main.on("unreadChannel", () => {
|
|
||||||
updateAccount({ hasUnreadChannel: true });
|
|
||||||
sound.play("channel");
|
|
||||||
});
|
|
||||||
|
|
||||||
// トークンが再生成されたとき
|
// トークンが再生成されたとき
|
||||||
// このままではMisskeyが利用できないので強制的にサインアウトさせる
|
// このままではMisskeyが利用できないので強制的にサインアウトさせる
|
||||||
main.on("myTokenRegenerated", () => {
|
main.on("myTokenRegenerated", () => {
|
||||||
|
|
|
@ -1,14 +1,10 @@
|
||||||
import { computed, ref, reactive } from "vue";
|
import { computed, reactive } from "vue";
|
||||||
import { $i } from "./account";
|
import { $i } from "./account";
|
||||||
import { search } from "@/scripts/search";
|
import { search } from "@/scripts/search";
|
||||||
import * as os from "@/os";
|
import * as os from "@/os";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
import { ui } from "@/config";
|
import { ui } from "@/config";
|
||||||
import { unisonReload } from "@/scripts/unison-reload";
|
import { unisonReload } from "@/scripts/unison-reload";
|
||||||
import { defaultStore } from "@/store";
|
|
||||||
import { instance } from "@/instance";
|
|
||||||
import { host } from "@/config";
|
|
||||||
import XTutorial from "@/components/MkTutorialDialog.vue";
|
|
||||||
|
|
||||||
export const navbarItemDef = reactive({
|
export const navbarItemDef = reactive({
|
||||||
notifications: {
|
notifications: {
|
||||||
|
@ -89,11 +85,6 @@ export const navbarItemDef = reactive({
|
||||||
show: computed(() => $i != null),
|
show: computed(() => $i != null),
|
||||||
to: "/my/clips",
|
to: "/my/clips",
|
||||||
},
|
},
|
||||||
channels: {
|
|
||||||
title: "channel",
|
|
||||||
icon: "ph-television ph-bold ph-lg",
|
|
||||||
to: "/channels",
|
|
||||||
},
|
|
||||||
groups: {
|
groups: {
|
||||||
title: "groups",
|
title: "groups",
|
||||||
icon: "ph-users-three ph-bold ph-lg",
|
icon: "ph-users-three ph-bold ph-lg",
|
||||||
|
|
|
@ -1,144 +0,0 @@
|
||||||
<template>
|
|
||||||
<MkStickyContainer>
|
|
||||||
<template #header
|
|
||||||
><MkPageHeader :actions="headerActions" :tabs="headerTabs"
|
|
||||||
/></template>
|
|
||||||
<MkSpacer :content-max="700">
|
|
||||||
<div class="_formRoot">
|
|
||||||
<MkInput v-model="name" class="_formBlock">
|
|
||||||
<template #label>{{ i18n.ts.name }}</template>
|
|
||||||
</MkInput>
|
|
||||||
|
|
||||||
<MkTextarea v-model="description" class="_formBlock">
|
|
||||||
<template #label>{{ i18n.ts.description }}</template>
|
|
||||||
</MkTextarea>
|
|
||||||
|
|
||||||
<div class="banner">
|
|
||||||
<MkButton v-if="bannerId == null" @click="setBannerImage"
|
|
||||||
><i class="ph-plus ph-bold ph-lg"></i>
|
|
||||||
{{ i18n.ts._channel.setBanner }}</MkButton
|
|
||||||
>
|
|
||||||
<div v-else-if="bannerUrl">
|
|
||||||
<img :src="bannerUrl" style="width: 100%" />
|
|
||||||
<MkButton @click="removeBannerImage()"
|
|
||||||
><i class="ph-trash ph-bold ph-lg"></i>
|
|
||||||
{{ i18n.ts._channel.removeBanner }}</MkButton
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="_formBlock">
|
|
||||||
<MkButton primary @click="save()"
|
|
||||||
><i class="ph-floppy-disk-back ph-bold ph-lg"></i>
|
|
||||||
{{
|
|
||||||
channelId ? i18n.ts.save : i18n.ts.create
|
|
||||||
}}</MkButton
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</MkSpacer>
|
|
||||||
</MkStickyContainer>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, inject, watch } from "vue";
|
|
||||||
import MkTextarea from "@/components/form/textarea.vue";
|
|
||||||
import MkButton from "@/components/MkButton.vue";
|
|
||||||
import MkInput from "@/components/form/input.vue";
|
|
||||||
import { selectFile } from "@/scripts/select-file";
|
|
||||||
import * as os from "@/os";
|
|
||||||
import { useRouter } from "@/router";
|
|
||||||
import { definePageMetadata } from "@/scripts/page-metadata";
|
|
||||||
import { i18n } from "@/i18n";
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
channelId?: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
let channel = $ref(null);
|
|
||||||
let name = $ref(null);
|
|
||||||
let description = $ref(null);
|
|
||||||
let bannerUrl = $ref<string | null>(null);
|
|
||||||
let bannerId = $ref<string | null>(null);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => bannerId,
|
|
||||||
async () => {
|
|
||||||
if (bannerId == null) {
|
|
||||||
bannerUrl = null;
|
|
||||||
} else {
|
|
||||||
bannerUrl = (
|
|
||||||
await os.api("drive/files/show", {
|
|
||||||
fileId: bannerId,
|
|
||||||
})
|
|
||||||
).url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
async function fetchChannel() {
|
|
||||||
if (props.channelId == null) return;
|
|
||||||
|
|
||||||
channel = await os.api("channels/show", {
|
|
||||||
channelId: props.channelId,
|
|
||||||
});
|
|
||||||
|
|
||||||
name = channel.name;
|
|
||||||
description = channel.description;
|
|
||||||
bannerId = channel.bannerId;
|
|
||||||
bannerUrl = channel.bannerUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchChannel();
|
|
||||||
|
|
||||||
function save() {
|
|
||||||
const params = {
|
|
||||||
name: name,
|
|
||||||
description: description,
|
|
||||||
bannerId: bannerId,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (props.channelId) {
|
|
||||||
params.channelId = props.channelId;
|
|
||||||
os.api("channels/update", params).then(() => {
|
|
||||||
os.success();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
os.api("channels/create", params).then((created) => {
|
|
||||||
os.success();
|
|
||||||
router.push(`/channels/${created.id}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setBannerImage(evt) {
|
|
||||||
selectFile(evt.currentTarget ?? evt.target, null).then((file) => {
|
|
||||||
bannerId = file.id;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeBannerImage() {
|
|
||||||
bannerId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const headerActions = $computed(() => []);
|
|
||||||
|
|
||||||
const headerTabs = $computed(() => []);
|
|
||||||
|
|
||||||
definePageMetadata(
|
|
||||||
computed(() =>
|
|
||||||
props.channelId
|
|
||||||
? {
|
|
||||||
title: i18n.ts._channel.edit,
|
|
||||||
icon: "ph-television ph-bold ph-lg",
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
title: i18n.ts._channel.create,
|
|
||||||
icon: "ph-television ph-bold ph-lg",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
|
|
@ -1,262 +0,0 @@
|
||||||
<template>
|
|
||||||
<MkStickyContainer>
|
|
||||||
<template #header
|
|
||||||
><MkPageHeader
|
|
||||||
:actions="headerActions"
|
|
||||||
:tabs="headerTabs"
|
|
||||||
:display-back-button="true"
|
|
||||||
/></template>
|
|
||||||
<MkSpacer :content-max="700">
|
|
||||||
<div v-if="channel">
|
|
||||||
<div
|
|
||||||
class="wpgynlbz _panel _gap"
|
|
||||||
:class="{ hide: !showBanner }"
|
|
||||||
>
|
|
||||||
<XChannelFollowButton
|
|
||||||
:channel="channel"
|
|
||||||
:full="true"
|
|
||||||
class="subscribe"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="_button toggle"
|
|
||||||
@click="() => (showBanner = !showBanner)"
|
|
||||||
>
|
|
||||||
<template v-if="showBanner"
|
|
||||||
><i class="ph-caret-up ph-bold ph-lg"></i
|
|
||||||
></template>
|
|
||||||
<template v-else
|
|
||||||
><i class="ph-caret-down ph-bold ph-lg"></i
|
|
||||||
></template>
|
|
||||||
</button>
|
|
||||||
<div v-if="!showBanner" class="hideOverlay"></div>
|
|
||||||
<div
|
|
||||||
:style="{
|
|
||||||
backgroundImage: channel.bannerUrl
|
|
||||||
? `url(${channel.bannerUrl})`
|
|
||||||
: null,
|
|
||||||
}"
|
|
||||||
class="banner"
|
|
||||||
>
|
|
||||||
<div class="status">
|
|
||||||
<div>
|
|
||||||
<i
|
|
||||||
class="ph-users ph-bold ph-lg ph-fw ph-lg"
|
|
||||||
></i
|
|
||||||
><I18n
|
|
||||||
:src="i18n.ts._channel.usersCount"
|
|
||||||
tag="span"
|
|
||||||
style="margin-left: 4px"
|
|
||||||
><template #n
|
|
||||||
><b>{{
|
|
||||||
channel.usersCount
|
|
||||||
}}</b></template
|
|
||||||
></I18n
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<i
|
|
||||||
class="ph-pencil ph-bold ph-lg ph-fw ph-lg"
|
|
||||||
></i
|
|
||||||
><I18n
|
|
||||||
:src="i18n.ts._channel.notesCount"
|
|
||||||
tag="span"
|
|
||||||
style="margin-left: 4px"
|
|
||||||
><template #n
|
|
||||||
><b>{{
|
|
||||||
channel.notesCount
|
|
||||||
}}</b></template
|
|
||||||
></I18n
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="fade"></div>
|
|
||||||
</div>
|
|
||||||
<div v-if="channel.description" class="description">
|
|
||||||
<Mfm
|
|
||||||
:text="channel.description"
|
|
||||||
:is-note="false"
|
|
||||||
:i="$i"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<XPostForm
|
|
||||||
v-if="$i"
|
|
||||||
:channel="channel"
|
|
||||||
class="post-form _panel _gap"
|
|
||||||
fixed
|
|
||||||
/>
|
|
||||||
|
|
||||||
<XTimeline
|
|
||||||
:key="channelId"
|
|
||||||
class="_gap"
|
|
||||||
src="channel"
|
|
||||||
:channel="channelId"
|
|
||||||
@before="before"
|
|
||||||
@after="after"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</MkSpacer>
|
|
||||||
</MkStickyContainer>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, inject, watch } from "vue";
|
|
||||||
import MkContainer from "@/components/MkContainer.vue";
|
|
||||||
import XPostForm from "@/components/MkPostForm.vue";
|
|
||||||
import XTimeline from "@/components/MkTimeline.vue";
|
|
||||||
import XChannelFollowButton from "@/components/MkChannelFollowButton.vue";
|
|
||||||
import * as os from "@/os";
|
|
||||||
import { useRouter } from "@/router";
|
|
||||||
import { $i } from "@/account";
|
|
||||||
import { i18n } from "@/i18n";
|
|
||||||
import { definePageMetadata } from "@/scripts/page-metadata";
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
channelId: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
let channel = $ref(null);
|
|
||||||
let showBanner = $ref(true);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.channelId,
|
|
||||||
async () => {
|
|
||||||
channel = await os.api("channels/show", {
|
|
||||||
channelId: props.channelId,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
function edit() {
|
|
||||||
router.push(`/channels/${channel?.id}/edit`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const headerActions = $computed(() => [
|
|
||||||
...(channel && channel?.userId === $i?.id
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
icon: "ph-gear-six ph-bold ph-lg",
|
|
||||||
text: i18n.ts.edit,
|
|
||||||
handler: edit,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const headerTabs = $computed(() => []);
|
|
||||||
|
|
||||||
definePageMetadata(
|
|
||||||
computed(() =>
|
|
||||||
channel
|
|
||||||
? {
|
|
||||||
title: channel.name,
|
|
||||||
icon: "ph-television ph-bold ph-lg",
|
|
||||||
}
|
|
||||||
: null
|
|
||||||
)
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.wpgynlbz {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
> .subscribe {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
top: 16px;
|
|
||||||
left: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .toggle {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 2;
|
|
||||||
top: 8px;
|
|
||||||
right: 8px;
|
|
||||||
font-size: 1.2em;
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
color: #fff;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
border-radius: 100%;
|
|
||||||
|
|
||||||
> i {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .banner {
|
|
||||||
position: relative;
|
|
||||||
height: 200px;
|
|
||||||
background-position: center;
|
|
||||||
background-size: cover;
|
|
||||||
|
|
||||||
> .fade {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 64px;
|
|
||||||
background: linear-gradient(0deg, var(--panel), var(--X15));
|
|
||||||
}
|
|
||||||
|
|
||||||
> .status {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
bottom: 16px;
|
|
||||||
right: 16px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-size: 80%;
|
|
||||||
background: rgba(0, 0, 0, 0.7);
|
|
||||||
border-radius: 6px;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .description {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .hideOverlay {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
-webkit-backdrop-filter: var(--blur, blur(16px));
|
|
||||||
backdrop-filter: var(--blur, blur(16px));
|
|
||||||
background: rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.hide {
|
|
||||||
> .subscribe {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .toggle {
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 100%;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .banner {
|
|
||||||
height: 42px;
|
|
||||||
filter: blur(8px);
|
|
||||||
|
|
||||||
> * {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .description {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,238 +0,0 @@
|
||||||
<template>
|
|
||||||
<MkStickyContainer>
|
|
||||||
<template #header
|
|
||||||
><MkPageHeader
|
|
||||||
v-model:tab="tab"
|
|
||||||
:actions="headerActions"
|
|
||||||
:tabs="headerTabs"
|
|
||||||
/></template>
|
|
||||||
<MkSpacer :content-max="700">
|
|
||||||
<MkInfo class="_gap" :warn="true">{{
|
|
||||||
i18n.ts.channelFederationWarn
|
|
||||||
}}</MkInfo>
|
|
||||||
<swiper
|
|
||||||
:round-lengths="true"
|
|
||||||
:touch-angle="25"
|
|
||||||
:threshold="10"
|
|
||||||
:centeredSlides="true"
|
|
||||||
:modules="[Virtual]"
|
|
||||||
:space-between="20"
|
|
||||||
:virtual="true"
|
|
||||||
:allow-touch-move="
|
|
||||||
!(
|
|
||||||
deviceKind === 'desktop' &&
|
|
||||||
!defaultStore.state.swipeOnDesktop
|
|
||||||
)
|
|
||||||
"
|
|
||||||
@swiper="setSwiperRef"
|
|
||||||
@slide-change="onSlideChange"
|
|
||||||
>
|
|
||||||
<swiper-slide>
|
|
||||||
<div class="_content grwlizim search">
|
|
||||||
<MkInput
|
|
||||||
v-model="searchQuery"
|
|
||||||
:large="true"
|
|
||||||
:autofocus="true"
|
|
||||||
type="search"
|
|
||||||
>
|
|
||||||
<template #prefix
|
|
||||||
><i
|
|
||||||
class="ph-magnifying-glass ph-bold ph-lg"
|
|
||||||
></i
|
|
||||||
></template>
|
|
||||||
</MkInput>
|
|
||||||
<MkRadios
|
|
||||||
v-model="searchType"
|
|
||||||
@update:model-value="search()"
|
|
||||||
class="_gap"
|
|
||||||
>
|
|
||||||
<option value="nameAndDescription">
|
|
||||||
{{ i18n.ts._channel.nameAndDescription }}
|
|
||||||
</option>
|
|
||||||
<option value="nameOnly">
|
|
||||||
{{ i18n.ts._channel.nameOnly }}
|
|
||||||
</option>
|
|
||||||
</MkRadios>
|
|
||||||
<MkButton large primary @click="search" class="_gap">{{
|
|
||||||
i18n.ts.search
|
|
||||||
}}</MkButton>
|
|
||||||
<MkFoldableSection v-if="channelPagination">
|
|
||||||
<template #header>{{
|
|
||||||
i18n.ts.searchResult
|
|
||||||
}}</template>
|
|
||||||
<MkChannelList
|
|
||||||
:key="key"
|
|
||||||
:pagination="channelPagination"
|
|
||||||
/>
|
|
||||||
</MkFoldableSection>
|
|
||||||
</div>
|
|
||||||
</swiper-slide>
|
|
||||||
<swiper-slide>
|
|
||||||
<div class="_content grwlizim featured">
|
|
||||||
<!-- <MkPagination
|
|
||||||
v-slot="{ items }"
|
|
||||||
:pagination="featuredPagination"
|
|
||||||
:disable-auto-load="true"
|
|
||||||
>
|
|
||||||
<MkChannelPreview
|
|
||||||
v-for="channel in items"
|
|
||||||
:key="channel.id"
|
|
||||||
class="_gap"
|
|
||||||
:channel="channel"
|
|
||||||
/>
|
|
||||||
</MkPagination> -->
|
|
||||||
<MkChannelList
|
|
||||||
key="featured"
|
|
||||||
:pagination="featuredPagination"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</swiper-slide>
|
|
||||||
<swiper-slide>
|
|
||||||
<div class="_content grwlizim following">
|
|
||||||
<MkChannelList
|
|
||||||
key="following"
|
|
||||||
:pagination="followingPagination"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</swiper-slide>
|
|
||||||
<swiper-slide>
|
|
||||||
<div class="_content grwlizim owned">
|
|
||||||
<MkButton class="new" @click="create()"
|
|
||||||
><i class="ph-plus ph-bold ph-lg"></i
|
|
||||||
></MkButton>
|
|
||||||
<MkChannelList
|
|
||||||
key="owned"
|
|
||||||
:pagination="ownedPagination"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</swiper-slide>
|
|
||||||
</swiper>
|
|
||||||
</MkSpacer>
|
|
||||||
</MkStickyContainer>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, onMounted, defineComponent, inject, watch } from "vue";
|
|
||||||
import { Virtual } from "swiper";
|
|
||||||
import { Swiper, SwiperSlide } from "swiper/vue";
|
|
||||||
import MkChannelPreview from "@/components/MkChannelPreview.vue";
|
|
||||||
import MkChannelList from "@/components/MkChannelList.vue";
|
|
||||||
import MkPagination from "@/components/MkPagination.vue";
|
|
||||||
import MkInput from "@/components/form/input.vue";
|
|
||||||
import MkRadios from "@/components/form/radios.vue";
|
|
||||||
import MkButton from "@/components/MkButton.vue";
|
|
||||||
import MkFolder from "@/components/MkFolder.vue";
|
|
||||||
import MkInfo from "@/components/MkInfo.vue";
|
|
||||||
import { useRouter } from "@/router";
|
|
||||||
import { definePageMetadata } from "@/scripts/page-metadata";
|
|
||||||
import { deviceKind } from "@/scripts/device-kind";
|
|
||||||
import { i18n } from "@/i18n";
|
|
||||||
import { defaultStore } from "@/store";
|
|
||||||
import "swiper/scss";
|
|
||||||
import "swiper/scss/virtual";
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const tabs = ["search", "featured", "following", "owned"];
|
|
||||||
let tab = $ref(tabs[1]);
|
|
||||||
watch($$(tab), () => syncSlide(tabs.indexOf(tab)));
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
query: string;
|
|
||||||
type?: string;
|
|
||||||
}>();
|
|
||||||
let key = $ref("");
|
|
||||||
let searchQuery = $ref("");
|
|
||||||
let searchType = $ref("nameAndDescription");
|
|
||||||
let channelPagination = $ref();
|
|
||||||
onMounted(() => {
|
|
||||||
searchQuery = props.query ?? "";
|
|
||||||
searchType = props.type ?? "nameAndDescription";
|
|
||||||
});
|
|
||||||
|
|
||||||
const featuredPagination = {
|
|
||||||
endpoint: "channels/featured" as const,
|
|
||||||
limit: 10,
|
|
||||||
noPaging: false,
|
|
||||||
};
|
|
||||||
const followingPagination = {
|
|
||||||
endpoint: "channels/followed" as const,
|
|
||||||
limit: 10,
|
|
||||||
};
|
|
||||||
const ownedPagination = {
|
|
||||||
endpoint: "channels/owned" as const,
|
|
||||||
limit: 10,
|
|
||||||
};
|
|
||||||
|
|
||||||
async function search() {
|
|
||||||
const query = searchQuery.toString().trim();
|
|
||||||
if (query == null || query === "") return;
|
|
||||||
const type = searchType.toString().trim();
|
|
||||||
channelPagination = {
|
|
||||||
endpoint: "channels/search",
|
|
||||||
limit: 10,
|
|
||||||
params: {
|
|
||||||
query: searchQuery,
|
|
||||||
type: type,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
key = query + type;
|
|
||||||
}
|
|
||||||
|
|
||||||
function create() {
|
|
||||||
router.push("/channels/new");
|
|
||||||
}
|
|
||||||
|
|
||||||
const headerActions = $computed(() => [
|
|
||||||
{
|
|
||||||
icon: "ph-plus ph-bold ph-lg",
|
|
||||||
text: i18n.ts.create,
|
|
||||||
handler: create,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const headerTabs = $computed(() => [
|
|
||||||
{
|
|
||||||
key: "search",
|
|
||||||
title: i18n.ts.search,
|
|
||||||
icon: "ph-magnifying-glass ph-bold ph-lg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "featured",
|
|
||||||
title: i18n.ts._channel.featured,
|
|
||||||
icon: "ph-fire-simple ph-bold ph-lg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "following",
|
|
||||||
title: i18n.ts._channel.following,
|
|
||||||
icon: "ph-heart ph-bold ph-lg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "owned",
|
|
||||||
title: i18n.ts._channel.owned,
|
|
||||||
icon: "ph-crown-simple ph-bold ph-lg",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
definePageMetadata(
|
|
||||||
computed(() => ({
|
|
||||||
title: i18n.ts.channel,
|
|
||||||
icon: "ph-television ph-bold ph-lg",
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
let swiperRef = null;
|
|
||||||
|
|
||||||
function setSwiperRef(swiper) {
|
|
||||||
swiperRef = swiper;
|
|
||||||
syncSlide(tabs.indexOf(tab));
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSlideChange() {
|
|
||||||
tab = tabs[swiperRef.activeIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncSlide(index) {
|
|
||||||
swiperRef.slideTo(index);
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -54,16 +54,14 @@ import "swiper/scss";
|
||||||
import "swiper/scss/virtual";
|
import "swiper/scss/virtual";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
query: string;
|
query: string
|
||||||
channel?: string;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const notesPagination = {
|
const notesPagination = {
|
||||||
endpoint: "notes/search" as const,
|
endpoint: "notes/search" as const,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
params: computed(() => ({
|
params: computed(() => ({
|
||||||
query: props.query,
|
query: props.query
|
||||||
channelId: props.channel,
|
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -85,7 +85,6 @@
|
||||||
<nav class="nav">
|
<nav class="nav">
|
||||||
<MkA to="/announcements">{{ i18n.ts.announcements }}</MkA>
|
<MkA to="/announcements">{{ i18n.ts.announcements }}</MkA>
|
||||||
<MkA to="/explore">{{ i18n.ts.explore }}</MkA>
|
<MkA to="/explore">{{ i18n.ts.explore }}</MkA>
|
||||||
<MkA to="/channels">{{ i18n.ts.channel }}</MkA>
|
|
||||||
<MkA to="/featured">{{ i18n.ts.featured }}</MkA>
|
<MkA to="/featured">{{ i18n.ts.featured }}</MkA>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -321,8 +321,7 @@ export const routes = [
|
||||||
path: "/search",
|
path: "/search",
|
||||||
component: page(() => import("./pages/search.vue")),
|
component: page(() => import("./pages/search.vue")),
|
||||||
query: {
|
query: {
|
||||||
q: "query",
|
q: "query"
|
||||||
channel: "channel",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -402,24 +401,6 @@ export const routes = [
|
||||||
path: "/gallery",
|
path: "/gallery",
|
||||||
component: page(() => import("./pages/gallery/index.vue")),
|
component: page(() => import("./pages/gallery/index.vue")),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/channels/:channelId/edit",
|
|
||||||
component: page(() => import("./pages/channel-editor.vue")),
|
|
||||||
loginRequired: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/channels/new",
|
|
||||||
component: page(() => import("./pages/channel-editor.vue")),
|
|
||||||
loginRequired: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/channels/:channelId",
|
|
||||||
component: page(() => import("./pages/channel.vue")),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/channels",
|
|
||||||
component: page(() => import("./pages/channels.vue")),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/registry/keys/system/:path(*)?",
|
path: "/registry/keys/system/:path(*)?",
|
||||||
component: page(() => import("./pages/registry.keys.vue")),
|
component: page(() => import("./pages/registry.keys.vue")),
|
||||||
|
|
|
@ -56,7 +56,6 @@ export function getNoteMenu(props: {
|
||||||
initialNote: appearNote,
|
initialNote: appearNote,
|
||||||
renote: appearNote.renote,
|
renote: appearNote.renote,
|
||||||
reply: appearNote.reply,
|
reply: appearNote.reply,
|
||||||
channel: appearNote.channel,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -66,7 +65,6 @@ export function getNoteMenu(props: {
|
||||||
initialNote: appearNote,
|
initialNote: appearNote,
|
||||||
renote: appearNote.renote,
|
renote: appearNote.renote,
|
||||||
reply: appearNote.reply,
|
reply: appearNote.reply,
|
||||||
channel: appearNote.channel,
|
|
||||||
editId: appearNote.id,
|
editId: appearNote.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,6 @@ const menuOptions = [
|
||||||
"followRequests",
|
"followRequests",
|
||||||
"explore",
|
"explore",
|
||||||
"favorites",
|
"favorites",
|
||||||
"channels",
|
|
||||||
"search",
|
"search",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -248,7 +248,6 @@ const addColumn = async (ev) => {
|
||||||
"tl",
|
"tl",
|
||||||
"antenna",
|
"antenna",
|
||||||
"list",
|
"list",
|
||||||
"channel",
|
|
||||||
"mentions",
|
"mentions",
|
||||||
"direct",
|
"direct",
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,73 +0,0 @@
|
||||||
<template>
|
|
||||||
<XColumn
|
|
||||||
:menu="menu"
|
|
||||||
:column="column"
|
|
||||||
:is-stacked="isStacked"
|
|
||||||
@parent-focus="($event) => emit('parent-focus', $event)"
|
|
||||||
>
|
|
||||||
<template #header>
|
|
||||||
<i class="ph-television ph-bold ph-lg"></i
|
|
||||||
><span style="margin-left: 8px">{{ column.name }}</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<XTimeline
|
|
||||||
v-if="column.channelId"
|
|
||||||
ref="timeline"
|
|
||||||
src="channel"
|
|
||||||
:channel="column.channelId"
|
|
||||||
@after="() => emit('loaded')"
|
|
||||||
/>
|
|
||||||
</XColumn>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import {} from "vue";
|
|
||||||
import XColumn from "./column.vue";
|
|
||||||
import { updateColumn, Column } from "./deck-store";
|
|
||||||
import XTimeline from "@/components/MkTimeline.vue";
|
|
||||||
import * as os from "@/os";
|
|
||||||
import { i18n } from "@/i18n";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
column: Column;
|
|
||||||
isStacked: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(ev: "loaded"): void;
|
|
||||||
(ev: "parent-focus", direction: "up" | "down" | "left" | "right"): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
let timeline = $ref<InstanceType<typeof XTimeline>>();
|
|
||||||
|
|
||||||
if (props.column.channelId == null) {
|
|
||||||
setChannel();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setChannel() {
|
|
||||||
const channels = await os.api("channels/followed");
|
|
||||||
const { canceled, result: channel } = await os.select({
|
|
||||||
title: i18n.ts.selectChannel,
|
|
||||||
items: channels.map((x) => ({
|
|
||||||
value: x,
|
|
||||||
text: x.name,
|
|
||||||
})),
|
|
||||||
default: props.column.channelId,
|
|
||||||
});
|
|
||||||
if (canceled) return;
|
|
||||||
updateColumn(props.column.id, {
|
|
||||||
name: channel.name,
|
|
||||||
channelId: channel.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const menu = [
|
|
||||||
{
|
|
||||||
icon: "ph-pencil ph-bold ph-lg",
|
|
||||||
text: i18n.ts.selectChannel,
|
|
||||||
action: setChannel,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" module></style>
|
|
|
@ -49,12 +49,6 @@
|
||||||
:is-stacked="isStacked"
|
:is-stacked="isStacked"
|
||||||
@parent-focus="emit('parent-focus', $event)"
|
@parent-focus="emit('parent-focus', $event)"
|
||||||
/>
|
/>
|
||||||
<XChannelColumn
|
|
||||||
v-else-if="column.type === 'channel'"
|
|
||||||
:column="column"
|
|
||||||
:is-stacked="isStacked"
|
|
||||||
@parent-focus="emit('parent-focus', $event)"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
@ -67,7 +61,6 @@ import XNotificationsColumn from "./notifications-column.vue";
|
||||||
import XWidgetsColumn from "./widgets-column.vue";
|
import XWidgetsColumn from "./widgets-column.vue";
|
||||||
import XMentionsColumn from "./mentions-column.vue";
|
import XMentionsColumn from "./mentions-column.vue";
|
||||||
import XDirectColumn from "./direct-column.vue";
|
import XDirectColumn from "./direct-column.vue";
|
||||||
import XChannelColumn from "./channel-column.vue";
|
|
||||||
import { Column } from "./deck-store";
|
import { Column } from "./deck-store";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
|
|
@ -45,10 +45,6 @@
|
||||||
><i class="ph-compass ph-bold ph-lg icon"></i
|
><i class="ph-compass ph-bold ph-lg icon"></i
|
||||||
>{{ i18n.ts.explore }}</MkA
|
>{{ i18n.ts.explore }}</MkA
|
||||||
>
|
>
|
||||||
<MkA to="/channels" class="link" active-class="active"
|
|
||||||
><i class="ph-television ph-bold ph-lg icon"></i
|
|
||||||
>{{ i18n.ts.channel }}</MkA
|
|
||||||
>
|
|
||||||
<MkA to="/pages" class="link" active-class="active"
|
<MkA to="/pages" class="link" active-class="active"
|
||||||
><i class="ph-file-text ph-bold ph-lg icon"></i
|
><i class="ph-file-text ph-bold ph-lg icon"></i
|
||||||
>{{ i18n.ts.pages }}</MkA
|
>{{ i18n.ts.pages }}</MkA
|
||||||
|
|
|
@ -18,10 +18,6 @@
|
||||||
><i class="ph-compass ph-bold ph-lg icon"></i
|
><i class="ph-compass ph-bold ph-lg icon"></i
|
||||||
>{{ i18n.ts.explore }}</MkA
|
>{{ i18n.ts.explore }}</MkA
|
||||||
>
|
>
|
||||||
<MkA to="/channels" class="link" active-class="active"
|
|
||||||
><i class="ph-television ph-bold ph-lg icon"></i
|
|
||||||
>{{ i18n.ts.channel }}</MkA
|
|
||||||
>
|
|
||||||
<MkA to="/pages" class="link" active-class="active"
|
<MkA to="/pages" class="link" active-class="active"
|
||||||
><i class="ph-file-text ph-bold ph-lg icon"></i
|
><i class="ph-file-text ph-bold ph-lg icon"></i
|
||||||
>{{ i18n.ts.pages }}</MkA
|
>{{ i18n.ts.pages }}</MkA
|
||||||
|
|
|
@ -43,7 +43,6 @@
|
||||||
"gulp-replace": "1.1.4",
|
"gulp-replace": "1.1.4",
|
||||||
"gulp-terser": "2.1.0",
|
"gulp-terser": "2.1.0",
|
||||||
"install-peers": "^1.0.4",
|
"install-peers": "^1.0.4",
|
||||||
"rome": "^12.1.3",
|
|
||||||
"start-server-and-test": "1.15.2",
|
"start-server-and-test": "1.15.2",
|
||||||
"typescript": "4.9.4"
|
"typescript": "4.9.4"
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue