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",
|
||||
"api": "pnpm api-extractor run --local --verbose",
|
||||
"api-prod": "pnpm api-extractor run --verbose",
|
||||
"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"
|
||||
"api-doc": "pnpm api-documenter markdown -i ./etc/"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -25,13 +21,7 @@
|
|||
"@microsoft/api-documenter": "^7.22.21",
|
||||
"@swc/cli": "^0.1.62",
|
||||
"@swc/core": "^1.3.62",
|
||||
"@types/jest": "^27.4.0",
|
||||
"@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",
|
||||
"tsd": "^0.28.1",
|
||||
"typescript": "5.1.3"
|
||||
|
|
|
@ -54,7 +54,6 @@ type NoteSubmitReq = {
|
|||
fileIds?: DriveFile["id"][];
|
||||
replyId?: null | Note["id"];
|
||||
renoteId?: null | Note["id"];
|
||||
channelId?: null | Channel["id"];
|
||||
poll?: null | {
|
||||
choices: string[];
|
||||
multiple?: boolean;
|
||||
|
@ -192,18 +191,6 @@ export type Endpoints = {
|
|||
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/active-users": {
|
||||
req: { span: "day" | "hour"; limit?: number; offset?: number | null };
|
||||
|
|
|
@ -51,8 +51,6 @@ export const permissions = [
|
|||
"read:page-likes",
|
||||
"read:user-groups",
|
||||
"write:user-groups",
|
||||
"read:channels",
|
||||
"write:channels",
|
||||
"read:gallery",
|
||||
"write:gallery",
|
||||
"read:gallery-likes",
|
||||
|
|
|
@ -91,7 +91,6 @@ export type MeDetailed = UserDetailed & {
|
|||
hasPendingReceivedFollowRequest: boolean;
|
||||
hasUnreadAnnouncement: boolean;
|
||||
hasUnreadAntenna: boolean;
|
||||
hasUnreadChannel: boolean;
|
||||
hasUnreadMentions: boolean;
|
||||
hasUnreadMessagingMessage: boolean;
|
||||
hasUnreadNotification: boolean;
|
||||
|
@ -145,7 +144,6 @@ export type Note = {
|
|||
visibility: "public" | "home" | "followers" | "specified";
|
||||
visibleUserIds?: User["id"][];
|
||||
localOnly?: boolean;
|
||||
channel?: Channel["id"];
|
||||
myReaction?: string;
|
||||
reactions: Record<string, number>;
|
||||
renoteCount: number;
|
||||
|
@ -420,11 +418,6 @@ export type FollowRequest = {
|
|||
followee: User;
|
||||
};
|
||||
|
||||
export type Channel = {
|
||||
id: ID;
|
||||
// TODO
|
||||
};
|
||||
|
||||
export type Following = {
|
||||
id: ID;
|
||||
createdAt: DateString;
|
||||
|
|
|
@ -39,8 +39,6 @@ export type Channels = {
|
|||
readAllAntennas: () => void;
|
||||
unreadAntenna: (payload: Antenna) => void;
|
||||
readAllAnnouncements: () => void;
|
||||
readAllChannels: () => void;
|
||||
unreadChannel: (payload: Note["id"]) => void;
|
||||
myTokenRegenerated: () => void;
|
||||
reversiNoInvites: () => 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
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "test/**/*"]
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
"scripts": {
|
||||
"watch": "pnpm vite build --watch --mode development",
|
||||
"build": "pnpm vite build",
|
||||
"lint": "pnpm rome check \"src/**/*.{ts,vue}\"",
|
||||
"format": "pnpm prettier --write '**/*.vue'"
|
||||
},
|
||||
"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)">
|
||||
<MkTime :time="appearNote.createdAt" mode="absolute" />
|
||||
</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>
|
||||
<footer ref="footerEl" class="footer" @click.stop tabindex="-1">
|
||||
<XReactionsViewer
|
||||
|
@ -288,8 +280,6 @@ const props = defineProps<{
|
|||
collapsedReply?: boolean;
|
||||
}>();
|
||||
|
||||
const inChannel = inject("inChannel", null);
|
||||
|
||||
let note = $ref(deepClone(props.note));
|
||||
|
||||
const softMuteReasonI18nSrc = (what?: string) => {
|
||||
|
|
|
@ -35,7 +35,6 @@
|
|||
ref="visibilityButton"
|
||||
v-tooltip="i18n.ts.visibility"
|
||||
class="_button visibility"
|
||||
:disabled="channel != null"
|
||||
@click="setVisibility"
|
||||
>
|
||||
<span v-if="visibility === 'public'"
|
||||
|
@ -265,7 +264,6 @@ const props = withDefaults(
|
|||
defineProps<{
|
||||
reply?: misskey.entities.Note;
|
||||
renote?: misskey.entities.Note;
|
||||
channel?: any; // TODO
|
||||
mention?: misskey.entities.User;
|
||||
specified?: misskey.entities.User;
|
||||
initialText?: string;
|
||||
|
@ -333,18 +331,12 @@ let hasNotSpecifiedMentions = $ref(false);
|
|||
let recentHashtags = $ref(JSON.parse(localStorage.getItem("hashtags") || "[]"));
|
||||
let imeText = $ref("");
|
||||
|
||||
const typing = throttle(3000, () => {
|
||||
if (props.channel) {
|
||||
stream.send("typingOnChannel", { channel: props.channel.id });
|
||||
}
|
||||
});
|
||||
|
||||
const draftKey = $computed((): string => {
|
||||
if (props.editId) {
|
||||
return `edit:${props.editId}`;
|
||||
}
|
||||
|
||||
let key = props.channel ? `channel:${props.channel.id}` : "";
|
||||
let key = "";
|
||||
|
||||
if (props.renote) {
|
||||
key += `renote:${props.renote.id}`;
|
||||
|
@ -362,8 +354,6 @@ const placeholder = $computed((): string => {
|
|||
return i18n.ts._postForm.quotePlaceholder;
|
||||
} else if (props.reply) {
|
||||
return i18n.ts._postForm.replyPlaceholder;
|
||||
} else if (props.channel) {
|
||||
return i18n.ts._postForm.channelPlaceholder;
|
||||
} else {
|
||||
const xs = [
|
||||
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 (
|
||||
props.reply &&
|
||||
|
@ -622,11 +607,6 @@ function upload(file: File, name?: string) {
|
|||
}
|
||||
|
||||
function setVisibility() {
|
||||
if (props.channel) {
|
||||
// TODO: information dialog
|
||||
return;
|
||||
}
|
||||
|
||||
os.popup(
|
||||
defineAsyncComponent(
|
||||
() => import("@/components/MkVisibilityPicker.vue")
|
||||
|
@ -830,7 +810,6 @@ async function post() {
|
|||
: quoteId
|
||||
? quoteId
|
||||
: undefined,
|
||||
channelId: props.channel ? props.channel.id : undefined,
|
||||
poll: poll,
|
||||
cw: useCw ? cw || "" : undefined,
|
||||
localOnly: localOnly,
|
||||
|
|
|
@ -27,7 +27,6 @@ import MkPostForm from "@/components/MkPostForm.vue";
|
|||
const props = defineProps<{
|
||||
reply?: misskey.entities.Note;
|
||||
renote?: misskey.entities.Note;
|
||||
channel?: any; // TODO
|
||||
mention?: misskey.entities.User;
|
||||
specified?: misskey.entities.User;
|
||||
initialText?: string;
|
||||
|
|
|
@ -32,7 +32,6 @@ const props = defineProps<{
|
|||
src: string;
|
||||
list?: string;
|
||||
antenna?: string;
|
||||
channel?: string;
|
||||
sound?: boolean;
|
||||
}>();
|
||||
|
||||
|
@ -41,11 +40,6 @@ const emit = defineEmits<{
|
|||
(ev: "queue", count: number): void;
|
||||
}>();
|
||||
|
||||
provide(
|
||||
"inChannel",
|
||||
computed(() => props.src === "channel")
|
||||
);
|
||||
|
||||
const tlComponent: InstanceType<typeof XNotes> = $ref();
|
||||
|
||||
const prepend = (note) => {
|
||||
|
@ -180,15 +174,6 @@ if (props.src === "antenna") {
|
|||
connection.on("note", prepend);
|
||||
connection.on("userAdded", onUserAdded);
|
||||
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() {
|
||||
|
|
|
@ -465,15 +465,6 @@ function checkForSplash() {
|
|||
updateAccount({ hasUnreadAnnouncement: false });
|
||||
});
|
||||
|
||||
main.on("readAllChannels", () => {
|
||||
updateAccount({ hasUnreadChannel: false });
|
||||
});
|
||||
|
||||
main.on("unreadChannel", () => {
|
||||
updateAccount({ hasUnreadChannel: true });
|
||||
sound.play("channel");
|
||||
});
|
||||
|
||||
// トークンが再生成されたとき
|
||||
// このままではMisskeyが利用できないので強制的にサインアウトさせる
|
||||
main.on("myTokenRegenerated", () => {
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
import { computed, ref, reactive } from "vue";
|
||||
import { computed, reactive } from "vue";
|
||||
import { $i } from "./account";
|
||||
import { search } from "@/scripts/search";
|
||||
import * as os from "@/os";
|
||||
import { i18n } from "@/i18n";
|
||||
import { ui } from "@/config";
|
||||
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({
|
||||
notifications: {
|
||||
|
@ -89,11 +85,6 @@ export const navbarItemDef = reactive({
|
|||
show: computed(() => $i != null),
|
||||
to: "/my/clips",
|
||||
},
|
||||
channels: {
|
||||
title: "channel",
|
||||
icon: "ph-television ph-bold ph-lg",
|
||||
to: "/channels",
|
||||
},
|
||||
groups: {
|
||||
title: "groups",
|
||||
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";
|
||||
|
||||
const props = defineProps<{
|
||||
query: string;
|
||||
channel?: string;
|
||||
query: string
|
||||
}>();
|
||||
|
||||
const notesPagination = {
|
||||
endpoint: "notes/search" as const,
|
||||
limit: 10,
|
||||
params: computed(() => ({
|
||||
query: props.query,
|
||||
channelId: props.channel,
|
||||
query: props.query
|
||||
})),
|
||||
};
|
||||
|
||||
|
|
|
@ -85,7 +85,6 @@
|
|||
<nav class="nav">
|
||||
<MkA to="/announcements">{{ i18n.ts.announcements }}</MkA>
|
||||
<MkA to="/explore">{{ i18n.ts.explore }}</MkA>
|
||||
<MkA to="/channels">{{ i18n.ts.channel }}</MkA>
|
||||
<MkA to="/featured">{{ i18n.ts.featured }}</MkA>
|
||||
</nav>
|
||||
</div>
|
||||
|
|
|
@ -321,8 +321,7 @@ export const routes = [
|
|||
path: "/search",
|
||||
component: page(() => import("./pages/search.vue")),
|
||||
query: {
|
||||
q: "query",
|
||||
channel: "channel",
|
||||
q: "query"
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -402,24 +401,6 @@ export const routes = [
|
|||
path: "/gallery",
|
||||
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(*)?",
|
||||
component: page(() => import("./pages/registry.keys.vue")),
|
||||
|
|
|
@ -56,7 +56,6 @@ export function getNoteMenu(props: {
|
|||
initialNote: appearNote,
|
||||
renote: appearNote.renote,
|
||||
reply: appearNote.reply,
|
||||
channel: appearNote.channel,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -66,7 +65,6 @@ export function getNoteMenu(props: {
|
|||
initialNote: appearNote,
|
||||
renote: appearNote.renote,
|
||||
reply: appearNote.reply,
|
||||
channel: appearNote.channel,
|
||||
editId: appearNote.id,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ const menuOptions = [
|
|||
"followRequests",
|
||||
"explore",
|
||||
"favorites",
|
||||
"channels",
|
||||
"search",
|
||||
];
|
||||
|
||||
|
|
|
@ -248,7 +248,6 @@ const addColumn = async (ev) => {
|
|||
"tl",
|
||||
"antenna",
|
||||
"list",
|
||||
"channel",
|
||||
"mentions",
|
||||
"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"
|
||||
@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>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@ -67,7 +61,6 @@ import XNotificationsColumn from "./notifications-column.vue";
|
|||
import XWidgetsColumn from "./widgets-column.vue";
|
||||
import XMentionsColumn from "./mentions-column.vue";
|
||||
import XDirectColumn from "./direct-column.vue";
|
||||
import XChannelColumn from "./channel-column.vue";
|
||||
import { Column } from "./deck-store";
|
||||
|
||||
defineProps<{
|
||||
|
|
|
@ -45,10 +45,6 @@
|
|||
><i class="ph-compass ph-bold ph-lg icon"></i
|
||||
>{{ 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"
|
||||
><i class="ph-file-text ph-bold ph-lg icon"></i
|
||||
>{{ i18n.ts.pages }}</MkA
|
||||
|
|
|
@ -18,10 +18,6 @@
|
|||
><i class="ph-compass ph-bold ph-lg icon"></i
|
||||
>{{ 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"
|
||||
><i class="ph-file-text ph-bold ph-lg icon"></i
|
||||
>{{ i18n.ts.pages }}</MkA
|
||||
|
|
|
@ -43,7 +43,6 @@
|
|||
"gulp-replace": "1.1.4",
|
||||
"gulp-terser": "2.1.0",
|
||||
"install-peers": "^1.0.4",
|
||||
"rome": "^12.1.3",
|
||||
"start-server-and-test": "1.15.2",
|
||||
"typescript": "4.9.4"
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue