Compare commits
71 Commits
Author | SHA1 | Date | |
---|---|---|---|
ab4f97ff20 | |||
79640d6861 | |||
d305c7e401 | |||
8afaca36d9 | |||
9950fafff7 | |||
0d9857db63 | |||
10c932876d | |||
d5d7a527a0 | |||
9eb8595130 | |||
8c46e5b3d9 | |||
73bd877993 | |||
de6cbf8a22 | |||
89f618d732 | |||
4955df3911 | |||
6785f50a1f | |||
a549327170 | |||
1f018d87f2 | |||
769ee734fa | |||
6419185228 | |||
b77cbeca22 | |||
4cce10a7d7 | |||
1a6fd7d72e | |||
97ce939a73 | |||
3f2a7a561e | |||
332af15e3c | |||
fe184ce84a | |||
661c7f45ba | |||
01c0545409 | |||
c6492d3d58 | |||
638a2ab684 | |||
650f79d0fd | |||
a64817cea1 | |||
c32c3c1370 | |||
29ad7ab0cf | |||
11716fa9d3 | |||
210f11ffd8 | |||
814751d76a | |||
440cf139bb | |||
276d8ffc66 | |||
d9cdc1f079 | |||
414c600356 | |||
e37c19fdcd | |||
d69b919961 | |||
1311db8060 | |||
ed9e7520f1 | |||
8fe6da0cad | |||
b8eac630ed | |||
a5b9d7eb3b | |||
06c453c3bc | |||
97b7567770 | |||
34345ea8a7 | |||
fc166b7bee | |||
cf3112c7c0 | |||
e7dd74a443 | |||
b5acf15877 | |||
f62603fd9d | |||
d000b9e7cd | |||
0e1f858dfd | |||
bcf28282f6 | |||
abfbb068d7 | |||
9643a87320 | |||
b3817240b8 | |||
8d53f8639e | |||
f850283147 | |||
c512c07630 | |||
105623e398 | |||
6c75bc6d51 | |||
a068741d05 | |||
4e83106853 | |||
0b688a909e | |||
fabcad6db9 |
@ -52,9 +52,9 @@ If you want to translate Misskey, please see [Translation guide](./docs/translat
|
|||||||
[List of all contributors](https://github.com/syuilo/misskey/graphs/contributors)
|
[List of all contributors](https://github.com/syuilo/misskey/graphs/contributors)
|
||||||
|
|
||||||
### :earth_americas: Translators
|
### :earth_americas: Translators
|
||||||
| ![][mirro-san-icon] | ![][Conan-kun-icon] |
|
| ![][mirro-san-icon] | ![][Conan-kun-icon] | ![][m4sk1n-icon] |
|
||||||
|:-:|:-:|
|
|:-:|:-:|:-:|
|
||||||
| [Mirro][mirro-san-link]<br>English, French | [Asriel][Conan-kun-link]<br>English, French |
|
| [Mirro][mirro-san-link]<br>English, French | [Asriel][Conan-kun-link]<br>English, French | [Marcin Mikołajczak][m4sk1n-link]<br>Polish |
|
||||||
|
|
||||||
:four_leaf_clover: Copyright
|
:four_leaf_clover: Copyright
|
||||||
----------------------------------------------------------------
|
----------------------------------------------------------------
|
||||||
@ -100,4 +100,5 @@ Misskey is an open-source software licensed under [GNU AGPLv3](LICENSE).
|
|||||||
[mirro-san-icon]: https://avatars1.githubusercontent.com/u/17948612?s=70&v=4
|
[mirro-san-icon]: https://avatars1.githubusercontent.com/u/17948612?s=70&v=4
|
||||||
[Conan-kun-link]: https://github.com/Conan-kun
|
[Conan-kun-link]: https://github.com/Conan-kun
|
||||||
[Conan-kun-icon]: https://avatars3.githubusercontent.com/u/30003708?s=70&v=4
|
[Conan-kun-icon]: https://avatars3.githubusercontent.com/u/30003708?s=70&v=4
|
||||||
|
[m4sk1n-link]: https://github.com/m4sk1n
|
||||||
|
[m4sk1n-icon]: https://avatars3.githubusercontent.com/u/21127288?s=70&v=4
|
||||||
|
101
cli/clean-cached-remote-files.js
Normal file
101
cli/clean-cached-remote-files.js
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
const chalk = require('chalk');
|
||||||
|
const log = require('single-line-log').stdout;
|
||||||
|
const sequential = require('promise-sequential');
|
||||||
|
const { default: DriveFile, DriveFileChunk } = require('../built/models/drive-file');
|
||||||
|
const { default: DriveFileThumbnail, DriveFileThumbnailChunk } = require('../built/models/drive-file-thumbnail');
|
||||||
|
const { default: User } = require('../built/models/user');
|
||||||
|
|
||||||
|
const q = {
|
||||||
|
'metadata._user.host': {
|
||||||
|
$ne: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const promiseGens = [];
|
||||||
|
|
||||||
|
const count = await DriveFile.count(q);
|
||||||
|
|
||||||
|
let prev;
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
promiseGens.push(() => {
|
||||||
|
const promise = new Promise(async (res, rej) => {
|
||||||
|
const file = await DriveFile.findOne(prev ? Object.assign({
|
||||||
|
_id: { $lt: prev._id }
|
||||||
|
}, q) : q, {
|
||||||
|
sort: {
|
||||||
|
_id: -1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
prev = file;
|
||||||
|
|
||||||
|
function skip() {
|
||||||
|
res([i, file, false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file == null) return skip();
|
||||||
|
|
||||||
|
log(chalk`{gray ${i}} scanning {bold ${file._id}} ${file.filename} ...`);
|
||||||
|
|
||||||
|
const attachingUsersCount = await User.count({
|
||||||
|
$or: [{
|
||||||
|
avatarId: file._id
|
||||||
|
}, {
|
||||||
|
bannerId: file._id
|
||||||
|
}]
|
||||||
|
}, { limit: 1 });
|
||||||
|
if (attachingUsersCount !== 0) return skip();
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
// チャンクをすべて削除
|
||||||
|
DriveFileChunk.remove({
|
||||||
|
files_id: file._id
|
||||||
|
}),
|
||||||
|
|
||||||
|
DriveFile.update({ _id: file._id }, {
|
||||||
|
$set: {
|
||||||
|
'metadata.deletedAt': new Date(),
|
||||||
|
'metadata.isExpired': true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]).then(async () => {
|
||||||
|
res([i, file, true]);
|
||||||
|
|
||||||
|
//#region サムネイルもあれば削除
|
||||||
|
const thumbnail = await DriveFileThumbnail.findOne({
|
||||||
|
'metadata.originalId': file._id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (thumbnail) {
|
||||||
|
DriveFileThumbnailChunk.remove({
|
||||||
|
files_id: thumbnail._id
|
||||||
|
});
|
||||||
|
|
||||||
|
DriveFileThumbnail.remove({ _id: thumbnail._id });
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.then(([i, file, deleted]) => {
|
||||||
|
if (deleted) {
|
||||||
|
log(chalk`{gray ${i}} {red deleted: {bold ${file._id}} ${file.filename}}`);
|
||||||
|
} else {
|
||||||
|
log(chalk`{gray ${i}} {green skipped: {bold ${file._id}} ${file.filename}}`);
|
||||||
|
}
|
||||||
|
log.clear();
|
||||||
|
console.log();
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return await sequential(promiseGens);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().then(() => {
|
||||||
|
console.log('ALL DONE');
|
||||||
|
}).catch(console.error);
|
@ -1,56 +0,0 @@
|
|||||||
const sequential = require('promise-sequential');
|
|
||||||
const { default: DriveFile, deleteDriveFile } = require('../built/models/drive-file');
|
|
||||||
const { default: Note } = require('../built/models/note');
|
|
||||||
const { default: MessagingMessage } = require('../built/models/messaging-message');
|
|
||||||
const { default: User } = require('../built/models/user');
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const promiseGens = [];
|
|
||||||
|
|
||||||
const count = await DriveFile.count({});
|
|
||||||
|
|
||||||
let prev;
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
promiseGens.push(() => new Promise(async (res, rej) => {
|
|
||||||
const file = await DriveFile.findOne(prev ? {
|
|
||||||
_id: { $gt: prev._id }
|
|
||||||
} : {}, {
|
|
||||||
sort: {
|
|
||||||
_id: 1
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
prev = file;
|
|
||||||
|
|
||||||
console.log(`scanning ${file._id}`);
|
|
||||||
|
|
||||||
const attachingUsersCount = await User.count({
|
|
||||||
$or: [{
|
|
||||||
avatarId: file._id
|
|
||||||
}, {
|
|
||||||
bannerId: file._id
|
|
||||||
}]
|
|
||||||
}, { limit: 1 });
|
|
||||||
if (attachingUsersCount !== 0) return res();
|
|
||||||
|
|
||||||
const attachingNotesCount = await Note.count({
|
|
||||||
mediaIds: file._id
|
|
||||||
}, { limit: 1 });
|
|
||||||
if (attachingNotesCount !== 0) return res();
|
|
||||||
|
|
||||||
const attachingMessagesCount = await MessagingMessage.count({
|
|
||||||
fileId: file._id
|
|
||||||
}, { limit: 1 });
|
|
||||||
if (attachingMessagesCount !== 0) return res();
|
|
||||||
|
|
||||||
console.log(`deleting ${file._id}`);
|
|
||||||
|
|
||||||
deleteDriveFile(file).then(res).catch(rej);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
return await sequential(promiseGens);
|
|
||||||
}
|
|
||||||
|
|
||||||
main().then(console.dir).catch(console.error);
|
|
80
cli/clean-unused-drive-files.js
Normal file
80
cli/clean-unused-drive-files.js
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
const chalk = require('chalk');
|
||||||
|
const log = require('single-line-log').stdout;
|
||||||
|
const sequential = require('promise-sequential');
|
||||||
|
const { default: DriveFile, deleteDriveFile } = require('../built/models/drive-file');
|
||||||
|
const { default: Note } = require('../built/models/note');
|
||||||
|
const { default: MessagingMessage } = require('../built/models/messaging-message');
|
||||||
|
const { default: User } = require('../built/models/user');
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const promiseGens = [];
|
||||||
|
|
||||||
|
const count = await DriveFile.count({});
|
||||||
|
|
||||||
|
let prev;
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
promiseGens.push(() => {
|
||||||
|
const promise = new Promise(async (res, rej) => {
|
||||||
|
const file = await DriveFile.findOne(prev ? {
|
||||||
|
_id: { $lt: prev._id }
|
||||||
|
} : {}, {
|
||||||
|
sort: {
|
||||||
|
_id: -1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
prev = file;
|
||||||
|
|
||||||
|
function skip() {
|
||||||
|
res([i, file, false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file == null) return skip();
|
||||||
|
|
||||||
|
log(chalk`{gray ${i}} scanning {bold ${file._id}} ${file.filename} ...`);
|
||||||
|
|
||||||
|
const attachingUsersCount = await User.count({
|
||||||
|
$or: [{
|
||||||
|
avatarId: file._id
|
||||||
|
}, {
|
||||||
|
bannerId: file._id
|
||||||
|
}]
|
||||||
|
}, { limit: 1 });
|
||||||
|
if (attachingUsersCount !== 0) return skip();
|
||||||
|
|
||||||
|
const attachingNotesCount = await Note.count({
|
||||||
|
mediaIds: file._id
|
||||||
|
}, { limit: 1 });
|
||||||
|
if (attachingNotesCount !== 0) return skip();
|
||||||
|
|
||||||
|
const attachingMessagesCount = await MessagingMessage.count({
|
||||||
|
fileId: file._id
|
||||||
|
}, { limit: 1 });
|
||||||
|
if (attachingMessagesCount !== 0) return skip();
|
||||||
|
|
||||||
|
deleteDriveFile(file).then(() => {
|
||||||
|
res([i, file, true]);
|
||||||
|
}).catch(rej);
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.then(([i, file, deleted]) => {
|
||||||
|
if (deleted) {
|
||||||
|
log(chalk`{gray ${i}} {red deleted: {bold ${file._id}} ${file.filename}}`);
|
||||||
|
} else {
|
||||||
|
log(chalk`{gray ${i}} {green skipped: {bold ${file._id}} ${file.filename}}`);
|
||||||
|
}
|
||||||
|
log.clear();
|
||||||
|
console.log();
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return await sequential(promiseGens);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().then(() => {
|
||||||
|
console.log('done');
|
||||||
|
}).catch(console.error);
|
@ -59,9 +59,15 @@ gulp.task('build:ts', () => {
|
|||||||
.pipe(gulp.dest('./built/'));
|
.pipe(gulp.dest('./built/'));
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task('build:copy', () =>
|
gulp.task('build:copy:views', () =>
|
||||||
|
gulp.src('./src/server/web/views/**/*').pipe(gulp.dest('./built/server/web/views'))
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task('build:copy', ['build:copy:views'], () =>
|
||||||
gulp.src([
|
gulp.src([
|
||||||
'./build/Release/crypto_key.node',
|
'./build/Release/crypto_key.node',
|
||||||
|
'./src/const.json',
|
||||||
|
'./src/server/web/views/**/*',
|
||||||
'./src/**/assets/**/*',
|
'./src/**/assets/**/*',
|
||||||
'!./src/client/app/**/assets/**/*'
|
'!./src/client/app/**/assets/**/*'
|
||||||
]).pipe(gulp.dest('./built/'))
|
]).pipe(gulp.dest('./built/'))
|
||||||
|
@ -13,7 +13,8 @@ const native = loadLang('ja');
|
|||||||
const langs = {
|
const langs = {
|
||||||
'en': loadLang('en'),
|
'en': loadLang('en'),
|
||||||
'fr': loadLang('fr'),
|
'fr': loadLang('fr'),
|
||||||
'ja': native
|
'ja': native,
|
||||||
|
'pl': loadLang('pl')
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.entries(langs).map(([, locale]) => {
|
Object.entries(langs).map(([, locale]) => {
|
||||||
|
637
locales/pl.yml
Normal file
637
locales/pl.yml
Normal file
@ -0,0 +1,637 @@
|
|||||||
|
common:
|
||||||
|
misskey: "Dziel się zawartością z innymi korzystając z Misskey."
|
||||||
|
|
||||||
|
time:
|
||||||
|
unknown: "nieznany"
|
||||||
|
future: "w przyszłości"
|
||||||
|
just_now: "teraz"
|
||||||
|
seconds_ago: "{} sek. temu"
|
||||||
|
minutes_ago: "{} min. temu"
|
||||||
|
hours_ago: "{} godz. temu"
|
||||||
|
days_ago: "{} dni temu"
|
||||||
|
weeks_ago: "{} tyg. temu"
|
||||||
|
months_ago: "{} mies. temu"
|
||||||
|
years_ago: "{} lat temu"
|
||||||
|
|
||||||
|
weekday-short:
|
||||||
|
sunday: "N"
|
||||||
|
monday: "Pn"
|
||||||
|
tuesday: "W"
|
||||||
|
wednesday: "Ś"
|
||||||
|
thursday: "C"
|
||||||
|
friday: "P"
|
||||||
|
satruday: "S"
|
||||||
|
|
||||||
|
reactions:
|
||||||
|
like: "Lubię"
|
||||||
|
love: "Kocham"
|
||||||
|
laugh: "Śmieszne"
|
||||||
|
hmm: "Hmm…?"
|
||||||
|
surprise: "Wow"
|
||||||
|
congrats: "Gratuluję!"
|
||||||
|
angry: "Wściekły"
|
||||||
|
confused: "Zmieszany"
|
||||||
|
pudding: "Pudding"
|
||||||
|
|
||||||
|
delete: "Usuń"
|
||||||
|
loading: "Ładowanie"
|
||||||
|
ok: "OK"
|
||||||
|
update-available: "Nowa wersja Misskey jest dostępna ({newer}, obecna to {current}). Odśwież stronę, aby zastosować aktualizację."
|
||||||
|
my-token-regenerated: "Twój token został wygenerowany. Zostaniesz wylogowany."
|
||||||
|
|
||||||
|
common/views/components/connect-failed.vue:
|
||||||
|
title: "Nie udało się połączyć z serwerem"
|
||||||
|
description: "Wystąpił problem z Twoim połączeniem z Internetem, lub z serwerem. {Spróbuj ponownie} wkrótce."
|
||||||
|
thanks: "Dziękujemy za korzystanie z Misskey."
|
||||||
|
troubleshoot: "Rozwiązywanie problemów"
|
||||||
|
|
||||||
|
common/views/components/connect-failed.troubleshooter.vue:
|
||||||
|
title: "Rozwiązywanie problemów"
|
||||||
|
network: "Połączenie z siecią"
|
||||||
|
checking-network: "Sprawdzanie połączenia sieciowego"
|
||||||
|
internet: "Połączenie z Internetem"
|
||||||
|
checking-internet: "Sprawdzanie połączenia z Internetem"
|
||||||
|
server: "Połączenie z serwerem"
|
||||||
|
checking-server: "Sprawdzanie połączenia z serwerem"
|
||||||
|
finding: "Wyszukiwanie problemu"
|
||||||
|
no-network: "Brak połączenia z siecią"
|
||||||
|
no-network-desc: "Upewnij się, że jesteś połączony z siecią."
|
||||||
|
no-internet: "Brak połączenia z Internetem"
|
||||||
|
no-internet-desc: "Upewnij się, że jesteś połączony z Internetem."
|
||||||
|
no-server: "Nie udało się połączyć z serwerem"
|
||||||
|
no-server-desc: "Połączenie sieciowe działa, ale nie udało się połączyć z serwerem Misskey. Możliwe że serwer nie działa lub trwają prace konserwacyjne, spróbuj ponownie później."
|
||||||
|
success: "Pomyślnie połączono z serwerem Misskey"
|
||||||
|
success-desc: "Wygląda na to, że udało się połączyć. Odśwież stronę."
|
||||||
|
flush: "Wyczyść pamięć podręczną"
|
||||||
|
set-version: "Określ wersję"
|
||||||
|
|
||||||
|
common/views/components/messaging.vue:
|
||||||
|
search-user: "Znajdź użytkownika"
|
||||||
|
you: "Ty"
|
||||||
|
no-history: "Brak historii"
|
||||||
|
|
||||||
|
common/views/components/messaging-room.vue:
|
||||||
|
empty: "Brak konwersacji"
|
||||||
|
more: "Więcej"
|
||||||
|
no-history: "Brak dalszej historii"
|
||||||
|
resize-form: "Przeciągnij aby zmienić rozmiar"
|
||||||
|
new-message: "Nowa wiadomość"
|
||||||
|
|
||||||
|
common/views/components/messaging-room.form.vue:
|
||||||
|
input-message-here: "Wprowadź wiadomość tutaj"
|
||||||
|
send: "Wyślij"
|
||||||
|
attach-from-local: "Załącz pliki z komputera"
|
||||||
|
attach-from-drive: "Załącz pliki z dysku"
|
||||||
|
|
||||||
|
common/views/components/messaging-room.message.vue:
|
||||||
|
is-read: "Przeczytano"
|
||||||
|
deleted: "Wiadomość została usunięta"
|
||||||
|
|
||||||
|
common/views/components/nav.vue:
|
||||||
|
about: "O stronie"
|
||||||
|
stats: "Statystyki"
|
||||||
|
status: "Stan"
|
||||||
|
wiki: "Wiki"
|
||||||
|
donors: "Sponsorzy"
|
||||||
|
repository: "Repozytorium"
|
||||||
|
develop: "Autorzy"
|
||||||
|
feedback: "Podziel się opinią"
|
||||||
|
|
||||||
|
common/views/components/note-menu.vue:
|
||||||
|
favorite: "Dodaj do ulubionych"
|
||||||
|
pin: "Przypnij do profilu"
|
||||||
|
|
||||||
|
common/views/components/poll.vue:
|
||||||
|
vote-to: "Zagłosuj na '{}'"
|
||||||
|
vote-count: "{} głosów"
|
||||||
|
total-users: "{} głosujących"
|
||||||
|
vote: "Zagłosuj"
|
||||||
|
show-result: "Pokaż wyniki"
|
||||||
|
voted: "Zagłosowano"
|
||||||
|
|
||||||
|
common/views/components/poll-editor.vue:
|
||||||
|
no-only-one-choice: "Musisz wprowadzić dwie lub więcej opcji."
|
||||||
|
choice-n: "Opcja {}"
|
||||||
|
remove: "Usuń tą opcję"
|
||||||
|
add: "+ Dodaj opcję"
|
||||||
|
destroy: "Usuń ankietę"
|
||||||
|
|
||||||
|
common/views/components/reaction-picker.vue:
|
||||||
|
choose-reaction: "Wybierz reakcję"
|
||||||
|
|
||||||
|
common/views/components/signin.vue:
|
||||||
|
username: "Nazwa użytkownika"
|
||||||
|
password: "Hasło"
|
||||||
|
token: "Token"
|
||||||
|
signing-in: "Logowanie…"
|
||||||
|
signin: "Zaloguj"
|
||||||
|
|
||||||
|
common/views/components/signup.vue:
|
||||||
|
username: "Nazwa użytkownika"
|
||||||
|
checking: "Sprawdzanie…"
|
||||||
|
available: "Dostępna"
|
||||||
|
unavailable: "Niedostępna"
|
||||||
|
error: "Błąd sieci"
|
||||||
|
invalid-format: "Może zawierać litery, cyfry i myślniki."
|
||||||
|
too-short: "Wprowadź przynajmniej jeden znak"
|
||||||
|
too-long: "Nazwa nie może zawierać więcej niż 20 znaków"
|
||||||
|
password: "Hasło"
|
||||||
|
password-placeholder: "Zalecamy korzystanie z hasła zawierającego przynajmniej 8 znaków."
|
||||||
|
weak-password: "Słabe"
|
||||||
|
normal-password: "Średnie"
|
||||||
|
strong-password: "Silne"
|
||||||
|
retype: "Powtórz hasło"
|
||||||
|
retype-placeholder: "Potwierdź hasło"
|
||||||
|
password-matched: "OK"
|
||||||
|
password-not-matched: "Hasła nie zgadzają się"
|
||||||
|
recaptcha: "Weryfikacja"
|
||||||
|
create: "Utwórz konto"
|
||||||
|
some-error: "Nie udało się utworzyć konta. Spróbuj ponownie."
|
||||||
|
|
||||||
|
common/views/components/special-message.vue:
|
||||||
|
new-year: "Szczęśliwego nowego roku!"
|
||||||
|
christmas: "Wesołych świąt!"
|
||||||
|
|
||||||
|
common/views/components/stream-indicator.vue:
|
||||||
|
connecting: "Łączenie"
|
||||||
|
reconnecting: "Ponowne łączenie"
|
||||||
|
connected: "Połączono"
|
||||||
|
|
||||||
|
common/views/components/twitter-setting.vue:
|
||||||
|
description: "Jeżeli połączysz konto Twittera z kontem Misskey, informacje z Twittera będą widoczne na Twoim profilu i będziesz mógł logować się z użyciem Twittera."
|
||||||
|
connected-to: "Jesteś połączony z tym kontem Twittera"
|
||||||
|
detail: "Szczegóły…"
|
||||||
|
reconnect: "Połącz ponownie"
|
||||||
|
connect: "Połącz z kontem Twittera"
|
||||||
|
disconnect: "Rozłącz"
|
||||||
|
|
||||||
|
common/views/components/uploader.vue:
|
||||||
|
waiting: "Oczekiwanie"
|
||||||
|
|
||||||
|
common/views/widgets/broadcast.vue:
|
||||||
|
no-broadcasts: "Brak transmisji"
|
||||||
|
have-a-nice-day: "Miłego dnia!"
|
||||||
|
next: "Dalej"
|
||||||
|
|
||||||
|
common/views/widgets/donation.vue:
|
||||||
|
title: "Dotacje"
|
||||||
|
text: "Aby utrzymywać Misskey, płacimy za domenę, serwery i nie tylko… Nie zarabiamy na tym, więc byłoby nam miło, gdybyśmy uzyskali od Ciebie dotację. Jeżeli jesteś zainteresowany, skontaktuj się z {}. Dziękujemy za wsparcie!"
|
||||||
|
|
||||||
|
common/views/widgets/photo-stream.vue:
|
||||||
|
no-photos: "Brak zdjęć"
|
||||||
|
|
||||||
|
common/views/widgets/server.vue:
|
||||||
|
title: "Informacje o serwerze"
|
||||||
|
toggle: "Przełącz widok"
|
||||||
|
|
||||||
|
desktop/views/components/activity.vue:
|
||||||
|
title: "Aktywność"
|
||||||
|
toggle: "Przełącz widok"
|
||||||
|
|
||||||
|
desktop/views/components/calendar.vue:
|
||||||
|
title: "{1} / {2}"
|
||||||
|
prev: "Poprzedni miesiąc"
|
||||||
|
next: "Następny miesiąc"
|
||||||
|
go: "Naciśnij, aby przejść"
|
||||||
|
|
||||||
|
desktop/views/components/drive-window.vue:
|
||||||
|
used: "wykorzystane"
|
||||||
|
drive: "Dysk"
|
||||||
|
|
||||||
|
desktop/views/components/drive.file.vue:
|
||||||
|
avatar: "Awatar"
|
||||||
|
banner: "Baner"
|
||||||
|
contextmenu:
|
||||||
|
rename: "Zmień nazwę"
|
||||||
|
copy-url: "Skopiuj adres"
|
||||||
|
download: "Pobierz"
|
||||||
|
else-files: "Inne"
|
||||||
|
set-as-avatar: "Ustaw jako awatar"
|
||||||
|
set-as-banner: "Ustaw jako baner"
|
||||||
|
open-in-app: "Otwórz w aplikacji"
|
||||||
|
add-app: "Dodaj aplikację"
|
||||||
|
rename-file: "Zmień nazwę pliku"
|
||||||
|
input-new-file-name: "Wprowadź nową nazwę"
|
||||||
|
copied: "Skopiowano"
|
||||||
|
copied-url-to-clipboard: "Skopiowano adres do schowka"
|
||||||
|
|
||||||
|
desktop/views/components/drive.folder.vue:
|
||||||
|
unable-to-process: "Nie udało się ukończyć działania."
|
||||||
|
circular-reference-detected: "Docelowy katalog znajduje się w katalogu, który chcesz przenieść."
|
||||||
|
unhandled-error: "Nieznany błąd"
|
||||||
|
contextmenu:
|
||||||
|
move-to-this-folder: "Przenieś do tego katalogu"
|
||||||
|
show-in-new-window: "Otwórz w nowym oknie"
|
||||||
|
rename: "Zmień nazwę"
|
||||||
|
rename-folder: "Zmień nazwę katalogu"
|
||||||
|
input-new-folder-name: "Wprowadź nową nazwę"
|
||||||
|
|
||||||
|
desktop/views/components/drive.nav-folder.vue:
|
||||||
|
drive: "Dysk"
|
||||||
|
|
||||||
|
desktop/views/components/drive.vue:
|
||||||
|
search: "Szukaj"
|
||||||
|
load-more: "Załaduj więcej"
|
||||||
|
empty-drive: "Twój dysk jest pusty"
|
||||||
|
empty-drive-description: "Możesz wysłać plik klikając prawym przyciskiem myszy i wybierając \"Wyślij plik\" lub przeciągnąć plik i upuścić w tym oknie."
|
||||||
|
empty-folder: "Ten katalog jest posty"
|
||||||
|
unable-to-process: "Nie udało się dokończyć działania."
|
||||||
|
circular-reference-detected: "Ten katalog znajduje się w katalogu, który chcesz przenieść."
|
||||||
|
unhandled-error: "Nieznany błąd"
|
||||||
|
url-upload: "Wyślij z adresu"
|
||||||
|
url-of-file: "Adres URL pliku, który chcesz wysłać"
|
||||||
|
url-upload-requested: "Zaplanowano wysyłanie"
|
||||||
|
may-take-time: "Może trochę potrwać, zanim wysyłanie zostanie ukończone."
|
||||||
|
create-folder: "Utwórz katalog"
|
||||||
|
folder-name: "Nazwa katalogu"
|
||||||
|
contextmenu:
|
||||||
|
create-folder: "Utwórz katalog"
|
||||||
|
upload: "Wyślij plik"
|
||||||
|
url-upload: "Wyślij z adresu URL"
|
||||||
|
|
||||||
|
desktop/views/components/messaging-window.vue:
|
||||||
|
title: "Wiadomości"
|
||||||
|
|
||||||
|
desktop/views/components/notes.note.vue:
|
||||||
|
reposted-by: "Udostępniono przez {}"
|
||||||
|
reply: "Odpowiedz"
|
||||||
|
renote: "Przeredaguj"
|
||||||
|
add-reaction: "Dodaj reakcję"
|
||||||
|
detail: "Pokaż szczegóły"
|
||||||
|
|
||||||
|
desktop/views/components/notifications.vue:
|
||||||
|
more: "Więcej"
|
||||||
|
empty: "Brak powiadomień"
|
||||||
|
|
||||||
|
desktop/views/components/post-form.vue:
|
||||||
|
note-placeholder: "Co się dzieje?"
|
||||||
|
reply-placeholder: "Odpowiedz na ten wpis…"
|
||||||
|
quote-placeholder: "Zacytuj ten wpis…"
|
||||||
|
note: "Wyślij"
|
||||||
|
reply: "Odpowiedz"
|
||||||
|
renote: "Przeredaguj"
|
||||||
|
posted: "Opublikowano!"
|
||||||
|
replied: "Odpowiedziano!"
|
||||||
|
reposted: "Udostępniono!"
|
||||||
|
note-failed: "Nie udało się wysłać"
|
||||||
|
reply-failed: "Nie udało się odpowiedzieć"
|
||||||
|
renote-failed: "Nie udało się przeredagować"
|
||||||
|
posting: "Wysyłanie"
|
||||||
|
attach-media-from-local: "Załącz zawartość multimedialną z komputera"
|
||||||
|
attach-media-from-drive: "Załącz zawartość multimedialną z dysku"
|
||||||
|
attach-cancel: "Usuń załącznik"
|
||||||
|
insert-a-kao: "v(‘ω’)v"
|
||||||
|
create-poll: "Utwórz ankietę"
|
||||||
|
text-remain: "pozostałe znaki: {}"
|
||||||
|
|
||||||
|
desktop/views/components/post-form-window.vue:
|
||||||
|
note: "Nowy wpis"
|
||||||
|
reply: "Odpowiedz"
|
||||||
|
attaches: "{} załączników multimedialnych"
|
||||||
|
uploading-media: "Wysyłanie {} treści multimedialnych"
|
||||||
|
|
||||||
|
desktop/views/components/renote-form.vue:
|
||||||
|
quote: "Cytuj…"
|
||||||
|
cancel: "Anuluj"
|
||||||
|
renote: "Przeredaguj"
|
||||||
|
reposting: "Udostępnianie…"
|
||||||
|
success: "Udostępniono!"
|
||||||
|
failure: "Nie udało się przeredagować"
|
||||||
|
|
||||||
|
desktop/views/components/renote-form-window.vue:
|
||||||
|
title: "Czy na pewno chcesz przeredagować ten wpis?"
|
||||||
|
|
||||||
|
desktop/views/components/settings.vue:
|
||||||
|
profile: "Profil"
|
||||||
|
notification: "Powiadomienia"
|
||||||
|
apps: "Aplikacje"
|
||||||
|
mute: "Wyciszanie"
|
||||||
|
drive: "Dysk"
|
||||||
|
security: "Bezpieczeństwo"
|
||||||
|
password: "Hasło"
|
||||||
|
2fa: "Uwierzytelnianie dwuetapowe"
|
||||||
|
other: "Inne"
|
||||||
|
license: "Licencja"
|
||||||
|
|
||||||
|
desktop/views/components/settings.2fa.vue:
|
||||||
|
intro: "Jeżeli skonfigurujesz uwierzytelnianie dwuetapowe, aby zablokować się będziesz potrzebować (oprócz hasła) kodu ze skonfigurowanego urządzenia (np. smartfonu), co zwiększy bezpieczeństwo."
|
||||||
|
detail: "Zobacz szczegóły…"
|
||||||
|
url: "https://www.google.com/landing/2step/"
|
||||||
|
caution: "Jeżeli stracisz dostęp do urządzenia, nie będziesz mógł logować się do Misskey!"
|
||||||
|
register: "Zarejestruj urządzenie"
|
||||||
|
already-registered: "Urządzenie jest już zarejestrowane"
|
||||||
|
unregister: "Wyłącz"
|
||||||
|
unregistered: "Wyłączono uwierzytelnianie dwuetapowe."
|
||||||
|
enter-password: "Wprowadź hasło"
|
||||||
|
authenticator: "Na początek musisz zainstalować Google Authenticator na swoim urządzeniu:"
|
||||||
|
howtoinstall: "Jak zainstalować"
|
||||||
|
scan: "Później, zeskanuje ten kod QR:"
|
||||||
|
done: "Wprowadź token wyświetlony na Twoim urządzeniu:"
|
||||||
|
submit: "Wyślij"
|
||||||
|
success: "Pomyślnie ukończono konfigurację!"
|
||||||
|
failed: "Nie udało się skonfigurować uwierzytelniania dwuetapowego, upewnij się że wprowadziłeś prawidłowy token."
|
||||||
|
info: "Od teraz, wprowadzaj token wyświetlany na urządzeniu przy każdym logowaniu do Misskey."
|
||||||
|
|
||||||
|
desktop/views/components/settings.api.vue:
|
||||||
|
caution: "Nie pokazuj tego tokenu osobom trzecim (nie wprowadzaj go nigdzie indziej), aby konto nie trafiło w niepowołane ręce."
|
||||||
|
regeneration-of-token: "W przypadku wycieku tokenu, możesz wygenerować nowy."
|
||||||
|
regenerate-token: "Wygeneruj nowy token"
|
||||||
|
enter-password: "Wprowadź hasło"
|
||||||
|
|
||||||
|
desktop/views/components/settings.app.vue:
|
||||||
|
no-apps: "Brak zautoryzowanych aplikacji"
|
||||||
|
|
||||||
|
desktop/views/components/settings.mute.vue:
|
||||||
|
no-users: "Brak wyciszonych użytkowników"
|
||||||
|
|
||||||
|
desktop/views/components/settings.password.vue:
|
||||||
|
reset: "Zmień hasło"
|
||||||
|
enter-current-password: "Wprowadź obecne hasło"
|
||||||
|
enter-new-password: "Wprowadź nowe hasło"
|
||||||
|
enter-new-password-again: "Wprowadź ponownie nowe hasło"
|
||||||
|
not-match: "Nowe hasła nie pasują do siebie"
|
||||||
|
changed: "Pomyślnie zmieniono hasło"
|
||||||
|
|
||||||
|
desktop/views/components/settings.profile.vue:
|
||||||
|
avatar: "Awatar"
|
||||||
|
choice-avatar: "Wybierz obraz"
|
||||||
|
name: "Nazwa"
|
||||||
|
location: "Lokalizacja"
|
||||||
|
description: "Opis"
|
||||||
|
birthday: "Data urodzenia"
|
||||||
|
save: "Aktualizuj profil"
|
||||||
|
|
||||||
|
desktop/views/components/ui.header.account.vue:
|
||||||
|
profile: "Twój profil"
|
||||||
|
drive: "Dysk"
|
||||||
|
favorites: "Ulubione"
|
||||||
|
lists: "Listy"
|
||||||
|
customize: "Dostosuj"
|
||||||
|
settings: "Ustawienia"
|
||||||
|
signout: "Wyloguj się"
|
||||||
|
dark: "Sprowadź ciemność"
|
||||||
|
|
||||||
|
desktop/views/components/ui.header.nav.vue:
|
||||||
|
home: "Strona główna"
|
||||||
|
messaging: "Wiadomości"
|
||||||
|
game: "Gra"
|
||||||
|
|
||||||
|
desktop/views/components/ui.header.notifications.vue:
|
||||||
|
title: "Powiadomienia"
|
||||||
|
|
||||||
|
desktop/views/components/ui.header.post.vue:
|
||||||
|
post: "Utwórz nowy wpis"
|
||||||
|
|
||||||
|
desktop/views/components/ui.header.search.vue:
|
||||||
|
placeholder: "Szukaj"
|
||||||
|
|
||||||
|
desktop/views/pages/note.vue:
|
||||||
|
prev: "Poprzedni wpis"
|
||||||
|
next: "Następny wpis"
|
||||||
|
|
||||||
|
desktop/views/pages/selectdrive.vue:
|
||||||
|
title: "Wybierz plik(i)"
|
||||||
|
ok: "OK"
|
||||||
|
cancel: "Anuluj"
|
||||||
|
upload: "Wyślij pliki z Twojego komputera"
|
||||||
|
|
||||||
|
desktop/views/pages/user/user.followers-you-know.vue:
|
||||||
|
title: "Śledzący których znasz"
|
||||||
|
loading: "Ładowanie"
|
||||||
|
no-users: "Brak użytkowników"
|
||||||
|
|
||||||
|
desktop/views/pages/user/user.friends.vue:
|
||||||
|
title: "Najbardziej aktywni"
|
||||||
|
loading: "Ładowanie"
|
||||||
|
no-users: "Brak użytkowników"
|
||||||
|
|
||||||
|
desktop/views/pages/user/user.header.vue:
|
||||||
|
is-suspended: "To konto zostało zawieszone."
|
||||||
|
is-remote: "To jest użytkownik zdalnej instancji, informacje mogą nie być w pełni dokładne."
|
||||||
|
view-remote: "Wyświetl dokładne informacje"
|
||||||
|
|
||||||
|
desktop/views/pages/user/user.home.vue:
|
||||||
|
last-used-at: "Ostatnio aktywny: "
|
||||||
|
|
||||||
|
desktop/views/pages/user/user.photos.vue:
|
||||||
|
title: "Zdjęcia"
|
||||||
|
loading: "Ładowanie"
|
||||||
|
no-photos: "Brak zdjęć"
|
||||||
|
|
||||||
|
desktop/views/pages/user/user.profile.vue:
|
||||||
|
follows-you: "Śledzi Cię"
|
||||||
|
stalk: "Stalkuj"
|
||||||
|
stalking: "Stalkujesz"
|
||||||
|
unstalk: "Przestań stalkować"
|
||||||
|
mute: "Wycisz"
|
||||||
|
muted: "Wyciszyłeś"
|
||||||
|
unmute: "Cofnij wyciszenie"
|
||||||
|
|
||||||
|
desktop/views/widgets/notifications.vue:
|
||||||
|
title: "Powiadomienia"
|
||||||
|
settings: "Ustawienia"
|
||||||
|
|
||||||
|
desktop/views/widgets/polls.vue:
|
||||||
|
title: "Ankiety"
|
||||||
|
refresh: "Pokaż inne"
|
||||||
|
nothing: "Pusto"
|
||||||
|
|
||||||
|
desktop/views/widgets/post-form.vue:
|
||||||
|
title: "Wpis"
|
||||||
|
note: "Wpis"
|
||||||
|
placeholder: "Co się dzieje?"
|
||||||
|
|
||||||
|
desktop/views/widgets/trends.vue:
|
||||||
|
title: "Na czasie"
|
||||||
|
refresh: "Pokaż inne"
|
||||||
|
nothing: "Pusto"
|
||||||
|
|
||||||
|
desktop/views/widgets/users.vue:
|
||||||
|
title: "Polecani użytkownicy"
|
||||||
|
refresh: "Pokaż innych"
|
||||||
|
no-one: "Pusto"
|
||||||
|
|
||||||
|
desktop/views/widgets/channel.vue:
|
||||||
|
title: "Kanał"
|
||||||
|
settings: "Ustawienia widżetu"
|
||||||
|
|
||||||
|
mobile/views/components/drive.vue:
|
||||||
|
drive: "Dysk"
|
||||||
|
used: "użyto"
|
||||||
|
folder-count: "Katalog(i)"
|
||||||
|
count-separator: ", "
|
||||||
|
file-count: "Plik(i)"
|
||||||
|
load-more: "Załaduj więcej"
|
||||||
|
nothing-in-drive: "Pusto"
|
||||||
|
folder-is-empty: "Ten katalog jest pusty"
|
||||||
|
|
||||||
|
mobile/views/components/drive-file-chooser.vue:
|
||||||
|
select-file: "Wybierz plik"
|
||||||
|
|
||||||
|
mobile/views/components/drive-folder-chooser.vue:
|
||||||
|
select-folder: "Wybierz katalog"
|
||||||
|
|
||||||
|
mobile/views/components/drive.file-detail.vue:
|
||||||
|
download: "Pobierz"
|
||||||
|
rename: "Zmień nazwę"
|
||||||
|
move: "Przenieś"
|
||||||
|
hash: "Hash (md5)"
|
||||||
|
exif: "EXIF"
|
||||||
|
|
||||||
|
mobile/views/components/follow-button.vue:
|
||||||
|
follow: "Śledź"
|
||||||
|
unfollow: "Przestań śledzić"
|
||||||
|
|
||||||
|
mobile/views/components/note-detail.vue:
|
||||||
|
reply: "Odpowiedz"
|
||||||
|
reaction: "Reakcja"
|
||||||
|
|
||||||
|
mobile/views/components/notifications.vue:
|
||||||
|
more: "Więcej"
|
||||||
|
empty: "Brak powiadomień"
|
||||||
|
|
||||||
|
mobile/views/components/post-form.vue:
|
||||||
|
submit: "Wyślij"
|
||||||
|
reply-placeholder: "Odpowiedź na ten wpis…"
|
||||||
|
note-placeholder: "Co się dzieje?"
|
||||||
|
|
||||||
|
mobile/views/components/sub-note-content.vue:
|
||||||
|
media-count: "{} zawartości multimedialnej"
|
||||||
|
poll: "Ankieta"
|
||||||
|
|
||||||
|
mobile/views/components/timeline.vue:
|
||||||
|
empty: "Brak wpisów"
|
||||||
|
load-more: "Więcej"
|
||||||
|
|
||||||
|
mobile/views/components/ui.nav.vue:
|
||||||
|
home: "Strona główna"
|
||||||
|
notifications: "Powiadomienia"
|
||||||
|
messaging: "Wiadomości"
|
||||||
|
drive: "Dysk"
|
||||||
|
settings: "Ustawienia"
|
||||||
|
about: "O Misskey"
|
||||||
|
search: "Szukaj"
|
||||||
|
|
||||||
|
mobile/views/components/user-timeline.vue:
|
||||||
|
no-notes: "Wygląda na to, że ten użytkownik nie opublikował jeszcze niczego"
|
||||||
|
no-notes-with-media: "Brak wpisów z zawartością multimedialną"
|
||||||
|
load-more: "Więcej"
|
||||||
|
|
||||||
|
mobile/views/components/users-list.vue:
|
||||||
|
all: "Wszyscy"
|
||||||
|
known: "Znasz"
|
||||||
|
load-more: "Więcej"
|
||||||
|
|
||||||
|
mobile/views/pages/drive.vue:
|
||||||
|
drive: "Dysk"
|
||||||
|
|
||||||
|
mobile/views/pages/followers.vue:
|
||||||
|
followers-of: "Śledzący {}"
|
||||||
|
|
||||||
|
mobile/views/pages/following.vue:
|
||||||
|
following-of: "Śledzeni przez {}"
|
||||||
|
|
||||||
|
mobile/views/pages/home.vue:
|
||||||
|
timeline: "Oś czasu"
|
||||||
|
|
||||||
|
mobile/views/pages/messaging.vue:
|
||||||
|
messaging: "Wiadomości"
|
||||||
|
|
||||||
|
mobile/views/pages/messaging-room.vue:
|
||||||
|
messaging: "Wiadomości"
|
||||||
|
|
||||||
|
mobile/views/pages/note.vue:
|
||||||
|
title: "Wyślij"
|
||||||
|
prev: "Poprzedni wpis"
|
||||||
|
next: "Następny wpis"
|
||||||
|
|
||||||
|
mobile/views/pages/notifications.vue:
|
||||||
|
notifications: "Powiadomienia"
|
||||||
|
read-all: "Czy na pewno chcesz oznaczyć wszystkie powiadomienia jako przeczytane?"
|
||||||
|
|
||||||
|
mobile/views/pages/profile-setting.vue:
|
||||||
|
title: "Ustawienia profilu"
|
||||||
|
will-be-published: "Te ustawienia profilu zostaną zaktualizowane."
|
||||||
|
name: "Nazwa"
|
||||||
|
location: "Lokalizacja"
|
||||||
|
description: "Opis"
|
||||||
|
birthday: "Data urodzenia"
|
||||||
|
avatar: "Awatar"
|
||||||
|
banner: "Baner"
|
||||||
|
avatar-saved: "Pomyślnie zaktualizowano awatar"
|
||||||
|
banner-saved: "Pomyślnie zaktualizowano baner"
|
||||||
|
set-avatar: "Wybierz awatar"
|
||||||
|
set-banner: "Wybierz baner"
|
||||||
|
save: "Zapisz"
|
||||||
|
saved: "Pomyślnie zaktualizowano profil"
|
||||||
|
|
||||||
|
mobile/views/pages/search.vue:
|
||||||
|
search: "Szukaj"
|
||||||
|
empty: "Nie znaleziono wpisów zawierających '{}'"
|
||||||
|
|
||||||
|
mobile/views/pages/selectdrive.vue:
|
||||||
|
select-file: "Wybierz plik"
|
||||||
|
|
||||||
|
mobile/views/pages/settings.vue:
|
||||||
|
signed-in-as: "Zalogowany jako {}"
|
||||||
|
profile: "Profil"
|
||||||
|
twitter-integration: "Integracja z Twitterem"
|
||||||
|
signin-history: "Historia logowań"
|
||||||
|
settings: "Ustawienia"
|
||||||
|
signout: "Wyloguj"
|
||||||
|
|
||||||
|
mobile/views/pages/user.vue:
|
||||||
|
follows-you: "Śledzi Cię"
|
||||||
|
following: "Śledzeni"
|
||||||
|
followers: "Śledzący"
|
||||||
|
notes: "Wpisy"
|
||||||
|
overview: "Przegląd"
|
||||||
|
timeline: "Oś czasu"
|
||||||
|
media: "Zawartość multimedialna"
|
||||||
|
is-suspended: "To konto zostało zablokowane"
|
||||||
|
is-remote: "To jest użytkownik zdalnej instancji, informacje mogą nie być w pełni dokładne."
|
||||||
|
view-remote: "Wyświetl dokładne informacje"
|
||||||
|
|
||||||
|
mobile/views/pages/user/home.vue:
|
||||||
|
recent-notes: "Ostatnie wpisy"
|
||||||
|
images: "Zdjęcia"
|
||||||
|
activity: "Aktywność"
|
||||||
|
keywords: "Słowa kluczowe"
|
||||||
|
domains: "Domeny"
|
||||||
|
frequently-replied-users: "Często aktywni użytkownicy"
|
||||||
|
followers-you-know: "Śledzący których znasz"
|
||||||
|
last-used-at: "Ostatnio aktywny:"
|
||||||
|
|
||||||
|
mobile/views/pages/user/home.followers-you-know.vue:
|
||||||
|
loading: "Ładowanie"
|
||||||
|
no-users: "Brak użytkowników"
|
||||||
|
|
||||||
|
mobile/views/pages/user/home.friends.vue:
|
||||||
|
loading: "Ładowanie"
|
||||||
|
no-users: "Brak użytkowników"
|
||||||
|
|
||||||
|
mobile/views/pages/user/home.notes.vue:
|
||||||
|
loading: "Ładowanie"
|
||||||
|
no-notes: "Brak wpisów"
|
||||||
|
|
||||||
|
mobile/views/pages/user/home.photos.vue:
|
||||||
|
loading: "Ładowanie"
|
||||||
|
no-photos: "Brak zdjęć"
|
||||||
|
|
||||||
|
docs:
|
||||||
|
edit-this-page-on-github: "Znalazłeś błąd lub chcesz pomóc w tworzeniu dokumentacji?"
|
||||||
|
edit-this-page-on-github-link: "Edytuj stronę na GitHubie!"
|
||||||
|
|
||||||
|
api:
|
||||||
|
entities:
|
||||||
|
properties: "Właściwości"
|
||||||
|
endpoints:
|
||||||
|
params: "Parametry"
|
||||||
|
res: "Odpowiedź"
|
||||||
|
props:
|
||||||
|
name: "Nazwa"
|
||||||
|
type: "Rodzaj"
|
||||||
|
optional: "Nieobowiązkowy"
|
||||||
|
description: "Opis"
|
||||||
|
yes: "Tak"
|
||||||
|
no: "Nie"
|
57
migration/2.0.0.js
Normal file
57
migration/2.0.0.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// for Node.js interpret
|
||||||
|
|
||||||
|
const chalk = require('chalk');
|
||||||
|
const sequential = require('promise-sequential');
|
||||||
|
|
||||||
|
const { default: User } = require('../built/models/user');
|
||||||
|
const { default: DriveFile } = require('../built/models/drive-file');
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const promiseGens = [];
|
||||||
|
|
||||||
|
const count = await DriveFile.count({});
|
||||||
|
|
||||||
|
let prev;
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
promiseGens.push(() => {
|
||||||
|
const promise = new Promise(async (res, rej) => {
|
||||||
|
const file = await DriveFile.findOne(prev ? {
|
||||||
|
_id: { $gt: prev._id }
|
||||||
|
} : {}, {
|
||||||
|
sort: {
|
||||||
|
_id: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
prev = file;
|
||||||
|
|
||||||
|
const user = await User.findOne({ _id: file.metadata.userId });
|
||||||
|
|
||||||
|
DriveFile.update({
|
||||||
|
_id: file._id
|
||||||
|
}, {
|
||||||
|
$set: {
|
||||||
|
'metadata._user': {
|
||||||
|
host: user.host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
res([i, file]);
|
||||||
|
}).catch(rej);
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.then(([i, file]) => {
|
||||||
|
console.log(chalk`{gray ${i}} {green done: {bold ${file._id}} ${file.filename}}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return await sequential(promiseGens);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().then(() => {
|
||||||
|
console.log('ALL DONE');
|
||||||
|
}).catch(console.error);
|
71
migration/2.4.0.js
Normal file
71
migration/2.4.0.js
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
// for Node.js interpret
|
||||||
|
|
||||||
|
const chalk = require('chalk');
|
||||||
|
const sequential = require('promise-sequential');
|
||||||
|
|
||||||
|
const { default: User } = require('../built/models/user');
|
||||||
|
const { default: DriveFile } = require('../built/models/drive-file');
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const promiseGens = [];
|
||||||
|
|
||||||
|
const count = await User.count({});
|
||||||
|
|
||||||
|
let prev;
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
promiseGens.push(() => {
|
||||||
|
const promise = new Promise(async (res, rej) => {
|
||||||
|
const user = await User.findOne(prev ? {
|
||||||
|
_id: { $gt: prev._id }
|
||||||
|
} : {}, {
|
||||||
|
sort: {
|
||||||
|
_id: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
prev = user;
|
||||||
|
|
||||||
|
const set = {};
|
||||||
|
|
||||||
|
if (user.avatarId != null) {
|
||||||
|
const file = await DriveFile.findOne({ _id: user.avatarId });
|
||||||
|
|
||||||
|
if (file && file.metadata.properties.avgColor) {
|
||||||
|
set.avatarColor = file.metadata.properties.avgColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.bannerId != null) {
|
||||||
|
const file = await DriveFile.findOne({ _id: user.bannerId });
|
||||||
|
|
||||||
|
if (file && file.metadata.properties.avgColor) {
|
||||||
|
set.bannerColor = file.metadata.properties.avgColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(set).length === 0) return res([i, user]);
|
||||||
|
|
||||||
|
User.update({
|
||||||
|
_id: user._id
|
||||||
|
}, {
|
||||||
|
$set: set
|
||||||
|
}).then(() => {
|
||||||
|
res([i, user]);
|
||||||
|
}).catch(rej);
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.then(([i, user]) => {
|
||||||
|
console.log(chalk`{gray ${i}} {green done: {bold ${user._id}} @${user.username}}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return await sequential(promiseGens);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().then(() => {
|
||||||
|
console.log('ALL DONE');
|
||||||
|
}).catch(console.error);
|
@ -1,49 +0,0 @@
|
|||||||
// for Node.js interpret
|
|
||||||
|
|
||||||
const { default: User } = require('../built/models/user');
|
|
||||||
const { default: Following } = require('../built/models/following');
|
|
||||||
const { default: zip } = require('@prezzemolo/zip')
|
|
||||||
|
|
||||||
const migrate = async (following) => {
|
|
||||||
const follower = await User.findOne({ _id: following.followerId });
|
|
||||||
const followee = await User.findOne({ _id: following.followeeId });
|
|
||||||
const result = await Following.update(following._id, {
|
|
||||||
$set: {
|
|
||||||
stalk: true,
|
|
||||||
_follower: {
|
|
||||||
host: follower.host,
|
|
||||||
inbox: follower.host != null ? follower.inbox : undefined
|
|
||||||
},
|
|
||||||
_followee: {
|
|
||||||
host: followee.host,
|
|
||||||
inbox: followee.host != null ? followee.inbox : undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result.ok === 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const count = await Following.count({});
|
|
||||||
|
|
||||||
const dop = Number.parseInt(process.argv[2]) || 5
|
|
||||||
const idop = ((count - (count % dop)) / dop) + 1
|
|
||||||
|
|
||||||
return zip(
|
|
||||||
1,
|
|
||||||
async (time) => {
|
|
||||||
console.log(`${time} / ${idop}`)
|
|
||||||
const doc = await Following.find({}, {
|
|
||||||
limit: dop, skip: time * dop
|
|
||||||
})
|
|
||||||
return Promise.all(doc.map(migrate))
|
|
||||||
},
|
|
||||||
idop
|
|
||||||
).then(a => {
|
|
||||||
const rv = []
|
|
||||||
a.forEach(e => rv.push(...e))
|
|
||||||
return rv
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
main().then(console.dir).catch(console.error)
|
|
10
package.json
10
package.json
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"author": "syuilo <i@syuilo.com>",
|
"author": "syuilo <i@syuilo.com>",
|
||||||
"version": "1.7.0",
|
"version": "2.5.0",
|
||||||
"clientVersion": "1.0.5175",
|
"clientVersion": "1.0.5241",
|
||||||
"codename": "nighthike",
|
"codename": "nighthike",
|
||||||
"main": "./built/index.js",
|
"main": "./built/index.js",
|
||||||
"private": true,
|
"private": true,
|
||||||
@ -58,6 +58,7 @@
|
|||||||
"@types/koa-multer": "1.0.0",
|
"@types/koa-multer": "1.0.0",
|
||||||
"@types/koa-router": "7.0.28",
|
"@types/koa-router": "7.0.28",
|
||||||
"@types/koa-send": "4.1.1",
|
"@types/koa-send": "4.1.1",
|
||||||
|
"@types/koa-views": "^2.0.3",
|
||||||
"@types/koa__cors": "2.2.2",
|
"@types/koa__cors": "2.2.2",
|
||||||
"@types/kue": "0.11.8",
|
"@types/kue": "0.11.8",
|
||||||
"@types/license-checker": "15.0.0",
|
"@types/license-checker": "15.0.0",
|
||||||
@ -68,6 +69,7 @@
|
|||||||
"@types/ms": "0.7.30",
|
"@types/ms": "0.7.30",
|
||||||
"@types/node": "9.6.6",
|
"@types/node": "9.6.6",
|
||||||
"@types/nopt": "3.0.29",
|
"@types/nopt": "3.0.29",
|
||||||
|
"@types/parse5": "^3.0.0",
|
||||||
"@types/pug": "2.0.4",
|
"@types/pug": "2.0.4",
|
||||||
"@types/qrcode": "0.8.1",
|
"@types/qrcode": "0.8.1",
|
||||||
"@types/ratelimiter": "2.1.28",
|
"@types/ratelimiter": "2.1.28",
|
||||||
@ -76,6 +78,7 @@
|
|||||||
"@types/request-promise-native": "1.0.14",
|
"@types/request-promise-native": "1.0.14",
|
||||||
"@types/rimraf": "2.0.2",
|
"@types/rimraf": "2.0.2",
|
||||||
"@types/seedrandom": "2.4.27",
|
"@types/seedrandom": "2.4.27",
|
||||||
|
"@types/single-line-log": "^1.1.0",
|
||||||
"@types/speakeasy": "2.0.2",
|
"@types/speakeasy": "2.0.2",
|
||||||
"@types/tmp": "0.0.33",
|
"@types/tmp": "0.0.33",
|
||||||
"@types/uuid": "3.4.3",
|
"@types/uuid": "3.4.3",
|
||||||
@ -145,6 +148,7 @@
|
|||||||
"koa-router": "7.4.0",
|
"koa-router": "7.4.0",
|
||||||
"koa-send": "4.1.3",
|
"koa-send": "4.1.3",
|
||||||
"koa-slow": "2.1.0",
|
"koa-slow": "2.1.0",
|
||||||
|
"koa-views": "^6.1.4",
|
||||||
"kue": "0.11.6",
|
"kue": "0.11.6",
|
||||||
"license-checker": "18.0.0",
|
"license-checker": "18.0.0",
|
||||||
"loader-utils": "1.1.0",
|
"loader-utils": "1.1.0",
|
||||||
@ -163,6 +167,7 @@
|
|||||||
"object-assign-deep": "0.4.0",
|
"object-assign-deep": "0.4.0",
|
||||||
"on-build-webpack": "0.1.0",
|
"on-build-webpack": "0.1.0",
|
||||||
"os-utils": "0.0.14",
|
"os-utils": "0.0.14",
|
||||||
|
"parse5": "^4.0.0",
|
||||||
"progress-bar-webpack-plugin": "1.11.0",
|
"progress-bar-webpack-plugin": "1.11.0",
|
||||||
"prominence": "0.2.0",
|
"prominence": "0.2.0",
|
||||||
"promise-sequential": "^1.1.1",
|
"promise-sequential": "^1.1.1",
|
||||||
@ -180,6 +185,7 @@
|
|||||||
"s-age": "1.1.2",
|
"s-age": "1.1.2",
|
||||||
"sass-loader": "7.0.1",
|
"sass-loader": "7.0.1",
|
||||||
"seedrandom": "2.4.3",
|
"seedrandom": "2.4.3",
|
||||||
|
"single-line-log": "^1.1.2",
|
||||||
"speakeasy": "2.0.0",
|
"speakeasy": "2.0.0",
|
||||||
"style-loader": "0.21.0",
|
"style-loader": "0.21.0",
|
||||||
"stylus": "0.54.5",
|
"stylus": "0.54.5",
|
||||||
|
@ -7,7 +7,7 @@ import locale from '../../locales';
|
|||||||
export default class Replacer {
|
export default class Replacer {
|
||||||
private lang: string;
|
private lang: string;
|
||||||
|
|
||||||
public pattern = /%i18n:([a-z_\-@\.\!]+?)%/g;
|
public pattern = /%i18n:([a-z0-9_\-@\.\!]+?)%/g;
|
||||||
|
|
||||||
constructor(lang: string) {
|
constructor(lang: string) {
|
||||||
this.lang = lang;
|
this.lang = lang;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
block vars
|
||||||
|
|
||||||
doctype html
|
doctype html
|
||||||
|
|
||||||
!= '\n<!-- Thank you for using Misskey! @syuilo -->\n'
|
!= '\n<!-- Thank you for using Misskey! @syuilo -->\n'
|
||||||
@ -9,9 +11,17 @@ html
|
|||||||
meta(name='application-name' content='Misskey')
|
meta(name='application-name' content='Misskey')
|
||||||
meta(name='theme-color' content=themeColor)
|
meta(name='theme-color' content=themeColor)
|
||||||
meta(name='referrer' content='origin')
|
meta(name='referrer' content='origin')
|
||||||
|
meta(property='og:site_name' content='Misskey')
|
||||||
link(rel='manifest' href='/manifest.json')
|
link(rel='manifest' href='/manifest.json')
|
||||||
|
|
||||||
title Misskey
|
title
|
||||||
|
block title
|
||||||
|
| Misskey
|
||||||
|
|
||||||
|
block desc
|
||||||
|
meta(name='description' content='A SNS')
|
||||||
|
|
||||||
|
block meta
|
||||||
|
|
||||||
style
|
style
|
||||||
include ./../../../built/client/assets/init.css
|
include ./../../../built/client/assets/init.css
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<router-link class="mk-avatar" :to="user | userPage" :title="user | acct" :target="target" :style="{ borderRadius: clientSettings.circleIcons ? '100%' : null }">
|
<router-link class="mk-avatar" :to="user | userPage" :title="user | acct" :target="target" :style="style" v-if="disablePreview"></router-link>
|
||||||
<img v-if="disablePreview" :src="`${user.avatarUrl}?thumbnail&size=128`" alt=""/>
|
<router-link class="mk-avatar" :to="user | userPage" :title="user | acct" :target="target" :style="style" v-else v-user-preview="user.id"></router-link>
|
||||||
<img v-else :src="`${user.avatarUrl}?thumbnail&size=128`" alt="" v-user-preview="user.id"/>
|
|
||||||
</router-link>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@ -10,6 +8,7 @@ import Vue from 'vue';
|
|||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
props: {
|
props: {
|
||||||
user: {
|
user: {
|
||||||
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
@ -20,20 +19,24 @@ export default Vue.extend({
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
style(): any {
|
||||||
|
return {
|
||||||
|
backgroundColor: this.user.avatarColor ? `rgb(${ this.user.avatarColor.join(',') })` : null,
|
||||||
|
backgroundImage: `url(${ this.user.avatarUrl }?thumbnail)`,
|
||||||
|
borderRadius: (this as any).clientSettings.circleIcons ? '100%' : null
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
.mk-avatar
|
.mk-avatar
|
||||||
display block
|
display inline-block
|
||||||
|
vertical-align bottom
|
||||||
> img
|
background-size cover
|
||||||
display inline-block
|
background-position center center
|
||||||
width 100%
|
transition border-radius 1s ease
|
||||||
height 100%
|
|
||||||
margin 0
|
|
||||||
border-radius inherit
|
|
||||||
vertical-align bottom
|
|
||||||
transition border-radius 1s ease
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<div class="mk-media-list" :data-count="mediaList.length">
|
<div class="mk-media-list" :data-count="mediaList.length">
|
||||||
<template v-for="media in mediaList">
|
<template v-for="media in mediaList">
|
||||||
<mk-media-video :video="media" :key="media.id" v-if="media.type.startsWith('video')" :inline-playable="mediaList.length === 1"/>
|
<mk-media-video :video="media" :key="media.id" v-if="media.type.startsWith('video')" :inline-playable="mediaList.length === 1"/>
|
||||||
<mk-media-image :image="media" :key="media.id" v-else />
|
<mk-media-image :image="media" :key="media.id" v-else :raw="raw"/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -11,7 +11,14 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
props: ['mediaList'],
|
props: {
|
||||||
|
mediaList: {
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
raw: {
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -23,7 +30,7 @@ export default Vue.extend({
|
|||||||
|
|
||||||
@media (max-width 500px)
|
@media (max-width 500px)
|
||||||
height 192px
|
height 192px
|
||||||
|
|
||||||
&[data-count="1"]
|
&[data-count="1"]
|
||||||
grid-template-rows 1fr
|
grid-template-rows 1fr
|
||||||
&[data-count="2"]
|
&[data-count="2"]
|
||||||
@ -40,7 +47,7 @@ export default Vue.extend({
|
|||||||
&[data-count="4"]
|
&[data-count="4"]
|
||||||
grid-template-columns 1fr 1fr
|
grid-template-columns 1fr 1fr
|
||||||
grid-template-rows 1fr 1fr
|
grid-template-rows 1fr 1fr
|
||||||
|
|
||||||
:nth-child(1)
|
:nth-child(1)
|
||||||
grid-column 1 / 2
|
grid-column 1 / 2
|
||||||
grid-row 1 / 2
|
grid-row 1 / 2
|
||||||
@ -53,5 +60,5 @@ export default Vue.extend({
|
|||||||
:nth-child(4)
|
:nth-child(4)
|
||||||
grid-column 2 / 3
|
grid-column 2 / 3
|
||||||
grid-row 2 / 3
|
grid-row 2 / 3
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
<section v-if="invitations.length > 0">
|
<section v-if="invitations.length > 0">
|
||||||
<h2>対局の招待があります!:</h2>
|
<h2>対局の招待があります!:</h2>
|
||||||
<div class="invitation" v-for="i in invitations" tabindex="-1" @click="accept(i)">
|
<div class="invitation" v-for="i in invitations" tabindex="-1" @click="accept(i)">
|
||||||
<img :src="`${i.parent.avatarUrl}?thumbnail&size=32`" alt="">
|
<mk-avatar class="avatar" :user="i.parent"/>
|
||||||
<span class="name"><b>{{ i.parent.name }}</b></span>
|
<span class="name"><b>{{ i.parent.name }}</b></span>
|
||||||
<span class="username">@{{ i.parent.username }}</span>
|
<span class="username">@{{ i.parent.username }}</span>
|
||||||
<mk-time :time="i.createdAt"/>
|
<mk-time :time="i.createdAt"/>
|
||||||
@ -40,8 +40,8 @@
|
|||||||
<section v-if="myGames.length > 0">
|
<section v-if="myGames.length > 0">
|
||||||
<h2>自分の対局</h2>
|
<h2>自分の対局</h2>
|
||||||
<a class="game" v-for="g in myGames" tabindex="-1" @click.prevent="go(g)" :href="`/othello/${g.id}`">
|
<a class="game" v-for="g in myGames" tabindex="-1" @click.prevent="go(g)" :href="`/othello/${g.id}`">
|
||||||
<img :src="`${g.user1.avatarUrl}?thumbnail&size=32`" alt="">
|
<mk-avatar class="avatar" :user="g.user1"/>
|
||||||
<img :src="`${g.user2.avatarUrl}?thumbnail&size=32`" alt="">
|
<mk-avatar class="avatar" :user="g.user2"/>
|
||||||
<span><b>{{ g.user1.name }}</b> vs <b>{{ g.user2.name }}</b></span>
|
<span><b>{{ g.user1.name }}</b> vs <b>{{ g.user2.name }}</b></span>
|
||||||
<span class="state">{{ g.isEnded ? '終了' : '進行中' }}</span>
|
<span class="state">{{ g.isEnded ? '終了' : '進行中' }}</span>
|
||||||
</a>
|
</a>
|
||||||
@ -49,8 +49,8 @@
|
|||||||
<section v-if="games.length > 0">
|
<section v-if="games.length > 0">
|
||||||
<h2>みんなの対局</h2>
|
<h2>みんなの対局</h2>
|
||||||
<a class="game" v-for="g in games" tabindex="-1" @click.prevent="go(g)" :href="`/othello/${g.id}`">
|
<a class="game" v-for="g in games" tabindex="-1" @click.prevent="go(g)" :href="`/othello/${g.id}`">
|
||||||
<img :src="`${g.user1.avatarUrl}?thumbnail&size=32`" alt="">
|
<mk-avatar class="avatar" :user="g.user1"/>
|
||||||
<img :src="`${g.user2.avatarUrl}?thumbnail&size=32`" alt="">
|
<mk-avatar class="avatar" :user="g.user2"/>
|
||||||
<span><b>{{ g.user1.name }}</b> vs <b>{{ g.user2.name }}</b></span>
|
<span><b>{{ g.user1.name }}</b> vs <b>{{ g.user2.name }}</b></span>
|
||||||
<span class="state">{{ g.isEnded ? '終了' : '進行中' }}</span>
|
<span class="state">{{ g.isEnded ? '終了' : '進行中' }}</span>
|
||||||
</a>
|
</a>
|
||||||
@ -271,8 +271,9 @@ export default Vue.extend({
|
|||||||
&:active
|
&:active
|
||||||
background #eee
|
background #eee
|
||||||
|
|
||||||
> img
|
> .avatar
|
||||||
vertical-align bottom
|
width 32px
|
||||||
|
height 32px
|
||||||
border-radius 100%
|
border-radius 100%
|
||||||
|
|
||||||
> span
|
> span
|
||||||
@ -301,8 +302,9 @@ export default Vue.extend({
|
|||||||
&:active
|
&:active
|
||||||
background #eee
|
background #eee
|
||||||
|
|
||||||
> img
|
> .avatar
|
||||||
vertical-align bottom
|
width 32px
|
||||||
|
height 32px
|
||||||
border-radius 100%
|
border-radius 100%
|
||||||
|
|
||||||
> span
|
> span
|
||||||
|
@ -14,12 +14,20 @@ import Vue from 'vue';
|
|||||||
import MkMediaImageDialog from './media-image-dialog.vue';
|
import MkMediaImageDialog from './media-image-dialog.vue';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
props: ['image'],
|
props: {
|
||||||
|
image: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
raw: {
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
style(): any {
|
style(): any {
|
||||||
return {
|
return {
|
||||||
'background-color': this.image.properties.avgColor ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
|
'background-color': this.image.properties.avgColor ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
|
||||||
'background-image': `url(${this.image.url}?thumbnail&size=512)`
|
'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.url}?thumbnail&size=512)`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -52,6 +52,7 @@ export default Vue.extend({
|
|||||||
width 100%
|
width 100%
|
||||||
height 100%
|
height 100%
|
||||||
border-radius 4px
|
border-radius 4px
|
||||||
|
|
||||||
.mk-media-video-thumbnail
|
.mk-media-video-thumbnail
|
||||||
display flex
|
display flex
|
||||||
justify-content center
|
justify-content center
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
<mk-note-html v-if="p.text" :text="p.text" :i="os.i"/>
|
<mk-note-html v-if="p.text" :text="p.text" :i="os.i"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="media" v-if="p.media.length > 0">
|
<div class="media" v-if="p.media.length > 0">
|
||||||
<mk-media-list :media-list="p.media"/>
|
<mk-media-list :media-list="p.media" :raw="true"/>
|
||||||
</div>
|
</div>
|
||||||
<mk-poll v-if="p.poll" :note="p"/>
|
<mk-poll v-if="p.poll" :note="p"/>
|
||||||
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
|
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
|
||||||
|
@ -53,6 +53,7 @@ root(isDark)
|
|||||||
|
|
||||||
> header
|
> header
|
||||||
display flex
|
display flex
|
||||||
|
align-items baseline
|
||||||
white-space nowrap
|
white-space nowrap
|
||||||
|
|
||||||
> .name
|
> .name
|
||||||
|
@ -65,6 +65,7 @@ root(isDark)
|
|||||||
|
|
||||||
> header
|
> header
|
||||||
display flex
|
display flex
|
||||||
|
align-items baseline
|
||||||
margin-bottom 2px
|
margin-bottom 2px
|
||||||
white-space nowrap
|
white-space nowrap
|
||||||
line-height 21px
|
line-height 21px
|
||||||
|
@ -333,7 +333,7 @@ root(isDark)
|
|||||||
|
|
||||||
> .renote
|
> .renote
|
||||||
display flex
|
display flex
|
||||||
align-items baseline
|
align-items center
|
||||||
padding 16px 32px
|
padding 16px 32px
|
||||||
line-height 28px
|
line-height 28px
|
||||||
color #9dbb00
|
color #9dbb00
|
||||||
@ -400,7 +400,7 @@ root(isDark)
|
|||||||
|
|
||||||
> header
|
> header
|
||||||
display flex
|
display flex
|
||||||
align-items center
|
align-items baseline
|
||||||
margin-bottom 4px
|
margin-bottom 4px
|
||||||
white-space nowrap
|
white-space nowrap
|
||||||
|
|
||||||
|
@ -105,21 +105,11 @@ export default Vue.extend({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
|
||||||
text() {
|
|
||||||
this.saveDraft();
|
|
||||||
},
|
|
||||||
|
|
||||||
poll() {
|
|
||||||
this.saveDraft();
|
|
||||||
},
|
|
||||||
|
|
||||||
files() {
|
|
||||||
this.saveDraft();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
if (this.reply && this.reply.user.host != null) {
|
||||||
|
this.text = `@${this.reply.user.username}@${this.reply.user.host} `;
|
||||||
|
}
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
// 書きかけの投稿を復元
|
// 書きかけの投稿を復元
|
||||||
const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftId];
|
const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftId];
|
||||||
@ -134,10 +124,18 @@ export default Vue.extend({
|
|||||||
}
|
}
|
||||||
this.$emit('change-attached-media', this.files);
|
this.$emit('change-attached-media', this.files);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.$nextTick(() => this.watch());
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
watch() {
|
||||||
|
this.$watch('text', () => this.saveDraft());
|
||||||
|
this.$watch('poll', () => this.saveDraft());
|
||||||
|
this.$watch('files', () => this.saveDraft());
|
||||||
|
},
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
(this.$refs.text as any).focus();
|
(this.$refs.text as any).focus();
|
||||||
},
|
},
|
||||||
|
@ -83,6 +83,7 @@
|
|||||||
<el-option label="ja-JP" value="ja"/>
|
<el-option label="ja-JP" value="ja"/>
|
||||||
<el-option label="en-US" value="en"/>
|
<el-option label="en-US" value="en"/>
|
||||||
<el-option label="fr" value="fr"/>
|
<el-option label="fr" value="fr"/>
|
||||||
|
<el-option label="pl" value="pl"/>
|
||||||
</el-option-group>
|
</el-option-group>
|
||||||
</el-select>
|
</el-select>
|
||||||
<div class="none ui info">
|
<div class="none ui info">
|
||||||
|
@ -31,6 +31,12 @@ export default Vue.extend({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if ((this as any).os.i.followingCount == 0) {
|
||||||
|
this.src = 'local';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
(this.$refs.tl as any).$once('loaded', () => {
|
(this.$refs.tl as any).$once('loaded', () => {
|
||||||
this.$emit('loaded');
|
this.$emit('loaded');
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
<div class="header" :data-is-dark-background="user.bannerUrl != null">
|
<div class="header" :data-is-dark-background="user.bannerUrl != null">
|
||||||
<div class="is-suspended" v-if="user.isSuspended"><p>%fa:exclamation-triangle% %i18n:@is-suspended%</p></div>
|
<div class="is-suspended" v-if="user.isSuspended"><p>%fa:exclamation-triangle% %i18n:@is-suspended%</p></div>
|
||||||
<div class="is-remote" v-if="user.host != null"><p>%fa:exclamation-triangle% %i18n:@is-remote%<a :href="user.url || user.uri" target="_blank">%i18n:@view-remote%</a></p></div>
|
<div class="is-remote" v-if="user.host != null"><p>%fa:exclamation-triangle% %i18n:@is-remote%<a :href="user.url || user.uri" target="_blank">%i18n:@view-remote%</a></p></div>
|
||||||
<div class="banner-container" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''">
|
<div class="banner-container" :style="style">
|
||||||
<div class="banner" ref="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''" @click="onBannerClick"></div>
|
<div class="banner" ref="banner" :style="style" @click="onBannerClick"></div>
|
||||||
<div class="fade"></div>
|
<div class="fade"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@ -25,6 +25,15 @@ import Vue from 'vue';
|
|||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
props: ['user'],
|
props: ['user'],
|
||||||
|
computed: {
|
||||||
|
style(): any {
|
||||||
|
if (this.user.bannerUrl == null) return {};
|
||||||
|
return {
|
||||||
|
backgroundColor: this.user.bannerColor ? `rgb(${ this.user.bannerColor.join(',') })` : null,
|
||||||
|
backgroundImage: `url(${ this.user.bannerUrl })`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
if (this.user.bannerUrl) {
|
if (this.user.bannerUrl) {
|
||||||
window.addEventListener('load', this.onScroll);
|
window.addEventListener('load', this.onScroll);
|
||||||
|
@ -6,12 +6,20 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
props: ['image'],
|
props: {
|
||||||
|
image: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
raw: {
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
style(): any {
|
style(): any {
|
||||||
return {
|
return {
|
||||||
'background-color': this.image.properties.avgColor ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
|
'background-color': this.image.properties.avgColor ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
|
||||||
'background-image': `url(${this.image.url}?thumbnail&size=512)`
|
'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.url}?thumbnail&size=512)`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,6 +55,7 @@ root(isDark)
|
|||||||
|
|
||||||
> header
|
> header
|
||||||
display flex
|
display flex
|
||||||
|
align-items baseline
|
||||||
margin-bottom 4px
|
margin-bottom 4px
|
||||||
white-space nowrap
|
white-space nowrap
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
|
<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="media" v-if="p.media.length > 0">
|
<div class="media" v-if="p.media.length > 0">
|
||||||
<mk-media-list :media-list="p.media"/>
|
<mk-media-list :media-list="p.media" :raw="true"/>
|
||||||
</div>
|
</div>
|
||||||
<mk-poll v-if="p.poll" :note="p"/>
|
<mk-poll v-if="p.poll" :note="p"/>
|
||||||
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
|
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
|
||||||
|
@ -49,6 +49,7 @@ root(isDark)
|
|||||||
|
|
||||||
> header
|
> header
|
||||||
display flex
|
display flex
|
||||||
|
align-items baseline
|
||||||
margin-bottom 4px
|
margin-bottom 4px
|
||||||
white-space nowrap
|
white-space nowrap
|
||||||
|
|
||||||
|
@ -69,6 +69,7 @@ root(isDark)
|
|||||||
|
|
||||||
> header
|
> header
|
||||||
display flex
|
display flex
|
||||||
|
align-items baseline
|
||||||
margin-bottom 2px
|
margin-bottom 2px
|
||||||
white-space nowrap
|
white-space nowrap
|
||||||
|
|
||||||
|
@ -252,7 +252,7 @@ root(isDark)
|
|||||||
|
|
||||||
> .renote
|
> .renote
|
||||||
display flex
|
display flex
|
||||||
align-items baseline
|
align-items center
|
||||||
padding 8px 16px
|
padding 8px 16px
|
||||||
line-height 28px
|
line-height 28px
|
||||||
color #9dbb00
|
color #9dbb00
|
||||||
@ -333,7 +333,7 @@ root(isDark)
|
|||||||
|
|
||||||
> header
|
> header
|
||||||
display flex
|
display flex
|
||||||
align-items center
|
align-items baseline
|
||||||
white-space nowrap
|
white-space nowrap
|
||||||
|
|
||||||
@media (min-width 500px)
|
@media (min-width 500px)
|
||||||
|
@ -188,6 +188,11 @@ export default Vue.extend({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ((this as any).clientSettings.fetchOnScroll !== false) {
|
if ((this as any).clientSettings.fetchOnScroll !== false) {
|
||||||
|
// 親要素が display none だったら弾く
|
||||||
|
// https://github.com/syuilo/misskey/issues/1569
|
||||||
|
// http://d.hatena.ne.jp/favril/20091105/1257403319
|
||||||
|
if (this.$el.offsetHeight == 0) return;
|
||||||
|
|
||||||
const current = window.scrollY + window.innerHeight;
|
const current = window.scrollY + window.innerHeight;
|
||||||
if (current > document.body.offsetHeight - 8) this.loadMore();
|
if (current > document.body.offsetHeight - 8) this.loadMore();
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,7 @@ root(isDark)
|
|||||||
|
|
||||||
> header
|
> header
|
||||||
display flex
|
display flex
|
||||||
align-items center
|
align-items baseline
|
||||||
white-space nowrap
|
white-space nowrap
|
||||||
|
|
||||||
i, .mk-reaction-icon
|
i, .mk-reaction-icon
|
||||||
|
@ -25,13 +25,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false"/>
|
<mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false"/>
|
||||||
<mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/>
|
<mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/>
|
||||||
<button class="upload" @click="chooseFile">%fa:upload%</button>
|
<footer>
|
||||||
<button class="drive" @click="chooseFileFromDrive">%fa:cloud%</button>
|
<button class="upload" @click="chooseFile">%fa:upload%</button>
|
||||||
<button class="kao" @click="kao">%fa:R smile%</button>
|
<button class="drive" @click="chooseFileFromDrive">%fa:cloud%</button>
|
||||||
<button class="poll" @click="poll = true">%fa:chart-pie%</button>
|
<button class="kao" @click="kao">%fa:R smile%</button>
|
||||||
<button class="poll" @click="useCw = !useCw">%fa:eye-slash%</button>
|
<button class="poll" @click="poll = true">%fa:chart-pie%</button>
|
||||||
<button class="geo" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button>
|
<button class="poll" @click="useCw = !useCw">%fa:eye-slash%</button>
|
||||||
<button class="visibility" @click="setVisibility" ref="visibilityButton">%fa:lock%</button>
|
<button class="geo" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button>
|
||||||
|
<button class="visibility" @click="setVisibility" ref="visibilityButton">%fa:lock%</button>
|
||||||
|
</footer>
|
||||||
<input ref="file" class="file" type="file" accept="image/*" multiple="multiple" @change="onChangeFile"/>
|
<input ref="file" class="file" type="file" accept="image/*" multiple="multiple" @change="onChangeFile"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -67,6 +69,10 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
if (this.reply && this.reply.user.host != null) {
|
||||||
|
this.text = `@${this.reply.user.username}@${this.reply.user.host} `;
|
||||||
|
}
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.focus();
|
this.focus();
|
||||||
});
|
});
|
||||||
@ -332,24 +338,25 @@ root(isDark)
|
|||||||
> .file
|
> .file
|
||||||
display none
|
display none
|
||||||
|
|
||||||
> .upload
|
> footer
|
||||||
> .drive
|
white-space nowrap
|
||||||
> .kao
|
overflow auto
|
||||||
> .poll
|
-webkit-overflow-scrolling touch
|
||||||
> .geo
|
overflow-scrolling touch
|
||||||
> .visibility
|
|
||||||
display inline-block
|
> *
|
||||||
padding 0
|
display inline-block
|
||||||
margin 0
|
padding 0
|
||||||
width 48px
|
margin 0
|
||||||
height 48px
|
width 48px
|
||||||
font-size 20px
|
height 48px
|
||||||
color #657786
|
font-size 20px
|
||||||
background transparent
|
color #657786
|
||||||
outline none
|
background transparent
|
||||||
border none
|
outline none
|
||||||
border-radius 0
|
border none
|
||||||
box-shadow none
|
border-radius 0
|
||||||
|
box-shadow none
|
||||||
|
|
||||||
.mk-post-form[data-darkmode]
|
.mk-post-form[data-darkmode]
|
||||||
root(true)
|
root(true)
|
||||||
|
@ -75,6 +75,12 @@ export default Vue.extend({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if ((this as any).os.i.followingCount == 0) {
|
||||||
|
this.src = 'local';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
document.title = 'Misskey';
|
document.title = 'Misskey';
|
||||||
|
|
||||||
|
@ -5,11 +5,11 @@
|
|||||||
<div class="is-suspended" v-if="user.isSuspended"><p>%fa:exclamation-triangle% %i18n:@is-suspended%</p></div>
|
<div class="is-suspended" v-if="user.isSuspended"><p>%fa:exclamation-triangle% %i18n:@is-suspended%</p></div>
|
||||||
<div class="is-remote" v-if="user.host != null"><p>%fa:exclamation-triangle% %i18n:@is-remote%<a :href="user.url || user.uri" target="_blank">%i18n:@view-remote%</a></p></div>
|
<div class="is-remote" v-if="user.host != null"><p>%fa:exclamation-triangle% %i18n:@is-remote%<a :href="user.url || user.uri" target="_blank">%i18n:@view-remote%</a></p></div>
|
||||||
<header>
|
<header>
|
||||||
<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div>
|
<div class="banner" :style="style"></div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div class="top">
|
<div class="top">
|
||||||
<a class="avatar">
|
<a class="avatar">
|
||||||
<img :src="`${user.avatarUrl}?thumbnail&size=200`" alt="avatar"/>
|
<img :src="user.avatarUrl" alt="avatar"/>
|
||||||
</a>
|
</a>
|
||||||
<mk-follow-button v-if="os.isSignedIn && os.i.id != user.id" :user="user"/>
|
<mk-follow-button v-if="os.isSignedIn && os.i.id != user.id" :user="user"/>
|
||||||
</div>
|
</div>
|
||||||
@ -80,6 +80,13 @@ export default Vue.extend({
|
|||||||
computed: {
|
computed: {
|
||||||
age(): number {
|
age(): number {
|
||||||
return age(this.user.profile.birthday);
|
return age(this.user.profile.birthday);
|
||||||
|
},
|
||||||
|
style(): any {
|
||||||
|
if (this.user.bannerUrl == null) return {};
|
||||||
|
return {
|
||||||
|
backgroundColor: this.user.bannerColor ? `rgb(${ this.user.bannerColor.join(',') })` : null,
|
||||||
|
backgroundImage: `url(${ this.user.bannerUrl })`
|
||||||
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -14,13 +14,13 @@ mixin propTable(props)
|
|||||||
if prop.kind == 'id'
|
if prop.kind == 'id'
|
||||||
if prop.entity
|
if prop.entity
|
||||||
| (
|
| (
|
||||||
a(href=`/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity
|
a(href=`/docs/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity
|
||||||
| ID)
|
| ID)
|
||||||
else
|
else
|
||||||
| (ID)
|
| (ID)
|
||||||
else if prop.kind == 'entity'
|
else if prop.kind == 'entity'
|
||||||
| (
|
| (
|
||||||
a(href=`/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity
|
a(href=`/docs/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity
|
||||||
| )
|
| )
|
||||||
else if prop.kind == 'object'
|
else if prop.kind == 'object'
|
||||||
if prop.def
|
if prop.def
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"copyright": "Copyright (c) 2014-2018 syuilo",
|
"copyright": "Copyright (c) 2014-2018 syuilo",
|
||||||
"themeColor": "#5cbb2d",
|
"themeColor": "#f66e4f",
|
||||||
"themeColorForeground": "#fff"
|
"themeColorForeground": "#fff"
|
||||||
}
|
}
|
||||||
|
@ -25,10 +25,13 @@ export const getDriveFileBucket = async (): Promise<mongo.GridFSBucket> => {
|
|||||||
export type IMetadata = {
|
export type IMetadata = {
|
||||||
properties: any;
|
properties: any;
|
||||||
userId: mongo.ObjectID;
|
userId: mongo.ObjectID;
|
||||||
|
_user: any;
|
||||||
folderId: mongo.ObjectID;
|
folderId: mongo.ObjectID;
|
||||||
comment: string;
|
comment: string;
|
||||||
uri: string;
|
uri?: string;
|
||||||
|
url?: string;
|
||||||
deletedAt?: Date;
|
deletedAt?: Date;
|
||||||
|
isExpired?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IDriveFile = {
|
export type IDriveFile = {
|
||||||
|
@ -286,7 +286,7 @@ export const pack = async (
|
|||||||
_id: -1
|
_id: -1
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return prev ? prev._id : null;
|
return prev ? prev._id.toHexString() : null;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Get next note info
|
// Get next note info
|
||||||
@ -304,7 +304,7 @@ export const pack = async (
|
|||||||
_id: 1
|
_id: 1
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return next ? next._id : null;
|
return next ? next._id.toHexString() : null;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
if (_note.replyId) {
|
if (_note.replyId) {
|
||||||
|
@ -2,6 +2,7 @@ import { createQueue } from 'kue';
|
|||||||
|
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import http from './processors/http';
|
import http from './processors/http';
|
||||||
|
import { ILocalUser } from '../models/user';
|
||||||
|
|
||||||
const queue = createQueue({
|
const queue = createQueue({
|
||||||
redis: {
|
redis: {
|
||||||
@ -20,7 +21,7 @@ export function createHttp(data) {
|
|||||||
.backoff({ delay: 16384, type: 'exponential' });
|
.backoff({ delay: 16384, type: 'exponential' });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deliver(user, content, to) {
|
export function deliver(user: ILocalUser, content, to) {
|
||||||
createHttp({
|
createHttp({
|
||||||
title: 'deliver',
|
title: 'deliver',
|
||||||
type: 'deliver',
|
type: 'deliver',
|
||||||
|
@ -7,6 +7,7 @@ export default async (job: kue.Job, done): Promise<void> => {
|
|||||||
await request(job.data.user, job.data.to, job.data.content);
|
await request(job.data.user, job.data.to, job.data.content);
|
||||||
done();
|
done();
|
||||||
} catch (res) {
|
} catch (res) {
|
||||||
|
if (res.statusCode == null) return done();
|
||||||
if (res.statusCode >= 400 && res.statusCode < 500) {
|
if (res.statusCode >= 400 && res.statusCode < 500) {
|
||||||
// HTTPステータスコード4xxはクライアントエラーであり、それはつまり
|
// HTTPステータスコード4xxはクライアントエラーであり、それはつまり
|
||||||
// 何回再送しても成功することはないということなのでエラーにはしないでおく
|
// 何回再送しても成功することはないということなのでエラーにはしないでおく
|
||||||
|
@ -11,7 +11,12 @@ export default function(note: INote) {
|
|||||||
if (note.poll != null) {
|
if (note.poll != null) {
|
||||||
const url = `${config.url}/notes/${note._id}`;
|
const url = `${config.url}/notes/${note._id}`;
|
||||||
// TODO: i18n
|
// TODO: i18n
|
||||||
html += `<p>【投票】<br />${url}</p>`;
|
html += `<p><a href="${url}">【Misskeyで投票を見る】</a></p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note.renoteId != null) {
|
||||||
|
const url = `${config.url}/notes/${note.renoteId}`;
|
||||||
|
html += `<p>RE: <a href="${url}">${url}</a></p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
|
@ -24,7 +24,7 @@ export async function createImage(actor: IRemoteUser, value): Promise<IDriveFile
|
|||||||
|
|
||||||
log(`Creating the Image: ${image.url}`);
|
log(`Creating the Image: ${image.url}`);
|
||||||
|
|
||||||
return await uploadFromUrl(image.url, actor);
|
return await uploadFromUrl(image.url, actor, null, image.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import * as mongo from 'mongodb';
|
import * as mongo from 'mongodb';
|
||||||
import { JSDOM } from 'jsdom';
|
import * as parse5 from 'parse5';
|
||||||
import * as debug from 'debug';
|
import * as debug from 'debug';
|
||||||
|
|
||||||
import config from '../../../config';
|
import config from '../../../config';
|
||||||
@ -13,6 +13,82 @@ import { IRemoteUser } from '../../../models/user';
|
|||||||
|
|
||||||
const log = debug('misskey:activitypub');
|
const log = debug('misskey:activitypub');
|
||||||
|
|
||||||
|
function parse(html: string): string {
|
||||||
|
const dom = parse5.parseFragment(html) as parse5.AST.Default.Document;
|
||||||
|
|
||||||
|
let text = '';
|
||||||
|
|
||||||
|
dom.childNodes.forEach(n => analyze(n));
|
||||||
|
|
||||||
|
return text.trim();
|
||||||
|
|
||||||
|
function getText(node) {
|
||||||
|
if (node.nodeName == '#text') return node.value;
|
||||||
|
|
||||||
|
if (node.childNodes) {
|
||||||
|
return node.childNodes.map(n => getText(n)).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function analyze(node) {
|
||||||
|
switch (node.nodeName) {
|
||||||
|
case '#text':
|
||||||
|
text += node.value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'br':
|
||||||
|
text += '\n';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'a':
|
||||||
|
const cls = node.attrs
|
||||||
|
? (node.attrs.find(x => x.name == 'class') || { value: '' }).value.split(' ')
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// for Mastodon
|
||||||
|
if (cls.includes('mention')) {
|
||||||
|
const mention = getText(node);
|
||||||
|
|
||||||
|
const part = mention.split('@');
|
||||||
|
|
||||||
|
if (part.length == 2) {
|
||||||
|
//#region ホスト名部分が省略されているので復元する
|
||||||
|
|
||||||
|
const href = new URL(node.attrs.find(x => x.name == 'href').value);
|
||||||
|
const acct = mention + '@' + href.hostname;
|
||||||
|
text += acct;
|
||||||
|
break;
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
} else if (part.length == 3) {
|
||||||
|
text += mention;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.childNodes) {
|
||||||
|
node.childNodes.forEach(n => analyze(n));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'p':
|
||||||
|
text += '\n\n';
|
||||||
|
if (node.childNodes) {
|
||||||
|
node.childNodes.forEach(n => analyze(n));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (node.childNodes) {
|
||||||
|
node.childNodes.forEach(n => analyze(n));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Noteをフェッチします。
|
* Noteをフェッチします。
|
||||||
*
|
*
|
||||||
@ -87,9 +163,8 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
|
|||||||
// リプライ
|
// リプライ
|
||||||
const reply = note.inReplyTo ? await resolveNote(note.inReplyTo, resolver) : null;
|
const reply = note.inReplyTo ? await resolveNote(note.inReplyTo, resolver) : null;
|
||||||
|
|
||||||
// MastodonはHTMLを送り付けてくる
|
// テキストのパース
|
||||||
// そして改行は<br />で表現されている
|
const text = parse(note.content);
|
||||||
const { window } = new JSDOM(note.content.replace(/<br \/>/g, '\n'));
|
|
||||||
|
|
||||||
// ユーザーの情報が古かったらついでに更新しておく
|
// ユーザーの情報が古かったらついでに更新しておく
|
||||||
if (actor.updatedAt == null || Date.now() - actor.updatedAt.getTime() > 1000 * 60 * 60 * 24) {
|
if (actor.updatedAt == null || Date.now() - actor.updatedAt.getTime() > 1000 * 60 * 60 * 24) {
|
||||||
@ -101,7 +176,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
|
|||||||
media,
|
media,
|
||||||
reply,
|
reply,
|
||||||
renote: undefined,
|
renote: undefined,
|
||||||
text: window.document.body.textContent,
|
text: text,
|
||||||
viaMobile: false,
|
viaMobile: false,
|
||||||
geo: undefined,
|
geo: undefined,
|
||||||
visibility,
|
visibility,
|
||||||
|
@ -15,6 +15,7 @@ export interface IObject {
|
|||||||
icon?: any;
|
icon?: any;
|
||||||
image?: any;
|
image?: any;
|
||||||
url?: string;
|
url?: string;
|
||||||
|
tag?: any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IActivity extends IObject {
|
export interface IActivity extends IObject {
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
import { toUnicode, toASCII } from 'punycode';
|
import { toUnicode, toASCII } from 'punycode';
|
||||||
import User from '../models/user';
|
import User, { IUser } from '../models/user';
|
||||||
import webFinger from './webfinger';
|
import webFinger from './webfinger';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { createPerson } from './activitypub/models/person';
|
import { createPerson } from './activitypub/models/person';
|
||||||
|
|
||||||
export default async (username, _host, option) => {
|
export default async (username, _host, option?): Promise<IUser> => {
|
||||||
const usernameLower = username.toLowerCase();
|
const usernameLower = username.toLowerCase();
|
||||||
|
|
||||||
|
if (_host == null) {
|
||||||
|
return await User.findOne({ usernameLower });
|
||||||
|
}
|
||||||
|
|
||||||
const hostAscii = toASCII(_host).toLowerCase();
|
const hostAscii = toASCII(_host).toLowerCase();
|
||||||
const host = toUnicode(hostAscii);
|
const host = toUnicode(hostAscii);
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* 投稿を表す文字列を取得します。
|
* 投稿を表す文字列を取得します。
|
||||||
* @param {*} note 投稿
|
* @param {*} note (packされた)投稿
|
||||||
*/
|
*/
|
||||||
const summarize = (note: any): string => {
|
const summarize = (note: any): string => {
|
||||||
if (note.isHidden) {
|
if (note.isHidden) {
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import $ from 'cafy'; import ID from '../../../../cafy-id';
|
import $ from 'cafy'; import ID from '../../../../cafy-id';
|
||||||
import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../../../models/user';
|
import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../../../models/user';
|
||||||
import event from '../../../../publishers/stream';
|
import event from '../../../../publishers/stream';
|
||||||
|
import DriveFile from '../../../../models/drive-file';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update myself
|
* Update myself
|
||||||
@ -51,12 +52,34 @@ module.exports = async (params, user, app) => new Promise(async (res, rej) => {
|
|||||||
if (autoWatchErr) return rej('invalid autoWatch param');
|
if (autoWatchErr) return rej('invalid autoWatch param');
|
||||||
if (autoWatch != null) user.settings.autoWatch = autoWatch;
|
if (autoWatch != null) user.settings.autoWatch = autoWatch;
|
||||||
|
|
||||||
|
if (avatarId) {
|
||||||
|
const avatar = await DriveFile.findOne({
|
||||||
|
_id: avatarId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (avatar != null && avatar.metadata.properties.avgColor) {
|
||||||
|
user.avatarColor = avatar.metadata.properties.avgColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bannerId) {
|
||||||
|
const banner = await DriveFile.findOne({
|
||||||
|
_id: bannerId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (banner != null && banner.metadata.properties.avgColor) {
|
||||||
|
user.bannerColor = banner.metadata.properties.avgColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await User.update(user._id, {
|
await User.update(user._id, {
|
||||||
$set: {
|
$set: {
|
||||||
name: user.name,
|
name: user.name,
|
||||||
description: user.description,
|
description: user.description,
|
||||||
avatarId: user.avatarId,
|
avatarId: user.avatarId,
|
||||||
|
avatarColor: user.avatarColor,
|
||||||
bannerId: user.bannerId,
|
bannerId: user.bannerId,
|
||||||
|
bannerColor: user.bannerColor,
|
||||||
profile: user.profile,
|
profile: user.profile,
|
||||||
isBot: user.isBot,
|
isBot: user.isBot,
|
||||||
settings: user.settings
|
settings: user.settings
|
||||||
|
BIN
src/server/file/assets/cache-expired.png
Normal file
BIN
src/server/file/assets/cache-expired.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
@ -6,6 +6,8 @@ import * as mongodb from 'mongodb';
|
|||||||
import DriveFile, { getDriveFileBucket } from '../../models/drive-file';
|
import DriveFile, { getDriveFileBucket } from '../../models/drive-file';
|
||||||
import DriveFileThumbnail, { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail';
|
import DriveFileThumbnail, { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail';
|
||||||
|
|
||||||
|
const assets = `${__dirname}/../../server/file/assets/`;
|
||||||
|
|
||||||
const commonReadableHandlerGenerator = (ctx: Koa.Context) => (e: Error): void => {
|
const commonReadableHandlerGenerator = (ctx: Koa.Context) => (e: Error): void => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
ctx.status = 500;
|
ctx.status = 500;
|
||||||
@ -25,13 +27,17 @@ export default async function(ctx: Koa.Context) {
|
|||||||
|
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
ctx.status = 404;
|
ctx.status = 404;
|
||||||
await send(ctx, `${__dirname}/assets/dummy.png`);
|
await send(ctx, '/dummy.png', { root: assets });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.metadata.deletedAt) {
|
if (file.metadata.deletedAt) {
|
||||||
ctx.status = 410;
|
ctx.status = 410;
|
||||||
await send(ctx, `${__dirname}/assets/tombstone.png`);
|
if (file.metadata.isExpired) {
|
||||||
|
await send(ctx, '/cache-expired.png', { root: assets });
|
||||||
|
} else {
|
||||||
|
await send(ctx, '/tombstone.png', { root: assets });
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,14 +7,32 @@ import * as Koa from 'koa';
|
|||||||
import * as Router from 'koa-router';
|
import * as Router from 'koa-router';
|
||||||
import * as send from 'koa-send';
|
import * as send from 'koa-send';
|
||||||
import * as favicon from 'koa-favicon';
|
import * as favicon from 'koa-favicon';
|
||||||
|
import * as views from 'koa-views';
|
||||||
|
|
||||||
import docs from './docs';
|
import docs from './docs';
|
||||||
|
import User from '../../models/user';
|
||||||
|
import parseAcct from '../../acct/parse';
|
||||||
|
import { fa } from '../../build/fa';
|
||||||
|
import config from '../../config';
|
||||||
|
import Note, { pack as packNote } from '../../models/note';
|
||||||
|
import getNoteSummary from '../../renderers/get-note-summary';
|
||||||
|
const consts = require('../../const.json');
|
||||||
|
|
||||||
const client = `${__dirname}/../../client/`;
|
const client = `${__dirname}/../../client/`;
|
||||||
|
|
||||||
// Init app
|
// Init app
|
||||||
const app = new Koa();
|
const app = new Koa();
|
||||||
|
|
||||||
|
// Init renderer
|
||||||
|
app.use(views(__dirname + '/views', {
|
||||||
|
extension: 'pug',
|
||||||
|
options: {
|
||||||
|
config,
|
||||||
|
themeColor: consts.themeColor,
|
||||||
|
facss: fa.dom.css()
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
// Serve favicon
|
// Serve favicon
|
||||||
app.use(favicon(`${client}/assets/favicon.ico`));
|
app.use(favicon(`${client}/assets/favicon.ico`));
|
||||||
|
|
||||||
@ -42,17 +60,21 @@ router.get('/assets/*', async ctx => {
|
|||||||
|
|
||||||
// Apple touch icon
|
// Apple touch icon
|
||||||
router.get('/apple-touch-icon.png', async ctx => {
|
router.get('/apple-touch-icon.png', async ctx => {
|
||||||
await send(ctx, `${client}/assets/apple-touch-icon.png`);
|
await send(ctx, '/assets/apple-touch-icon.png', {
|
||||||
|
root: client
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ServiceWroker
|
// ServiceWroker
|
||||||
router.get(/^\/sw\.(.+?)\.js$/, async ctx => {
|
//router.get(/^\/sw\.(.+?)\.js$/, async ctx => {
|
||||||
await send(ctx, `${client}/assets/sw.${ctx.params[0]}.js`);
|
// await send(ctx, `${client}/assets/sw.${ctx.params[0]}.js`);
|
||||||
});
|
//});
|
||||||
|
|
||||||
// Manifest
|
// Manifest
|
||||||
router.get('/manifest.json', async ctx => {
|
router.get('/manifest.json', async ctx => {
|
||||||
await send(ctx, `${client}/assets/manifest.json`);
|
await send(ctx, '/assets/manifest.json', {
|
||||||
|
root: client
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
@ -63,6 +85,38 @@ router.use('/docs', docs.routes());
|
|||||||
// URL preview endpoint
|
// URL preview endpoint
|
||||||
router.get('/url', require('./url-preview'));
|
router.get('/url', require('./url-preview'));
|
||||||
|
|
||||||
|
//#region for crawlers
|
||||||
|
// User
|
||||||
|
router.get('/@:user', async ctx => {
|
||||||
|
const { username, host } = parseAcct(ctx.params.user);
|
||||||
|
const user = await User.findOne({
|
||||||
|
usernameLower: username.toLowerCase(),
|
||||||
|
host
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user != null) {
|
||||||
|
await ctx.render('user', { user });
|
||||||
|
} else {
|
||||||
|
ctx.status = 404;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Note
|
||||||
|
router.get('/notes/:note', async ctx => {
|
||||||
|
const note = await Note.findOne({ _id: ctx.params.note });
|
||||||
|
|
||||||
|
if (note != null) {
|
||||||
|
const _note = await packNote(note);
|
||||||
|
await ctx.render('note', {
|
||||||
|
note: _note,
|
||||||
|
summary: getNoteSummary(_note)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ctx.status = 404;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
//#endregion
|
||||||
|
|
||||||
// Render base html for all requests
|
// Render base html for all requests
|
||||||
router.get('*', async ctx => {
|
router.get('*', async ctx => {
|
||||||
await send(ctx, `app/base.html`, {
|
await send(ctx, `app/base.html`, {
|
||||||
|
@ -14,6 +14,8 @@ module.exports = async (ctx: Koa.Context) => {
|
|||||||
|
|
||||||
function wrap(url: string): string {
|
function wrap(url: string): string {
|
||||||
return url != null
|
return url != null
|
||||||
? `https://images.weserv.nl/?url=${url.replace(/^https?:\/\//, '')}`
|
? url.startsWith('https://')
|
||||||
|
? url
|
||||||
|
: `https://images.weserv.nl/?url=${url.replace(/^http:\/\//, '')}`
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
25
src/server/web/views/note.pug
Normal file
25
src/server/web/views/note.pug
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
extends ../../../../src/client/app/base
|
||||||
|
|
||||||
|
block vars
|
||||||
|
- const user = note.user;
|
||||||
|
- const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`;
|
||||||
|
- const url = `${config.url}/notes/${note.id}`;
|
||||||
|
|
||||||
|
block title
|
||||||
|
= `${title} | Misskey`
|
||||||
|
|
||||||
|
block desc
|
||||||
|
meta(name='description' content= summary)
|
||||||
|
|
||||||
|
block meta
|
||||||
|
meta(name='twitter:card' content='summary')
|
||||||
|
meta(property='og:type' content='article')
|
||||||
|
meta(property='og:title' content= title)
|
||||||
|
meta(property='og:description' content= summary)
|
||||||
|
meta(property='og:url' content= url)
|
||||||
|
meta(property='og:image' content= user.avatarUrl)
|
||||||
|
|
||||||
|
if note.prev
|
||||||
|
link(rel='prev' href=`${config.url}/notes/${note.prev}`)
|
||||||
|
if note.next
|
||||||
|
link(rel='next' href=`${config.url}/notes/${note.next}`)
|
20
src/server/web/views/user.pug
Normal file
20
src/server/web/views/user.pug
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
extends ../../../../src/client/app/base
|
||||||
|
|
||||||
|
block vars
|
||||||
|
- const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`;
|
||||||
|
- const url = config.url + '/@' + (user.host ? `${user.username}@${user.host}` : user.username);
|
||||||
|
- const img = user.avatarId ? `${config.drive_url}/${user.avatarId}` : null;
|
||||||
|
|
||||||
|
block title
|
||||||
|
= `${title} | Misskey`
|
||||||
|
|
||||||
|
block desc
|
||||||
|
meta(name='description' content= user.description)
|
||||||
|
|
||||||
|
block meta
|
||||||
|
meta(name='twitter:card' content='summary')
|
||||||
|
meta(property='og:type' content='blog')
|
||||||
|
meta(property='og:title' content= title)
|
||||||
|
meta(property='og:description' content= user.description)
|
||||||
|
meta(property='og:url' content= url)
|
||||||
|
meta(property='og:image' content= img)
|
@ -15,7 +15,7 @@ import DriveFolder from '../../models/drive-folder';
|
|||||||
import { pack } from '../../models/drive-file';
|
import { pack } from '../../models/drive-file';
|
||||||
import event, { publishDriveStream } from '../../publishers/stream';
|
import event, { publishDriveStream } from '../../publishers/stream';
|
||||||
import getAcct from '../../acct/render';
|
import getAcct from '../../acct/render';
|
||||||
import { IUser, isLocalUser } from '../../models/user';
|
import { IUser, isLocalUser, isRemoteUser } from '../../models/user';
|
||||||
import DriveFileThumbnail, { getDriveFileThumbnailBucket, DriveFileThumbnailChunk } from '../../models/drive-file-thumbnail';
|
import DriveFileThumbnail, { getDriveFileThumbnailBucket, DriveFileThumbnailChunk } from '../../models/drive-file-thumbnail';
|
||||||
import genThumbnail from '../../drive/gen-thumbnail';
|
import genThumbnail from '../../drive/gen-thumbnail';
|
||||||
|
|
||||||
@ -62,6 +62,7 @@ const addFile = async (
|
|||||||
comment: string = null,
|
comment: string = null,
|
||||||
folderId: mongodb.ObjectID = null,
|
folderId: mongodb.ObjectID = null,
|
||||||
force: boolean = false,
|
force: boolean = false,
|
||||||
|
url: string = null,
|
||||||
uri: string = null
|
uri: string = null
|
||||||
): Promise<IDriveFile> => {
|
): Promise<IDriveFile> => {
|
||||||
log(`registering ${name} (user: ${getAcct(user)}, path: ${path})`);
|
log(`registering ${name} (user: ${getAcct(user)}, path: ${path})`);
|
||||||
@ -201,7 +202,10 @@ const addFile = async (
|
|||||||
// Calculate drive usage
|
// Calculate drive usage
|
||||||
const usage = await DriveFile
|
const usage = await DriveFile
|
||||||
.aggregate([{
|
.aggregate([{
|
||||||
$match: { 'metadata.userId': user._id }
|
$match: {
|
||||||
|
'metadata.userId': user._id,
|
||||||
|
'metadata.deletedAt': { $exists: false }
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
$project: {
|
$project: {
|
||||||
length: true
|
length: true
|
||||||
@ -245,7 +249,8 @@ const addFile = async (
|
|||||||
|
|
||||||
DriveFile.update({ _id: oldFile._id }, {
|
DriveFile.update({ _id: oldFile._id }, {
|
||||||
$set: {
|
$set: {
|
||||||
'metadata.deletedAt': new Date()
|
'metadata.deletedAt': new Date(),
|
||||||
|
'metadata.isExpired': true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -284,11 +289,18 @@ const addFile = async (
|
|||||||
|
|
||||||
const metadata = {
|
const metadata = {
|
||||||
userId: user._id,
|
userId: user._id,
|
||||||
|
_user: {
|
||||||
|
host: user.host
|
||||||
|
},
|
||||||
folderId: folder !== null ? folder._id : null,
|
folderId: folder !== null ? folder._id : null,
|
||||||
comment: comment,
|
comment: comment,
|
||||||
properties: properties
|
properties: properties
|
||||||
} as IMetadata;
|
} as IMetadata;
|
||||||
|
|
||||||
|
if (url !== null) {
|
||||||
|
metadata.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
if (uri !== null) {
|
if (uri !== null) {
|
||||||
metadata.uri = uri;
|
metadata.uri = uri;
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ export default async (url, user, folderId = null, uri = null): Promise<IDriveFil
|
|||||||
let error;
|
let error;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
driveFile = await create(user, path, name, null, folderId, false, uri);
|
driveFile = await create(user, path, name, null, folderId, false, url, uri);
|
||||||
log(`created: ${driveFile._id}`);
|
log(`created: ${driveFile._id}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e;
|
error = e;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import Note, { pack, INote } from '../../models/note';
|
import Note, { pack, INote } from '../../models/note';
|
||||||
import User, { isLocalUser, IUser, isRemoteUser } from '../../models/user';
|
import User, { isLocalUser, IUser, isRemoteUser, IRemoteUser, ILocalUser } from '../../models/user';
|
||||||
import stream, { publishLocalTimelineStream, publishGlobalTimelineStream, publishUserListStream } from '../../publishers/stream';
|
import stream, { publishLocalTimelineStream, publishGlobalTimelineStream, publishUserListStream } from '../../publishers/stream';
|
||||||
import Following from '../../models/following';
|
import Following from '../../models/following';
|
||||||
import { deliver } from '../../queue';
|
import { deliver } from '../../queue';
|
||||||
@ -17,6 +17,62 @@ import event from '../../publishers/stream';
|
|||||||
import parse from '../../text/parse';
|
import parse from '../../text/parse';
|
||||||
import { IApp } from '../../models/app';
|
import { IApp } from '../../models/app';
|
||||||
import UserList from '../../models/user-list';
|
import UserList from '../../models/user-list';
|
||||||
|
import resolveUser from '../../remote/resolve-user';
|
||||||
|
|
||||||
|
type Reason = 'reply' | 'quote' | 'mention';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ServiceWorkerへの通知を担当
|
||||||
|
*/
|
||||||
|
class NotificationManager {
|
||||||
|
private user: IUser;
|
||||||
|
private note: any;
|
||||||
|
private list: Array<{
|
||||||
|
user: ILocalUser['_id'],
|
||||||
|
reason: Reason;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
constructor(user, note) {
|
||||||
|
this.user = user;
|
||||||
|
this.note = note;
|
||||||
|
}
|
||||||
|
|
||||||
|
public push(user: ILocalUser['_id'], reason: Reason) {
|
||||||
|
// 自分自身へは通知しない
|
||||||
|
if (this.user._id.equals(user)) return;
|
||||||
|
|
||||||
|
const exist = this.list.find(x => x.user.equals(user));
|
||||||
|
|
||||||
|
if (exist) {
|
||||||
|
// 「メンションされているかつ返信されている」場合は、メンションとしての通知ではなく返信としての通知にする
|
||||||
|
if (reason != 'mention') {
|
||||||
|
exist.reason = reason;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.list.push({
|
||||||
|
user, reason
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public deliver() {
|
||||||
|
this.list.forEach(async x => {
|
||||||
|
const mentionee = x.user;
|
||||||
|
|
||||||
|
// ミュート情報を取得
|
||||||
|
const mentioneeMutes = await Mute.find({
|
||||||
|
muterId: mentionee
|
||||||
|
});
|
||||||
|
|
||||||
|
const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString());
|
||||||
|
|
||||||
|
// 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
|
||||||
|
if (!mentioneesMutedUserIds.includes(this.user._id.toString())) {
|
||||||
|
pushSw(mentionee, x.reason, this.note);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default async (user: IUser, data: {
|
export default async (user: IUser, data: {
|
||||||
createdAt?: Date;
|
createdAt?: Date;
|
||||||
@ -40,7 +96,7 @@ export default async (user: IUser, data: {
|
|||||||
|
|
||||||
const tags = data.tags || [];
|
const tags = data.tags || [];
|
||||||
|
|
||||||
let tokens = null;
|
let tokens: any[] = null;
|
||||||
|
|
||||||
if (data.text) {
|
if (data.text) {
|
||||||
// Analyze
|
// Analyze
|
||||||
@ -119,152 +175,142 @@ export default async (user: IUser, data: {
|
|||||||
// Serialize
|
// Serialize
|
||||||
const noteObj = await pack(note);
|
const noteObj = await pack(note);
|
||||||
|
|
||||||
// タイムラインへの投稿
|
const nm = new NotificationManager(user, noteObj);
|
||||||
if (note.channelId == null) {
|
|
||||||
if (!silent) {
|
|
||||||
if (isLocalUser(user)) {
|
|
||||||
if (note.visibility == 'private' || note.visibility == 'followers' || note.visibility == 'specified') {
|
|
||||||
// Publish event to myself's stream
|
|
||||||
stream(note.userId, 'note', await pack(note, user, {
|
|
||||||
detail: true
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
// Publish event to myself's stream
|
|
||||||
stream(note.userId, 'note', noteObj);
|
|
||||||
|
|
||||||
// Publish note to local timeline stream
|
const render = async () => {
|
||||||
if (note.visibility != 'home') {
|
const content = data.renote && data.text == null
|
||||||
publishLocalTimelineStream(noteObj);
|
? renderAnnounce(data.renote.uri ? data.renote.uri : await renderNote(data.renote))
|
||||||
}
|
: renderCreate(await renderNote(note));
|
||||||
|
return packAp(content);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!silent) {
|
||||||
|
if (isLocalUser(user)) {
|
||||||
|
if (note.visibility == 'private' || note.visibility == 'followers' || note.visibility == 'specified') {
|
||||||
|
// Publish event to myself's stream
|
||||||
|
stream(note.userId, 'note', await pack(note, user, {
|
||||||
|
detail: true
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
// Publish event to myself's stream
|
||||||
|
stream(note.userId, 'note', noteObj);
|
||||||
|
|
||||||
|
// Publish note to local timeline stream
|
||||||
|
if (note.visibility != 'home') {
|
||||||
|
publishLocalTimelineStream(noteObj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Publish note to global timeline stream
|
// Publish note to global timeline stream
|
||||||
publishGlobalTimelineStream(noteObj);
|
publishGlobalTimelineStream(noteObj);
|
||||||
|
|
||||||
if (note.visibility == 'specified') {
|
if (note.visibility == 'specified') {
|
||||||
data.visibleUsers.forEach(async u => {
|
data.visibleUsers.forEach(async u => {
|
||||||
stream(u._id, 'note', await pack(note, u, {
|
stream(u._id, 'note', await pack(note, u, {
|
||||||
detail: true
|
detail: true
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note.visibility == 'public' || note.visibility == 'home' || note.visibility == 'followers') {
|
if (note.visibility == 'public' || note.visibility == 'home' || note.visibility == 'followers') {
|
||||||
// フォロワーに配信
|
// フォロワーに配信
|
||||||
Following.find({
|
Following.find({
|
||||||
followeeId: note.userId
|
followeeId: note.userId
|
||||||
}).then(followers => {
|
}).then(followers => {
|
||||||
followers.map(async following => {
|
followers.map(async following => {
|
||||||
const follower = following._follower;
|
const follower = following._follower;
|
||||||
|
|
||||||
if (isLocalUser(follower)) {
|
if (isLocalUser(follower)) {
|
||||||
// ストーキングしていない場合
|
// ストーキングしていない場合
|
||||||
if (!following.stalk) {
|
if (!following.stalk) {
|
||||||
// この投稿が返信ならスキップ
|
// この投稿が返信ならスキップ
|
||||||
if (note.replyId && !note._reply.userId.equals(following.followerId) && !note._reply.userId.equals(note.userId)) return;
|
if (note.replyId && !note._reply.userId.equals(following.followerId) && !note._reply.userId.equals(note.userId)) return;
|
||||||
}
|
|
||||||
|
|
||||||
// Publish event to followers stream
|
|
||||||
stream(following.followerId, 'note', noteObj);
|
|
||||||
} else {
|
|
||||||
//#region AP配送
|
|
||||||
// フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信
|
|
||||||
if (isLocalUser(user)) {
|
|
||||||
deliver(user, await render(), follower.inbox);
|
|
||||||
}
|
|
||||||
//#endergion
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// リストに配信
|
// Publish event to followers stream
|
||||||
UserList.find({
|
stream(following.followerId, 'note', noteObj);
|
||||||
userIds: note.userId
|
} else {
|
||||||
}).then(lists => {
|
//#region AP配送
|
||||||
lists.forEach(list => {
|
// フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信
|
||||||
publishUserListStream(list._id, 'note', noteObj);
|
if (isLocalUser(user)) {
|
||||||
|
deliver(user, await render(), follower.inbox);
|
||||||
|
}
|
||||||
|
//#endergion
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region リプライとAnnounceのAP配送
|
// リストに配信
|
||||||
const render = async () => {
|
UserList.find({
|
||||||
const content = data.renote && data.text == null
|
userIds: note.userId
|
||||||
? renderAnnounce(data.renote.uri ? data.renote.uri : await renderNote(data.renote))
|
}).then(lists => {
|
||||||
: renderCreate(await renderNote(note));
|
lists.forEach(list => {
|
||||||
return packAp(content);
|
publishUserListStream(list._id, 'note', noteObj);
|
||||||
};
|
});
|
||||||
|
});
|
||||||
// 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送
|
|
||||||
if (data.reply && isLocalUser(user) && isRemoteUser(data.reply._user)) {
|
|
||||||
deliver(user, await render(), data.reply._user.inbox);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 投稿がRenoteかつ投稿者がローカルユーザーかつRenote元の投稿の投稿者がリモートユーザーなら配送
|
|
||||||
if (data.renote && isLocalUser(user) && isRemoteUser(data.renote._user)) {
|
|
||||||
deliver(user, await render(), data.renote._user.inbox);
|
|
||||||
}
|
|
||||||
//#endergion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// チャンネルへの投稿
|
//#region リプライとAnnounceのAP配送
|
||||||
/* TODO
|
|
||||||
if (note.channelId) {
|
// 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送
|
||||||
promises.push(
|
if (data.reply && isLocalUser(user) && isRemoteUser(data.reply._user)) {
|
||||||
// Increment channel index(notes count)
|
deliver(user, await render(), data.reply._user.inbox);
|
||||||
Channel.update({ _id: note.channelId }, {
|
}
|
||||||
$inc: {
|
|
||||||
index: 1
|
// 投稿がRenoteかつ投稿者がローカルユーザーかつRenote元の投稿の投稿者がリモートユーザーなら配送
|
||||||
|
if (data.renote && isLocalUser(user) && isRemoteUser(data.renote._user)) {
|
||||||
|
deliver(user, await render(), data.renote._user.inbox);
|
||||||
|
}
|
||||||
|
//#endergion
|
||||||
|
|
||||||
|
//#region メンション
|
||||||
|
if (data.text) {
|
||||||
|
// TODO: Drop dupulicates
|
||||||
|
const mentions = tokens
|
||||||
|
.filter(t => t.type == 'mention');
|
||||||
|
|
||||||
|
let mentionedUsers = await Promise.all(mentions.map(async m => {
|
||||||
|
try {
|
||||||
|
return await resolveUser(m.username, m.host);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// TODO: Drop dupulicates
|
||||||
|
mentionedUsers = mentionedUsers.filter(x => x != null);
|
||||||
|
|
||||||
|
mentionedUsers.filter(u => isLocalUser(u)).forEach(async u => {
|
||||||
|
// 既に言及されたユーザーに対する返信や引用renoteの場合も無視
|
||||||
|
if (data.reply && data.reply.userId.equals(u._id)) return;
|
||||||
|
if (data.renote && data.renote.userId.equals(u._id)) return;
|
||||||
|
|
||||||
|
// Create notification
|
||||||
|
notify(u._id, user._id, 'mention', {
|
||||||
|
noteId: note._id
|
||||||
|
});
|
||||||
|
|
||||||
|
nm.push(u._id, 'mention');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLocalUser(user)) {
|
||||||
|
mentionedUsers.filter(u => isRemoteUser(u)).forEach(async u => {
|
||||||
|
deliver(user, await render(), (u as IRemoteUser).inbox);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append mentions data
|
||||||
|
if (mentionedUsers.length > 0) {
|
||||||
|
Note.update({ _id: note._id }, {
|
||||||
|
$set: {
|
||||||
|
mentions: mentionedUsers.map(u => u._id)
|
||||||
}
|
}
|
||||||
}),
|
|
||||||
|
|
||||||
// Publish event to channel
|
|
||||||
promisedNoteObj.then(noteObj => {
|
|
||||||
publishChannelStream(note.channelId, 'note', noteObj);
|
|
||||||
}),
|
|
||||||
|
|
||||||
Promise.all([
|
|
||||||
promisedNoteObj,
|
|
||||||
|
|
||||||
// Get channel watchers
|
|
||||||
ChannelWatching.find({
|
|
||||||
channelId: note.channelId,
|
|
||||||
// 削除されたドキュメントは除く
|
|
||||||
deletedAt: { $exists: false }
|
|
||||||
})
|
|
||||||
]).then(([noteObj, watches]) => {
|
|
||||||
// チャンネルの視聴者(のタイムライン)に配信
|
|
||||||
watches.forEach(w => {
|
|
||||||
stream(w.userId, 'note', noteObj);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}*/
|
|
||||||
|
|
||||||
const mentions = [];
|
|
||||||
|
|
||||||
async function addMention(mentionee, reason) {
|
|
||||||
// Reject if already added
|
|
||||||
if (mentions.some(x => x.equals(mentionee))) return;
|
|
||||||
|
|
||||||
// Add mention
|
|
||||||
mentions.push(mentionee);
|
|
||||||
|
|
||||||
// Publish event
|
|
||||||
if (!user._id.equals(mentionee)) {
|
|
||||||
const mentioneeMutes = await Mute.find({
|
|
||||||
muter_id: mentionee,
|
|
||||||
deleted_at: { $exists: false }
|
|
||||||
});
|
});
|
||||||
const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString());
|
|
||||||
if (mentioneesMutedUserIds.indexOf(user._id.toString()) == -1) {
|
|
||||||
event(mentionee, reason, noteObj);
|
|
||||||
pushSw(mentionee, reason, noteObj);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
// If has in reply to note
|
// If has in reply to note
|
||||||
if (data.reply) {
|
if (data.reply) {
|
||||||
@ -303,8 +349,7 @@ export default async (user: IUser, data: {
|
|||||||
watch(user._id, data.reply);
|
watch(user._id, data.reply);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add mention
|
nm.push(data.reply.userId, 'reply');
|
||||||
addMention(data.reply.userId, 'reply');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it is renote
|
// If it is renote
|
||||||
@ -339,7 +384,7 @@ export default async (user: IUser, data: {
|
|||||||
// If it is quote renote
|
// If it is quote renote
|
||||||
if (data.text) {
|
if (data.text) {
|
||||||
// Add mention
|
// Add mention
|
||||||
addMention(data.renote.userId, 'quote');
|
nm.push(data.renote.userId, 'quote');
|
||||||
} else {
|
} else {
|
||||||
// Publish event
|
// Publish event
|
||||||
if (!user._id.equals(data.renote.userId)) {
|
if (!user._id.equals(data.renote.userId)) {
|
||||||
@ -365,48 +410,4 @@ export default async (user: IUser, data: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If has text content
|
|
||||||
if (data.text) {
|
|
||||||
// Extract an '@' mentions
|
|
||||||
const atMentions = tokens
|
|
||||||
.filter(t => t.type == 'mention')
|
|
||||||
.map(m => m.username)
|
|
||||||
// Drop dupulicates
|
|
||||||
.filter((v, i, s) => s.indexOf(v) == i);
|
|
||||||
|
|
||||||
// Resolve all mentions
|
|
||||||
await Promise.all(atMentions.map(async mention => {
|
|
||||||
// Fetch mentioned user
|
|
||||||
// SELECT _id
|
|
||||||
const mentionee = await User
|
|
||||||
.findOne({
|
|
||||||
usernameLower: mention.toLowerCase()
|
|
||||||
}, { _id: true });
|
|
||||||
|
|
||||||
// When mentioned user not found
|
|
||||||
if (mentionee == null) return;
|
|
||||||
|
|
||||||
// 既に言及されたユーザーに対する返信や引用renoteの場合も無視
|
|
||||||
if (data.reply && data.reply.userId.equals(mentionee._id)) return;
|
|
||||||
if (data.renote && data.renote.userId.equals(mentionee._id)) return;
|
|
||||||
|
|
||||||
// Add mention
|
|
||||||
addMention(mentionee._id, 'mention');
|
|
||||||
|
|
||||||
// Create notification
|
|
||||||
notify(mentionee._id, user._id, 'mention', {
|
|
||||||
noteId: note._id
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append mentions data
|
|
||||||
if (mentions.length > 0) {
|
|
||||||
Note.update({ _id: note._id }, {
|
|
||||||
$set: {
|
|
||||||
mentions
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user