Frontend: Removed channels
ci/woodpecker/push/ociImagePush Pipeline is pending Details

This commit is contained in:
Natty 2023-07-20 17:14:08 +02:00
parent c09188fa9c
commit e88e46508d
Signed by: natty
GPG Key ID: BF6CB659ADEE60EC
35 changed files with 10 additions and 3780 deletions

View File

@ -1,2 +0,0 @@
codecov:
token: d55e1270-f20a-4727-aa05-2bd57793315a

View File

@ -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,
};

View File

@ -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"

View File

@ -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 };

View File

@ -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",

View File

@ -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;

View File

@ -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;

View File

@ -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);
}
});
});

View File

@ -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: チャンネル接続が使いまわされるかのテスト
});

View File

@ -16,5 +16,5 @@
"esModuleInterop": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "test/**/*"]
"exclude": ["node_modules"]
}

View File

@ -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": {

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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) => {

View File

@ -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,

View File

@ -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;

View File

@ -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() {

View File

@ -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", () => {

View File

@ -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",

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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
})),
};

View File

@ -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>

View File

@ -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")),

View File

@ -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,
});
}

View File

@ -13,7 +13,6 @@ const menuOptions = [
"followRequests",
"explore",
"favorites",
"channels",
"search",
];

View File

@ -248,7 +248,6 @@ const addColumn = async (ev) => {
"tl",
"antenna",
"list",
"channel",
"mentions",
"direct",
];

View File

@ -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>

View File

@ -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<{

View File

@ -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

View File

@ -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

View File

@ -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