Compare commits

...

14 commits

Author SHA1 Message Date
dakkar
84fbfb8d6a merge: more timeline filters - #228 (!455)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/455
2024-03-14 14:48:45 +00:00
dakkar
58bc8f2c10 merge: always align code to the left - fixes #436 (!453)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/453

Closes #436

Approved-by: Essem <smswessem@gmail.com>
Approved-by: Leah <kevinlukej@gmail.com>
2024-03-14 14:48:30 +00:00
dakkar
94aed953b5 merge: make cookie a bit more secure - fixes #445 (!468)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/468

Closes #445

Approved-by: Luna <her@mint.lgbt>
Approved-by: Amelia Yukii <amelia.yukii@shourai.de>
2024-03-14 14:47:38 +00:00
dakkar
aa7035a35a merge: longer statement_timeout for migrations - fixes 450 (!466)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/466

Approved-by: Luna <her@mint.lgbt>
Approved-by: Amelia Yukii <amelia.yukii@shourai.de>
2024-03-14 14:46:42 +00:00
dakkar
45eab01fc4 merge: hide CW-ed featured notes on welcome page - fixes #458 (!467)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/467

Closes #458

Approved-by: Amelia Yukii <amelia.yukii@shourai.de>
Approved-by: Leah <kevinlukej@gmail.com>
Approved-by: Marie <marie@kaifa.ch>
2024-03-14 14:45:53 +00:00
Marie
71bcd76cc5 merge: Update IMPORTANT_NOTES.md (!470)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/470

Approved-by: Amelia Yukii <amelia.yukii@shourai.de>
Approved-by: Marie <marie@kaifa.ch>
Approved-by: dakkar <dakkar@thenautilus.net>
2024-03-14 11:53:15 +00:00
Dalek
cdb82c0ade Update IMPORTANT_NOTES.md 2024-03-13 00:17:57 +00:00
dakkar
6826e43ad7 make cookie a bit more secure - fixes #445
We can't make the cookie `HttpOnly` because we're setting it from
Javascript, but I'm not sure it's worth the trouble to redesign that:
`JSON.parse(localStorage.account).token` gives you the token anyway,
hiding the cookie from JS won't offer much protection.

At least we can mark is `Secure` (meaning, only send it over HTTPS)
and _delete it on logout_ (it wasn't!)
2024-03-10 10:26:04 +00:00
dakkar
ff189b1952 hide CW-ed featured notes on welcome page - fixes #458
not the most elegant solution, but simple and robust
2024-03-10 10:13:35 +00:00
dakkar
43544a6479 longer statement_timeout for migrations - fixes 450 2024-03-09 15:38:36 +00:00
dakkar
2071e72b2b use all filters to dbFallback for channel timeline 2024-03-04 20:48:27 +00:00
dakkar
3bb8a91124 add boost / files filters to antenna timeline - #228 2024-03-03 14:03:21 +00:00
dakkar
84abe50f84 add boost / files filters to channel timeline - #228 2024-03-03 13:33:32 +00:00
dakkar
03464cc379 always align code to the left - fixes #436
"featured notes" on the welcome page's right-hand column are shown
with the text right-aligned; code should not be affected by that. This
makes sure it isn't
2024-03-03 12:06:22 +00:00
12 changed files with 162 additions and 16 deletions

View file

@ -6,8 +6,11 @@ When using a service with Sharkey, there are several important points to keep in
2. Even for posts made in private, there is no guarantee that the recipient's server will treat them as private in the same way. Please exercise caution when posting personal or confidential information. (Again, this applies to the internet in general.)
3. Account deletion can be a resource-intensive process and may take a long time. In cases with a lot of uploaded data, it may even be impossible to delete an account.
3. The "Drive" feature is NOT secure cloud storage. This feature exists for easier managing of your uploaded files.
Any data uploaded, whether shared via post or not, will be publicly accessible. Please use 3rd party cloud storage providers if you need to upload data with sensitive information of any kind.
4. Please disable ad blockers. Some servers may rely on advertising revenue to cover operating costs. Additionally, ad blockers can mistakenly block content and features unrelated to ads, potentially causing issues with the client's functionality and preventing normal use of Sharkey. Therefore, we recommend turning off ad blockers and similar features when using Sharkey.
4. Account deletion can be a resource-intensive process and may take a long time. In cases with a lot of uploaded data, it may even be impossible to delete an account.
Please understand these points and enjoy using the service.
5. Please disable ad blockers. Some servers may rely on advertising revenue to cover operating costs. Additionally, ad blockers can mistakenly block content and features unrelated to ads, potentially causing issues with the client's functionality and preventing normal use of Sharkey. Therefore, we recommend turning off ad blockers and similar features when using Sharkey.
Please understand these points and enjoy using the service.

View file

@ -11,7 +11,11 @@ export default new DataSource({
username: config.db.user,
password: config.db.pass,
database: config.db.db,
extra: config.db.extra,
extra: {
...config.db.extra,
// migrations may be very slow, give them longer to run (that 10*1000 comes from postgres.ts)
statement_timeout: (config.db.extra?.statement_timeout ?? 1000 * 10) * 10,
},
entities: entities,
migrations: ['migration/*.js'],
});

View file

@ -51,6 +51,12 @@ export const paramDef = {
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default
withRenotes: { type: 'boolean', default: true },
withFiles: {
type: 'boolean',
default: false,
description: 'Only show notes that have attached files.',
},
},
required: ['channelId'],
} as const;
@ -89,7 +95,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (me) this.activeUsersChart.read(me);
if (!serverSettings.enableFanoutTimeline) {
return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id }, me), me);
return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id, withFiles: ps.withFiles, withRenotes: ps.withRenotes }, me), me);
}
return await this.fanoutTimelineEndpointService.timeline({
@ -100,9 +106,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
me,
useDbFallback: true,
redisTimelines: [`channelTimeline:${channel.id}`],
excludePureRenotes: false,
excludePureRenotes: !ps.withRenotes,
excludeNoFiles: ps.withFiles,
dbFallback: async (untilId, sinceId, limit) => {
return await this.getFromDb({ untilId, sinceId, limit, channelId: channel.id }, me);
return await this.getFromDb({ untilId, sinceId, limit, channelId: channel.id, withFiles: ps.withFiles, withRenotes: ps.withRenotes }, me);
},
});
});
@ -112,7 +119,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
untilId: string | null,
sinceId: string | null,
limit: number,
channelId: string
channelId: string,
withFiles: boolean,
withRenotes: boolean,
}, me: MiLocalUser | null) {
//#region fallback to database
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
@ -128,6 +137,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
}
if (ps.withRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteId IS NULL');
qb.orWhere(new Brackets(qb => {
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
}));
}));
}
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
}
//#endregion
return await query.limit(ps.limit).getMany();

View file

@ -15,6 +15,8 @@ class ChannelChannel extends Channel {
public static shouldShare = false;
public static requireCredential = false as const;
private channelId: string;
private withFiles: boolean;
private withRenotes: boolean;
constructor(
private noteEntityService: NoteEntityService,
@ -29,6 +31,8 @@ class ChannelChannel extends Channel {
@bindThis
public async init(params: any) {
this.channelId = params.channelId as string;
this.withFiles = params.withFiles ?? false;
this.withRenotes = params.withRenotes ?? true;
// Subscribe stream
this.subscriber.on('notesStream', this.onNote);
@ -38,6 +42,10 @@ class ChannelChannel extends Channel {
private async onNote(note: Packed<'Note'>) {
if (note.channelId !== this.channelId) return;
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する

View file

@ -43,6 +43,7 @@ export async function signout() {
waiting();
miLocalStorage.removeItem('account');
await removeAccount($i.id);
document.cookie = `token=; path=/; max-age=0${ location.protocol === 'https:' ? '; Secure' : ''}`;
const accounts = await getAccounts();
//#region Remove service worker registration
@ -200,7 +201,7 @@ export async function login(token: Account['token'], redirect?: string) {
throw reason;
});
miLocalStorage.setItem('account', JSON.stringify(me));
document.cookie = `token=${token}; path=/; max-age=31536000`; // bull dashboardの認証とかで使う
document.cookie = `token=${token}; path=/; max-age=31536000${ location.protocol === 'https:' ? '; Secure' : ''}`; // bull dashboardの認証とかで使う
await addAccount(me.id, token);
if (redirect) {

View file

@ -72,6 +72,10 @@ watch(() => props.lang, (to) => {
</script>
<style module lang="scss">
.codeBlockRoot {
text-align: left;
}
.codeBlockRoot :global(.shiki) > code {
counter-reset: step;
counter-increment: step 0;

View file

@ -154,6 +154,8 @@ function connectChannel() {
} else if (props.src === 'channel') {
if (props.channel == null) return;
connection = stream.useChannel('channel', {
withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined,
channelId: props.channel,
});
} else if (props.src === 'role') {
@ -234,6 +236,8 @@ function updatePaginationQuery() {
} else if (props.src === 'channel') {
endpoint = 'channels/timeline';
query = {
withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined,
channelId: props.channel,
};
} else if (props.src === 'role') {

View file

@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<!-- スマホタブレットの場合キーボードが表示されると投稿が見づらくなるのでデスクトップ場合のみ自動でフォーカスを当てる -->
<MkPostForm v-if="$i && defaultStore.reactiveState.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/>
<MkTimeline :key="channelId" src="channel" :channel="channelId" @before="before" @after="after" @note="miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.id}`, Date.now())"/>
<MkTimeline :key="channelId + withRenotes + onlyFiles" src="channel" :channel="channelId" :withRenotes="withRenotes" :onlyFiles="onlyFiles" @before="before" @after="after" @note="miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.id}`, Date.now())"/>
</div>
<div v-else-if="tab === 'featured'" key="featured">
<MkNotes :pagination="featuredPagination"/>
@ -95,6 +95,7 @@ import { isSupportShare } from '@/scripts/navigator.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { miLocalStorage } from '@/local-storage.js';
import { useRouter } from '@/router/supplier.js';
import { deepMerge } from '@/scripts/merge.js';
const router = useRouter();
@ -116,6 +117,15 @@ const featuredPagination = computed(() => ({
channelId: props.channelId,
},
}));
const withRenotes = computed<boolean>({
get: () => defaultStore.reactiveState.tl.value.filter.withRenotes,
set: (x) => saveTlFilter('withRenotes', x),
});
const onlyFiles = computed<boolean>({
get: () => defaultStore.reactiveState.tl.value.filter.onlyFiles,
set: (x) => saveTlFilter('onlyFiles', x),
});
watch(() => props.channelId, async () => {
channel.value = await misskeyApi('channels/show', {
@ -136,6 +146,13 @@ watch(() => props.channelId, async () => {
}
}, { immediate: true });
function saveTlFilter(key: keyof typeof defaultStore.state.tl.filter, newValue: boolean) {
if (key !== 'withReplies' || $i) {
const out = deepMerge({ filter: { [key]: newValue } }, defaultStore.state.tl);
defaultStore.set('tl', out);
}
}
function edit() {
router.push(`/channels/${channel.value?.id}/edit`);
}
@ -192,7 +209,21 @@ async function search() {
const headerActions = computed(() => {
if (channel.value && channel.value.userId) {
const headerItems: PageHeaderItem[] = [];
const headerItems: PageHeaderItem[] = [{
icon: 'ph-dots-three ph-bold ph-lg',
text: i18n.ts.options,
handler: (ev) => {
os.popupMenu([{
type: 'switch',
text: i18n.ts.showRenotes,
ref: withRenotes,
}, {
type: 'switch',
text: i18n.ts.fileAttachedOnly,
ref: onlyFiles,
}], ev.currentTarget ?? ev.target);
},
}];
headerItems.push({
icon: 'ph-share-network ph-bold ph-lg',

View file

@ -11,10 +11,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
<div :class="$style.tl">
<MkTimeline
ref="tlEl" :key="listId"
ref="tlEl" :key="listId + withRenotes + onlyFiles"
src="list"
:list="listId"
:sound="true"
:withRenotes="withRenotes"
:onlyFiles="onlyFiles"
@queue="queueUpdated"
/>
</div>
@ -32,6 +34,9 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
import { useRouter } from '@/router/supplier.js';
import { defaultStore } from '@/store.js';
import { deepMerge } from '@/scripts/merge.js';
import * as os from '@/os.js';
const router = useRouter();
@ -43,6 +48,21 @@ const list = ref<Misskey.entities.UserList | null>(null);
const queue = ref(0);
const tlEl = shallowRef<InstanceType<typeof MkTimeline>>();
const rootEl = shallowRef<HTMLElement>();
const withRenotes = computed<boolean>({
get: () => defaultStore.reactiveState.tl.value.filter.withRenotes,
set: (x) => saveTlFilter('withRenotes', x),
});
const onlyFiles = computed<boolean>({
get: () => defaultStore.reactiveState.tl.value.filter.onlyFiles,
set: (x) => saveTlFilter('onlyFiles', x),
});
function saveTlFilter(key: keyof typeof defaultStore.state.tl.filter, newValue: boolean) {
if (key !== 'withReplies' || $i) {
const out = deepMerge({ filter: { [key]: newValue } }, defaultStore.state.tl);
defaultStore.set('tl', out);
}
}
watch(() => props.listId, async () => {
list.value = await misskeyApi('users/lists/show', {
@ -63,6 +83,20 @@ function settings() {
}
const headerActions = computed(() => list.value ? [{
icon: 'ph-dots-three ph-bold ph-lg',
text: i18n.ts.options,
handler: (ev) => {
os.popupMenu([{
type: 'switch',
text: i18n.ts.showRenotes,
ref: withRenotes,
}, {
type: 'switch',
text: i18n.ts.fileAttachedOnly,
ref: onlyFiles,
}], ev.currentTarget ?? ev.target);
},
}, {
icon: 'ph-gear ph-bold ph-lg',
text: i18n.ts.settings,
handler: settings,

View file

@ -40,7 +40,7 @@ const isScrolling = ref(false);
const scrollEl = shallowRef<HTMLElement>();
misskeyApiGet('notes/featured').then(_notes => {
notes.value = _notes;
notes.value = _notes.filter(n => n.cw == null);
});
onUpdated(() => {

View file

@ -13,13 +13,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<div style="padding: 8px; text-align: center;">
<MkButton primary gradate rounded inline small @click="post"><i class="ph-pencil-simple ph-bold ph-lg"></i></MkButton>
</div>
<MkTimeline ref="timeline" src="channel" :channel="column.channelId"/>
<MkTimeline ref="timeline" src="channel" :channel="column.channelId" :key="column.channelId + column.withRenotes + column.onlyFiles" :withRenotes="withRenotes" :onlyFiles="onlyFiles"/>
</template>
</XColumn>
</template>
<script lang="ts" setup>
import { shallowRef } from 'vue';
import { watch, ref, shallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import XColumn from './column.vue';
import { updateColumn, Column } from './deck-store.js';
@ -36,6 +36,20 @@ const props = defineProps<{
const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
const channel = shallowRef<Misskey.entities.Channel>();
const withRenotes = ref(props.column.withRenotes ?? true);
const onlyFiles = ref(props.column.onlyFiles ?? false);
watch(withRenotes, v => {
updateColumn(props.column.id, {
withRenotes: v,
});
});
watch(onlyFiles, v => {
updateColumn(props.column.id, {
onlyFiles: v,
});
});
if (props.column.channelId == null) {
setChannel();
@ -75,5 +89,13 @@ const menu = [{
icon: 'ph-pencil-simple ph-bold ph-lg',
text: i18n.ts.selectChannel,
action: setChannel,
}, {
type: 'switch',
text: i18n.ts.showRenotes,
ref: withRenotes,
}, {
type: 'switch',
text: i18n.ts.fileAttachedOnly,
ref: onlyFiles,
}];
</script>

View file

@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ph-list ph-bold ph-lg"></i><span style="margin-left: 8px;">{{ column.name }}</span>
</template>
<MkTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" :withRenotes="withRenotes"/>
<MkTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" :key="column.listId + column.withRenotes + column.onlyFiles" :withRenotes="withRenotes" :onlyFiles="onlyFiles"/>
</XColumn>
</template>
@ -29,6 +29,7 @@ const props = defineProps<{
const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
const withRenotes = ref(props.column.withRenotes ?? true);
const onlyFiles = ref(props.column.onlyFiles ?? false);
if (props.column.listId == null) {
setList();
@ -40,6 +41,12 @@ watch(withRenotes, v => {
});
});
watch(onlyFiles, v => {
updateColumn(props.column.id, {
onlyFiles: v,
});
});
async function setList() {
const lists = await misskeyApi('users/lists/list');
const { canceled, result: list } = await os.select({
@ -75,5 +82,10 @@ const menu = [
text: i18n.ts.showRenotes,
ref: withRenotes,
},
{
type: 'switch',
text: i18n.ts.fileAttachedOnly,
ref: onlyFiles,
},
];
</script>