mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-11-06 04:25:00 +00:00
Merge branch 'nutomic-federation' into federation
This commit is contained in:
commit
a94537bfd1
124 changed files with 8113 additions and 5973 deletions
13
.dockerignore
vendored
13
.dockerignore
vendored
|
@ -1,6 +1,11 @@
|
|||
# build folders and similar which are not needed for the docker build
|
||||
ui/node_modules
|
||||
server/target/debug
|
||||
!server/target/debug/lemmy_server
|
||||
server/target/release
|
||||
server/target/debug/incremental
|
||||
server/target
|
||||
docker/dev/volumes
|
||||
docker/federation-test/volumes
|
||||
.git
|
||||
ansible
|
||||
|
||||
# exceptions, needed for federation-test build
|
||||
|
||||
!server/target/debug/lemmy_server
|
||||
|
|
10
.gitignore
vendored
10
.gitignore
vendored
|
@ -1,7 +1,17 @@
|
|||
# local ansible configuration
|
||||
ansible/inventory
|
||||
ansible/inventory_dev
|
||||
ansible/passwords/
|
||||
|
||||
# docker build files
|
||||
docker/lemmy_mine.hjson
|
||||
docker/dev/env_deploy.sh
|
||||
docker/federation-test/volumes
|
||||
docker/dev/volumes
|
||||
|
||||
# local build files
|
||||
build/
|
||||
ui/src/translations
|
||||
|
||||
# ide config
|
||||
.idea/
|
||||
|
|
2
CODE_OF_CONDUCT.md
vendored
2
CODE_OF_CONDUCT.md
vendored
|
@ -19,7 +19,7 @@ These are the policies for upholding our community’s standards of conduct. If
|
|||
|
||||
1. Remarks that violate the Lemmy standards of conduct, including hateful, hurtful, oppressive, or exclusionary remarks, are not allowed. (Cursing is allowed, but never targeting another user, and never in a hateful manner.)
|
||||
2. Remarks that moderators find inappropriate, whether listed in the code of conduct or not, are also not allowed.
|
||||
3. Moderators will first respond to such remarks with a warning.
|
||||
3. Moderators will first respond to such remarks with a warning, at the same time the offending content will likely be removed whenever possible.
|
||||
4. If the warning is unheeded, the user will be “kicked,” i.e., kicked out of the communication channel to cool off.
|
||||
5. If the user comes back and continues to make trouble, they will be banned, i.e., indefinitely excluded.
|
||||
6. Moderators may choose at their discretion to un-ban the user if it was a first offense and they offer the offended party a genuine apology.
|
||||
|
|
34
README.md
vendored
34
README.md
vendored
|
@ -4,6 +4,7 @@
|
|||
[![Build Status](https://travis-ci.org/dessalines/lemmy.svg?branch=master)](https://travis-ci.org/dessalines/lemmy)
|
||||
[![GitHub issues](https://img.shields.io/github/issues-raw/dessalines/lemmy.svg)](https://github.com/dessalines/lemmy/issues)
|
||||
[![Docker Pulls](https://img.shields.io/docker/pulls/dessalines/lemmy.svg)](https://cloud.docker.com/repository/docker/dessalines/lemmy/)
|
||||
[![Translation status](http://weblate.yerbamate.dev/widgets/lemmy/-/lemmy/svg-badge.svg)](http://weblate.yerbamate.dev/engage/lemmy/)
|
||||
[![License](https://img.shields.io/github/license/dessalines/lemmy.svg)](LICENSE)
|
||||
![GitHub stars](https://img.shields.io/github/stars/dessalines/lemmy?style=social)
|
||||
</div>
|
||||
|
@ -77,6 +78,7 @@ Each lemmy server can set its own moderation policy; appointing site-wide admins
|
|||
- A post can consist of a title and any combination of self text, a URL, or nothing else.
|
||||
- Notifications, on comment replies and when you're tagged.
|
||||
- Notifications can be sent via email.
|
||||
- Private messaging support.
|
||||
- i18n / internationalization support.
|
||||
- RSS / Atom feeds for `All`, `Subscribed`, `Inbox`, `User`, and `Community`.
|
||||
- Cross-posting support.
|
||||
|
@ -90,6 +92,7 @@ Each lemmy server can set its own moderation policy; appointing site-wide admins
|
|||
- Can transfer site and communities to others.
|
||||
- Can fully erase your data, replacing all posts and comments.
|
||||
- NSFW post / community support.
|
||||
- OEmbed support via Iframely.
|
||||
- High performance.
|
||||
- Server is written in rust.
|
||||
- Front end is `~80kB` gzipped.
|
||||
|
@ -100,6 +103,7 @@ Each lemmy server can set its own moderation policy; appointing site-wide admins
|
|||
- [Docker](https://dev.lemmy.ml/docs/administration_install_docker.html)
|
||||
- [Ansible](https://dev.lemmy.ml/docs/administration_install_ansible.html)
|
||||
- [Kubernetes](https://dev.lemmy.ml/docs/administration_install_kubernetes.html)
|
||||
|
||||
## Support / Donate
|
||||
|
||||
Lemmy is free, open-source software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project.
|
||||
|
@ -122,35 +126,7 @@ Lemmy is free, open-source software, meaning no advertising, monetizing, or vent
|
|||
|
||||
### Translations
|
||||
|
||||
If you'd like to add translations, take a look at the [English translation file](ui/src/translations/en.ts).
|
||||
|
||||
- Languages supported: Brazilian Portuguese (`pt-br`), Catalan, (`ca`), Farsi (`fa`), English (`en`), Chinese (`zh`), Dutch (`nl`), Esperanto (`eo`), Finnish (`fi`), French (`fr`), Spanish (`es`), Swedish (`sv`), German (`de`), Russian (`ru`), Italian (`it`).
|
||||
|
||||
<!-- translations -->
|
||||
|
||||
lang | done | missing
|
||||
---- | ---- | -------
|
||||
ca | 97% | cross_posted_to,old,support_on_liberapay,couldnt_get_comments,post_title_too_long,time,action
|
||||
de | 86% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,old,docs,message_sent,messages,old_password,matrix_user_id,private_message_disclaimer,send_notifications_to_email,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,couldnt_get_comments,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
|
||||
fa | 71% | cross_post,cross_posted_to,subscribed_to_communities,trending_communities,create_private_message,send_secure_message,send_message,message,mod,mods,moderates,remove_as_mod,appoint_as_mod,modlog,stickied,ban,ban_from_site,unban,unban_from_site,banned,number_of_subscribers,subscribers,both,saved,unsubscribe,subscribe,subscribed,old,api,docs,inbox,inbox_for,message_sent,notifications_error,messages,no_email_setup,matrix_user_id,private_message_disclaimer,url,body,copy_suggested_title,community,expand_here,subscribe_to_communities,theme,sponsor_message,support_on_liberapay,general_sponsors,joined,by,to,from,landing_0,logged_in,couldnt_get_comments,community_moderator_already_exists,community_follower_already_exists,community_user_already_banned,post_title_too_long,no_slurs,admin_already_created,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
|
||||
eo | 73% | cross_posted_to,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme,support_on_liberapay,donate_to_lemmy,donate,from,are_you_sure,yes,no,logged_in,couldnt_get_comments,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
|
||||
es | 99% | cross_posted_to,couldnt_get_comments,post_title_too_long
|
||||
fi | 97% | cross_posted_to,old,support_on_liberapay,couldnt_get_comments,post_title_too_long,time,action
|
||||
fr | 81% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,couldnt_get_comments,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
|
||||
it | 82% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,old,docs,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,couldnt_get_comments,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
|
||||
nl | 98% | cross_posted_to,couldnt_get_comments,post_title_too_long,time,action
|
||||
pt-br | 99% | couldnt_get_comments,post_title_too_long
|
||||
ru | 70% | cross_posts,cross_post,cross_posted_to,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,theme,support_on_liberapay,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,couldnt_get_comments,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
|
||||
sv | 81% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,support_on_liberapay,donate_to_lemmy,donate,from,logged_in,couldnt_get_comments,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
|
||||
zh | 69% | cross_posts,cross_post,cross_posted_to,users,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,nsfw,show_nsfw,theme,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,couldnt_get_comments,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
|
||||
<!-- translationsstop -->
|
||||
|
||||
If you'd like to update this report, run:
|
||||
|
||||
```bash
|
||||
cd ui
|
||||
ts-node translation_report.ts
|
||||
```
|
||||
If you want to help with translating, take a look at [Weblate](https://weblate.yerbamate.dev/projects/lemmy/).
|
||||
|
||||
## Contact
|
||||
|
||||
|
|
2
ansible/VERSION
vendored
2
ansible/VERSION
vendored
|
@ -1 +1 @@
|
|||
v0.6.17
|
||||
v0.6.39
|
||||
|
|
3
ansible/lemmy.yml
vendored
3
ansible/lemmy.yml
vendored
|
@ -35,6 +35,7 @@
|
|||
with_items:
|
||||
- { src: 'templates/docker-compose.yml', dest: '/lemmy/docker-compose.yml', mode: '0600' }
|
||||
- { src: 'templates/nginx.conf', dest: '/etc/nginx/sites-enabled/lemmy.conf', mode: '0644' }
|
||||
- { src: '../docker/iframely.config.local.js', dest: '/lemmy/iframely.config.local.js', mode: '0600' }
|
||||
|
||||
- name: add config file (only during initial setup)
|
||||
template: src='templates/config.hjson' dest='/lemmy/lemmy.hjson' mode='0600' force='no' owner='1000' group='1000'
|
||||
|
@ -63,4 +64,4 @@
|
|||
special_time=daily
|
||||
name=certbot-renew-lemmy
|
||||
user=root
|
||||
job="certbot certonly --nginx -d '{{ domain }}' --deploy-hook 'docker-compose -f /peertube/docker-compose.yml exec nginx nginx -s reload'"
|
||||
job="certbot certonly --nginx -d '{{ domain }}' --deploy-hook 'nginx -s reload'"
|
||||
|
|
3
ansible/lemmy_dev.yml
vendored
3
ansible/lemmy_dev.yml
vendored
|
@ -37,6 +37,7 @@
|
|||
with_items:
|
||||
- { src: 'templates/docker-compose.yml', dest: '/lemmy/docker-compose.yml', mode: '0600' }
|
||||
- { src: 'templates/nginx.conf', dest: '/etc/nginx/sites-enabled/lemmy.conf', mode: '0644' }
|
||||
- { src: '../docker/iframely.config.local.js', dest: '/lemmy/iframely.config.local.js', mode: '0600' }
|
||||
|
||||
- name: add config file (only during initial setup)
|
||||
template: src='templates/config.hjson' dest='/lemmy/lemmy.hjson' mode='0600' force='no' owner='1000' group='1000'
|
||||
|
@ -97,4 +98,4 @@
|
|||
special_time=daily
|
||||
name=certbot-renew-lemmy
|
||||
user=root
|
||||
job="certbot certonly --nginx -d '{{ domain }}' --deploy-hook 'docker-compose -f /peertube/docker-compose.yml exec nginx nginx -s reload'"
|
||||
job="certbot certonly --nginx -d '{{ domain }}' --deploy-hook 'nginx -s reload'"
|
||||
|
|
2
ansible/templates/config.hjson
vendored
2
ansible/templates/config.hjson
vendored
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
database: {
|
||||
password: "{{ postgres_password }}"
|
||||
host: "lemmy_db"
|
||||
host: "postgres"
|
||||
}
|
||||
hostname: "{{ domain }}"
|
||||
jwt_secret: "{{ jwt_password }}"
|
||||
|
|
26
ansible/templates/docker-compose.yml
vendored
26
ansible/templates/docker-compose.yml
vendored
|
@ -6,28 +6,39 @@ services:
|
|||
ports:
|
||||
- "127.0.0.1:8536:8536"
|
||||
restart: always
|
||||
environment:
|
||||
- RUST_LOG=debug
|
||||
volumes:
|
||||
- ./lemmy.hjson:/config/config.hjson:ro
|
||||
depends_on:
|
||||
- lemmy_db
|
||||
- lemmy_pictshare
|
||||
- postgres
|
||||
- pictshare
|
||||
- iframely
|
||||
|
||||
lemmy_db:
|
||||
postgres:
|
||||
image: postgres:12-alpine
|
||||
environment:
|
||||
- POSTGRES_USER=lemmy
|
||||
- POSTGRES_PASSWORD={{ postgres_password }}
|
||||
- POSTGRES_DB=lemmy
|
||||
volumes:
|
||||
- lemmy_db:/var/lib/postgresql/data
|
||||
- ./volumes/postgres:/var/lib/postgresql/data
|
||||
restart: always
|
||||
|
||||
lemmy_pictshare:
|
||||
pictshare:
|
||||
image: shtripok/pictshare:latest
|
||||
ports:
|
||||
- "127.0.0.1:8537:80"
|
||||
volumes:
|
||||
- lemmy_pictshare:/usr/share/nginx/html/data
|
||||
- ./volumes/pictshare:/usr/share/nginx/html/data
|
||||
restart: always
|
||||
|
||||
iframely:
|
||||
image: dogbin/iframely:latest
|
||||
ports:
|
||||
- "127.0.0.1:8061:80"
|
||||
volumes:
|
||||
- ./iframely.config.local.js:/iframely/config.local.js:ro
|
||||
restart: always
|
||||
|
||||
postfix:
|
||||
|
@ -35,6 +46,3 @@ services:
|
|||
environment:
|
||||
- POSTFIX_myhostname={{ domain }}
|
||||
restart: "always"
|
||||
volumes:
|
||||
lemmy_db:
|
||||
lemmy_pictshare:
|
||||
|
|
7
ansible/templates/nginx.conf
vendored
7
ansible/templates/nginx.conf
vendored
|
@ -80,6 +80,13 @@ server {
|
|||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
}
|
||||
}
|
||||
|
||||
location /iframely/ {
|
||||
proxy_pass http://0.0.0.0:8061/;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
|
||||
# Anonymize IP addresses
|
||||
|
|
17
docker/dev/deploy.sh
vendored
17
docker/dev/deploy.sh
vendored
|
@ -1,24 +1,26 @@
|
|||
#!/bin/sh
|
||||
git checkout master
|
||||
|
||||
# Import translations
|
||||
git fetch weblate
|
||||
git merge weblate/master
|
||||
|
||||
# Creating the new tag
|
||||
new_tag="$1"
|
||||
git tag $new_tag
|
||||
|
||||
third_semver=$(echo $new_tag | cut -d "." -f 3)
|
||||
|
||||
# Setting the version on the front end
|
||||
cd ../../
|
||||
echo "export const version: string = '$(git describe --tags)';" > "ui/src/version.ts"
|
||||
echo "export const version: string = '$new_tag';" > "ui/src/version.ts"
|
||||
git add "ui/src/version.ts"
|
||||
# Setting the version on the backend
|
||||
echo "pub const VERSION: &str = \"$(git describe --tags)\";" > "server/src/version.rs"
|
||||
echo "pub const VERSION: &str = \"$new_tag\";" > "server/src/version.rs"
|
||||
git add "server/src/version.rs"
|
||||
# Setting the version for Ansible
|
||||
git describe --tags > "ansible/VERSION"
|
||||
echo $new_tag > "ansible/VERSION"
|
||||
git add "ansible/VERSION"
|
||||
|
||||
cd docker/dev
|
||||
cd docker/dev || exit
|
||||
|
||||
# Changing the docker-compose prod
|
||||
sed -i "s/dessalines\/lemmy:.*/dessalines\/lemmy:$new_tag/" ../prod/docker-compose.yml
|
||||
|
@ -28,6 +30,7 @@ git add ../../ansible/templates/docker-compose.yml
|
|||
|
||||
# The commit
|
||||
git commit -m"Version $new_tag"
|
||||
git tag $new_tag
|
||||
|
||||
# Rebuilding docker
|
||||
docker-compose build
|
||||
|
@ -69,5 +72,5 @@ git push origin $new_tag
|
|||
git push
|
||||
|
||||
# Pushing to any ansible deploys
|
||||
cd ../../ansible
|
||||
cd ../../ansible || exit
|
||||
ansible-playbook lemmy.yml --become
|
||||
|
|
27
docker/dev/docker-compose.yml
vendored
27
docker/dev/docker-compose.yml
vendored
|
@ -1,15 +1,16 @@
|
|||
version: '3.3'
|
||||
|
||||
services:
|
||||
lemmy_db:
|
||||
postgres:
|
||||
image: postgres:12-alpine
|
||||
environment:
|
||||
- POSTGRES_USER=lemmy
|
||||
- POSTGRES_PASSWORD=password
|
||||
- POSTGRES_DB=lemmy
|
||||
volumes:
|
||||
- lemmy_db:/var/lib/postgresql/data
|
||||
- ./volumes/postgres:/var/lib/postgresql/data
|
||||
restart: always
|
||||
|
||||
lemmy:
|
||||
build:
|
||||
context: ../../
|
||||
|
@ -17,17 +18,27 @@ services:
|
|||
ports:
|
||||
- "127.0.0.1:8536:8536"
|
||||
restart: always
|
||||
environment:
|
||||
- RUST_LOG=debug
|
||||
volumes:
|
||||
- ../lemmy.hjson:/config/config.hjson:ro
|
||||
depends_on:
|
||||
- lemmy_db
|
||||
lemmy_pictshare:
|
||||
- postgres
|
||||
- pictshare
|
||||
- iframely
|
||||
|
||||
pictshare:
|
||||
image: shtripok/pictshare:latest
|
||||
ports:
|
||||
- "127.0.0.1:8537:80"
|
||||
volumes:
|
||||
- lemmy_pictshare:/usr/share/nginx/html/data
|
||||
- ./volumes/pictshare:/usr/share/nginx/html/data
|
||||
restart: always
|
||||
|
||||
iframely:
|
||||
image: dogbin/iframely:latest
|
||||
ports:
|
||||
- "127.0.0.1:8061:80"
|
||||
volumes:
|
||||
- ../iframely.config.local.js:/iframely/config.local.js:ro
|
||||
restart: always
|
||||
volumes:
|
||||
lemmy_db:
|
||||
lemmy_pictshare:
|
||||
|
|
11
docker/federation-test/Dockerfile
vendored
11
docker/federation-test/Dockerfile
vendored
|
@ -1,14 +1,15 @@
|
|||
FROM ekidd/rust-musl-builder:1.38.0-openssl11
|
||||
|
||||
USER root
|
||||
RUN mkdir /app/dist/documentation/ -p \
|
||||
&& addgroup --gid 1001 lemmy \
|
||||
&& adduser --disabled-password --shell /bin/sh -u 1001 --ingroup lemmy lemmy
|
||||
|
||||
# Copy resources
|
||||
COPY server/config/defaults.hjson /app/config/defaults.hjson
|
||||
COPY server/target/debug/lemmy_server /app/lemmy
|
||||
COPY ui/dist /app/dist
|
||||
COPY server/target/debug/lemmy_server /app/lemmy
|
||||
|
||||
USER root
|
||||
RUN mkdir /app/dist/documentation/
|
||||
RUN addgroup --gid 1001 lemmy
|
||||
RUN adduser --disabled-password --shell /bin/sh -u 1001 --ingroup lemmy lemmy
|
||||
RUN chown lemmy:lemmy /app/ -R
|
||||
USER lemmy
|
||||
EXPOSE 8536
|
||||
|
|
60
docker/federation-test/docker-compose.yml
vendored
60
docker/federation-test/docker-compose.yml
vendored
|
@ -6,69 +6,59 @@ services:
|
|||
ports:
|
||||
- "127.0.0.1:8540:8540"
|
||||
environment:
|
||||
- LEMMY_HOSTNAME=localhost:8540
|
||||
- LEMMY_DATABASE_URL=postgres://lemmy:password@lemmy_db_alpha:5432/lemmy
|
||||
- LEMMY_HOSTNAME=lemmy_alpha:8540
|
||||
- LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_alpha:5432/lemmy
|
||||
- LEMMY_JWT_SECRET=changeme
|
||||
- LEMMY_FRONT_END_DIR=/app/dist
|
||||
- LEMMY_FEDERATION_ENABLED=true
|
||||
- LEMMY_FEDERATED_INSTANCE=lemmy_beta:8541
|
||||
- LEMMY_FEDERATION__ENABLED=true
|
||||
- LEMMY_FEDERATION__FOLLOWED_INSTANCES=lemmy_beta:8541
|
||||
- LEMMY_FEDERATION__TLS_ENABLED=false
|
||||
- LEMMY_PORT=8540
|
||||
- RUST_BACKTRACE=1
|
||||
restart: always
|
||||
depends_on:
|
||||
- lemmy_db_alpha
|
||||
lemmy_db_alpha:
|
||||
- postgres_alpha
|
||||
postgres_alpha:
|
||||
image: postgres:12-alpine
|
||||
environment:
|
||||
- POSTGRES_USER=lemmy
|
||||
- POSTGRES_PASSWORD=${LEMMY_DATABASE_PASSWORD}
|
||||
- POSTGRES_PASSWORD=password
|
||||
- POSTGRES_DB=lemmy
|
||||
volumes:
|
||||
- lemmy_db_alpha:/var/lib/postgresql/data
|
||||
- ./volumes/postgres_alpha:/var/lib/postgresql/data
|
||||
restart: always
|
||||
# lemmy_pictshare_alpha:
|
||||
# image: shtripok/pictshare:latest
|
||||
# ports:
|
||||
# - "127.0.0.1:8550:80"
|
||||
# volumes:
|
||||
# - lemmy_pictshare_alpha:/usr/share/nginx/html/data
|
||||
# restart: always
|
||||
|
||||
lemmy_beta:
|
||||
image: lemmy-federation-test:latest
|
||||
ports:
|
||||
- "127.0.0.1:8541:8541"
|
||||
environment:
|
||||
- LEMMY_HOSTNAME=localhost:8541
|
||||
- LEMMY_DATABASE_URL=postgres://lemmy:password@lemmy_db_beta:5432/lemmy
|
||||
- LEMMY_HOSTNAME=lemmy_beta:8541
|
||||
- LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_beta:5432/lemmy
|
||||
- LEMMY_JWT_SECRET=changeme
|
||||
- LEMMY_FRONT_END_DIR=/app/dist
|
||||
- LEMMY_FEDERATION_ENABLED=true
|
||||
- LEMMY_FEDERATED_INSTANCE=lemmy_alpha:8540
|
||||
- LEMMY_FEDERATION__ENABLED=true
|
||||
- LEMMY_FEDERATION__FOLLOWED_INSTANCES=lemmy_alpha:8540
|
||||
- LEMMY_FEDERATION__TLS_ENABLED=false
|
||||
- LEMMY_PORT=8541
|
||||
- RUST_BACKTRACE=1
|
||||
restart: always
|
||||
depends_on:
|
||||
- lemmy_db_beta
|
||||
lemmy_db_beta:
|
||||
- postgres_beta
|
||||
postgres_beta:
|
||||
image: postgres:12-alpine
|
||||
environment:
|
||||
- POSTGRES_USER=lemmy
|
||||
- POSTGRES_PASSWORD=${LEMMY_DATABASE_PASSWORD}
|
||||
- POSTGRES_PASSWORD=password
|
||||
- POSTGRES_DB=lemmy
|
||||
volumes:
|
||||
- lemmy_db_beta:/var/lib/postgresql/data
|
||||
- ./volumes/postgres_beta:/var/lib/postgresql/data
|
||||
restart: always
|
||||
# lemmy_pictshare_beta:
|
||||
# image: shtripok/pictshare:latest
|
||||
# ports:
|
||||
# - "127.0.0.1:8551:80"
|
||||
# volumes:
|
||||
# - lemmy_pictshare_beta:/usr/share/nginx/html/data
|
||||
# restart: always
|
||||
|
||||
volumes:
|
||||
lemmy_db_alpha:
|
||||
# lemmy_pictshare_alpha:
|
||||
lemmy_db_beta:
|
||||
# lemmy_pictshare_beta:
|
||||
iframely:
|
||||
image: dogbin/iframely:latest
|
||||
ports:
|
||||
- "127.0.0.1:8061:80"
|
||||
volumes:
|
||||
- ../iframely.config.local.js:/iframely/config.local.js:ro
|
||||
restart: always
|
||||
|
|
283
docker/iframely.config.local.js
vendored
Normal file
283
docker/iframely.config.local.js
vendored
Normal file
|
@ -0,0 +1,283 @@
|
|||
(function() {
|
||||
var config = {
|
||||
|
||||
// Specify a path for custom plugins. Custom plugins will override core plugins.
|
||||
// CUSTOM_PLUGINS_PATH: __dirname + '/yourcustom-plugin-folder',
|
||||
|
||||
DEBUG: false,
|
||||
RICH_LOG_ENABLED: false,
|
||||
|
||||
// For embeds that require render, baseAppUrl will be used as the host.
|
||||
baseAppUrl: "http://yourdomain.com",
|
||||
relativeStaticUrl: "/r",
|
||||
|
||||
// Or just skip built-in renders altogether
|
||||
SKIP_IFRAMELY_RENDERS: true,
|
||||
|
||||
// For legacy reasons the response format of Iframely open-source is
|
||||
// different by default as it does not group the links array by rel.
|
||||
// In order to get the same grouped response as in Cloud API,
|
||||
// add `&group=true` to your request to change response per request
|
||||
// or set `GROUP_LINKS` in your config to `true` for a global change.
|
||||
GROUP_LINKS: true,
|
||||
|
||||
// Number of maximum redirects to follow before aborting the page
|
||||
// request with `redirect loop` error.
|
||||
MAX_REDIRECTS: 4,
|
||||
|
||||
SKIP_OEMBED_RE_LIST: [
|
||||
// /^https?:\/\/yourdomain\.com\//,
|
||||
],
|
||||
|
||||
/*
|
||||
// Used to pass parameters to the generate functions when creating HTML elements
|
||||
// disableSizeWrapper: Don't wrap element (iframe, video, etc) in a positioned div
|
||||
GENERATE_LINK_PARAMS: {
|
||||
disableSizeWrapper: true
|
||||
},
|
||||
*/
|
||||
|
||||
port: 80, //can be overridden by PORT env var
|
||||
host: '0.0.0.0', // Dockers beware. See https://github.com/itteco/iframely/issues/132#issuecomment-242991246
|
||||
//can be overridden by HOST env var
|
||||
|
||||
// Optional SSL cert, if you serve under HTTPS.
|
||||
/*
|
||||
ssl: {
|
||||
key: require('fs').readFileSync(__dirname + '/key.pem'),
|
||||
cert: require('fs').readFileSync(__dirname + '/cert.pem'),
|
||||
port: 443
|
||||
},
|
||||
*/
|
||||
|
||||
/*
|
||||
Supported cache engines:
|
||||
- no-cache - no caching will be used.
|
||||
- node-cache - good for debug, node memory will be used (https://github.com/tcs-de/nodecache).
|
||||
- redis - https://github.com/mranney/node_redis.
|
||||
- memcached - https://github.com/3rd-Eden/node-memcached
|
||||
*/
|
||||
CACHE_ENGINE: 'node-cache',
|
||||
CACHE_TTL: 0, // In seconds.
|
||||
// 0 = 'never expire' for memcached & node-cache to let cache engine decide itself when to evict the record
|
||||
// 0 = 'no cache' for redis. Use high enough (e.g. 365*24*60*60*1000) ttl for similar 'never expire' approach instead
|
||||
|
||||
/*
|
||||
// Redis cache options.
|
||||
REDIS_OPTIONS: {
|
||||
host: '127.0.0.1',
|
||||
port: 6379
|
||||
},
|
||||
*/
|
||||
|
||||
/*
|
||||
// Memcached options. See https://github.com/3rd-Eden/node-memcached#server-locations
|
||||
MEMCACHED_OPTIONS: {
|
||||
locations: "127.0.0.1:11211"
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
// Access-Control-Allow-Origin list.
|
||||
allowedOrigins: [
|
||||
"*",
|
||||
"http://another_domain.com"
|
||||
],
|
||||
*/
|
||||
|
||||
/*
|
||||
// Uncomment to enable plugin testing framework.
|
||||
tests: {
|
||||
mongodb: 'mongodb://localhost:27017/iframely-tests',
|
||||
single_test_timeout: 10 * 1000,
|
||||
plugin_test_period: 2 * 60 * 60 * 1000,
|
||||
relaunch_script_period: 5 * 60 * 1000
|
||||
},
|
||||
*/
|
||||
|
||||
// If there's no response from remote server, the timeout will occur after
|
||||
RESPONSE_TIMEOUT: 5 * 1000, //ms
|
||||
|
||||
/* From v1.4.0, Iframely supports HTTP/2 by default. Disable it, if you'd rather not.
|
||||
Alternatively, you can also disable per origin. See `proxy` option below.
|
||||
*/
|
||||
// DISABLE_HTTP2: true,
|
||||
|
||||
// Customize API calls to oembed endpoints.
|
||||
ADD_OEMBED_PARAMS: [{
|
||||
// Endpoint url regexp array.
|
||||
re: [/^http:\/\/api\.instagram\.com\/oembed/],
|
||||
// Custom get params object.
|
||||
params: {
|
||||
hidecaption: true
|
||||
}
|
||||
}, {
|
||||
re: [/^https:\/\/www\.facebook\.com\/plugins\/page\/oembed\.json/i],
|
||||
params: {
|
||||
show_posts: 0,
|
||||
show_facepile: 0,
|
||||
maxwidth: 600
|
||||
}
|
||||
}, {
|
||||
// match i=user or i=moment or i=timeline to configure these types invidually
|
||||
// see params spec at https://dev.twitter.com/web/embedded-timelines/oembed
|
||||
re: [/^https?:\/\/publish\.twitter\.com\/oembed\?i=user/i],
|
||||
params: {
|
||||
limit: 1,
|
||||
maxwidth: 600
|
||||
}
|
||||
/*
|
||||
}, {
|
||||
// Facebook https://developers.facebook.com/docs/plugins/oembed-endpoints
|
||||
re: [/^https:\/\/www\.facebook\.com\/plugins\/\w+\/oembed\.json/i],
|
||||
params: {
|
||||
// Skip script tag and fb-root div.
|
||||
omitscript: true
|
||||
}
|
||||
*/
|
||||
}],
|
||||
|
||||
/*
|
||||
// Configure use of HTTP proxies as needed.
|
||||
// You don't have to specify all options per regex - just what you need to override
|
||||
PROXY: [{
|
||||
re: [/^https?:\/\/www\.domain\.com/],
|
||||
proxy_server: 'http://1.2.3.4:8080',
|
||||
user_agent: 'CHANGE YOUR AGENT',
|
||||
headers: {
|
||||
// HTTP headers
|
||||
// Overrides previous params if overlapped.
|
||||
},
|
||||
request_options: {
|
||||
// Refer to: https://github.com/request/request
|
||||
// Overrides previous params if overlapped.
|
||||
},
|
||||
disable_http2: true
|
||||
}],
|
||||
*/
|
||||
|
||||
// Customize API calls to 3rd parties. At the very least - configure required keys.
|
||||
providerOptions: {
|
||||
locale: "en_US", // ISO 639-1 two-letter language code, e.g. en_CA or fr_CH.
|
||||
// Will be added as highest priotity in accept-language header with each request.
|
||||
// Plus is used in FB, YouTube and perhaps other plugins
|
||||
"twitter": {
|
||||
"max-width": 550,
|
||||
"min-width": 250,
|
||||
hide_media: false,
|
||||
hide_thread: false,
|
||||
omit_script: false,
|
||||
center: false,
|
||||
// dnt: true,
|
||||
cache_ttl: 100 * 365 * 24 * 3600 // 100 Years.
|
||||
},
|
||||
readability: {
|
||||
enabled: false
|
||||
// allowPTagDescription: true // to enable description fallback to first paragraph
|
||||
},
|
||||
images: {
|
||||
loadSize: false, // if true, will try an load first bytes of all images to get/confirm the sizes
|
||||
checkFavicon: false // if true, will verify all favicons
|
||||
},
|
||||
tumblr: {
|
||||
consumer_key: "INSERT YOUR VALUE"
|
||||
// media_only: true // disables status embeds for images and videos - will return plain media
|
||||
},
|
||||
google: {
|
||||
// https://developers.google.com/maps/documentation/embed/guide#api_key
|
||||
maps_key: "INSERT YOUR VALUE"
|
||||
},
|
||||
|
||||
/*
|
||||
// Optional Camo Proxy to wrap all images: https://github.com/atmos/camo
|
||||
camoProxy: {
|
||||
camo_proxy_key: "INSERT YOUR VALUE",
|
||||
camo_proxy_host: "INSERT YOUR VALUE"
|
||||
// ssl_only: true // will only proxy non-ssl images
|
||||
},
|
||||
*/
|
||||
|
||||
// List of query parameters to add to YouTube and Vimeo frames
|
||||
// Start it with leading "?". Or omit alltogether for default values
|
||||
// API key is optional, youtube will work without it too.
|
||||
// It is probably the same API key you use for Google Maps.
|
||||
youtube: {
|
||||
// api_key: "INSERT YOUR VALUE",
|
||||
get_params: "?rel=0&showinfo=1" // https://developers.google.com/youtube/player_parameters
|
||||
},
|
||||
vimeo: {
|
||||
get_params: "?byline=0&badge=0" // https://developer.vimeo.com/player/embedding
|
||||
},
|
||||
|
||||
/*
|
||||
soundcloud: {
|
||||
old_player: true // enables classic player
|
||||
},
|
||||
giphy: {
|
||||
media_only: true // disables branded player for gifs and returns just the image
|
||||
}
|
||||
*/
|
||||
/*
|
||||
bandcamp: {
|
||||
get_params: '/size=large/bgcol=333333/linkcol=ffffff/artwork=small/transparent=true/',
|
||||
media: {
|
||||
album: {
|
||||
height: 472,
|
||||
'max-width': 700
|
||||
},
|
||||
track: {
|
||||
height: 120,
|
||||
'max-width': 700
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
},
|
||||
|
||||
// WHITELIST_WILDCARD, if present, will be added to whitelist as record for top level domain: "*"
|
||||
// with it, you can define what parsers do when they run accross unknown publisher.
|
||||
// If absent or empty, all generic media parsers will be disabled except for known domains
|
||||
// More about format: https://iframely.com/docs/qa-format
|
||||
|
||||
/*
|
||||
WHITELIST_WILDCARD: {
|
||||
"twitter": {
|
||||
"player": "allow",
|
||||
"photo": "deny"
|
||||
},
|
||||
"oembed": {
|
||||
"video": "allow",
|
||||
"photo": "allow",
|
||||
"rich": "deny",
|
||||
"link": "deny"
|
||||
},
|
||||
"og": {
|
||||
"video": ["allow", "ssl", "responsive"]
|
||||
},
|
||||
"iframely": {
|
||||
"survey": "allow",
|
||||
"reader": "allow",
|
||||
"player": "allow",
|
||||
"image": "allow"
|
||||
},
|
||||
"html-meta": {
|
||||
"video": ["allow", "responsive"],
|
||||
"promo": "allow"
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// Black-list any of the inappropriate domains. Iframely will return 417
|
||||
// At minimum, keep your localhosts blacklisted to avoid SSRF
|
||||
BLACKLIST_DOMAINS_RE: [
|
||||
/^https?:\/\/127\.0\.0\.1/i,
|
||||
/^https?:\/\/localhost/i,
|
||||
|
||||
// And this is AWS metadata service
|
||||
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html
|
||||
/^https?:\/\/169\.254\.169\.254/
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
})();
|
2
docker/lemmy.hjson
vendored
2
docker/lemmy.hjson
vendored
|
@ -5,7 +5,7 @@
|
|||
# password to connect to postgres
|
||||
password: "password"
|
||||
# host where postgres is running
|
||||
host: "lemmy_db"
|
||||
host: "postgres"
|
||||
# port where postgres can be accessed
|
||||
port: 5432
|
||||
# name of the postgres database for lemmy
|
||||
|
|
29
docker/prod/docker-compose.yml
vendored
29
docker/prod/docker-compose.yml
vendored
|
@ -1,31 +1,42 @@
|
|||
version: '3.3'
|
||||
|
||||
services:
|
||||
lemmy_db:
|
||||
postgres:
|
||||
image: postgres:12-alpine
|
||||
environment:
|
||||
- POSTGRES_USER=lemmy
|
||||
- POSTGRES_PASSWORD=password
|
||||
- POSTGRES_DB=lemmy
|
||||
volumes:
|
||||
- lemmy_db:/var/lib/postgresql/data
|
||||
- ./volumes/postgres:/var/lib/postgresql/data
|
||||
restart: always
|
||||
|
||||
lemmy:
|
||||
image: dessalines/lemmy:v0.6.17
|
||||
image: dessalines/lemmy:v0.6.39
|
||||
ports:
|
||||
- "127.0.0.1:8536:8536"
|
||||
restart: always
|
||||
environment:
|
||||
- RUST_LOG=debug
|
||||
volumes:
|
||||
- ./lemmy.hjson:/config/config.hjson:ro
|
||||
depends_on:
|
||||
- lemmy_db
|
||||
lemmy_pictshare:
|
||||
- postgres
|
||||
- pictshare
|
||||
- iframely
|
||||
|
||||
pictshare:
|
||||
image: shtripok/pictshare:latest
|
||||
ports:
|
||||
- "127.0.0.1:8537:80"
|
||||
volumes:
|
||||
- lemmy_pictshare:/usr/share/nginx/html/data
|
||||
- ./volumes/pictshare:/usr/share/nginx/html/data
|
||||
restart: always
|
||||
|
||||
iframely:
|
||||
image: dogbin/iframely:latest
|
||||
ports:
|
||||
- "127.0.0.1:8061:80"
|
||||
volumes:
|
||||
- ./iframely.config.local.js:/iframely/config.local.js:ro
|
||||
restart: always
|
||||
volumes:
|
||||
lemmy_db:
|
||||
lemmy_pictshare:
|
||||
|
|
3
docs/src/SUMMARY.md
vendored
3
docs/src/SUMMARY.md
vendored
|
@ -4,6 +4,7 @@
|
|||
- [Features](about_features.md)
|
||||
- [Goals](about_goals.md)
|
||||
- [Post and Comment Ranking](about_ranking.md)
|
||||
- [Guide](about_guide.md)
|
||||
- [Administration](administration.md)
|
||||
- [Install with Docker](administration_install_docker.md)
|
||||
- [Install with Ansible](administration_install_ansible.md)
|
||||
|
@ -14,3 +15,5 @@
|
|||
- [Local Development](contributing_local_development.md)
|
||||
- [Websocket/HTTP API](contributing_websocket_http_api.md)
|
||||
- [ActivityPub API Outline](contributing_apub_api_outline.md)
|
||||
- [Theming Guide](contributing_theming.md)
|
||||
- [Lemmy Council](lemmy_council.md)
|
||||
|
|
23
docs/src/about_goals.md
vendored
23
docs/src/about_goals.md
vendored
|
@ -1,4 +1,5 @@
|
|||
# Goals
|
||||
|
||||
- Come up with a name / codename.
|
||||
- Must have communities.
|
||||
- Must have threaded comments.
|
||||
|
@ -7,6 +8,7 @@
|
|||
- Use websockets for post / gets to your own instance.
|
||||
|
||||
# Questions
|
||||
|
||||
- How does voting work? Should we go back to the old way of showing up and downvote counts? Or just a score?
|
||||
- Decide on tech to be used
|
||||
- Backend: Actix, Diesel.
|
||||
|
@ -17,10 +19,7 @@
|
|||
- On mobile, allow you to switch between them. Default?
|
||||
|
||||
# Resources / Potential Libraries
|
||||
- Use the [activitypub crate.](https://docs.rs/activitypub/0.1.4/activitypub/)
|
||||
- https://docs.rs/activitypub/0.1.4/activitypub/
|
||||
- [Activitypub vocab.](https://www.w3.org/TR/activitystreams-vocabulary/)
|
||||
- [Activitypub main](https://www.w3.org/TR/activitypub/)
|
||||
|
||||
- [Diesel to Postgres data types](https://kotiri.com/2018/01/31/postgresql-diesel-rust-types.html)
|
||||
- [helpful diesel examples](http://siciarz.net/24-days-rust-diesel/)
|
||||
- [Recursive query for adjacency list for nested comments](https://stackoverflow.com/questions/192220/what-is-the-most-efficient-elegant-way-to-parse-a-flat-table-into-a-tree/192462#192462)
|
||||
|
@ -36,9 +35,15 @@
|
|||
- [Temp Icon](https://www.flaticon.com/free-icon/mouse_194242)
|
||||
- [Rust docker build](https://shaneutt.com/blog/rust-fast-small-docker-image-builds/)
|
||||
- [Zurb mentions](https://github.com/zurb/tribute)
|
||||
- Activitypub guides
|
||||
- https://blog.joinmastodon.org/2018/06/how-to-implement-a-basic-activitypub-server/
|
||||
- https://raw.githubusercontent.com/w3c/activitypub/gh-pages/activitypub-tutorial.txt
|
||||
- https://github.com/tOkeshu/activitypub-example
|
||||
- https://blog.joinmastodon.org/2018/07/how-to-make-friends-and-verify-requests/
|
||||
- [TippyJS](https://github.com/atomiks/tippyjs)
|
||||
|
||||
## Activitypub guides
|
||||
|
||||
- https://blog.joinmastodon.org/2018/06/how-to-implement-a-basic-activitypub-server/
|
||||
- https://raw.githubusercontent.com/w3c/activitypub/gh-pages/activitypub-tutorial.txt
|
||||
- https://github.com/tOkeshu/activitypub-example
|
||||
- https://blog.joinmastodon.org/2018/07/how-to-make-friends-and-verify-requests/
|
||||
- Use the [activitypub crate.](https://docs.rs/activitypub/0.1.4/activitypub/)
|
||||
- https://docs.rs/activitypub/0.1.4/activitypub/
|
||||
- [Activitypub vocab.](https://www.w3.org/TR/activitystreams-vocabulary/)
|
||||
- [Activitypub main](https://www.w3.org/TR/activitypub/)
|
||||
|
|
40
docs/src/about_guide.md
vendored
Normal file
40
docs/src/about_guide.md
vendored
Normal file
|
@ -0,0 +1,40 @@
|
|||
# Lemmy Guide
|
||||
|
||||
Start typing...
|
||||
|
||||
- `@a_user_name` to get a list of usernames.
|
||||
- `#a_community` to get a list of communities.
|
||||
- `:emoji` to get a list of emojis.
|
||||
|
||||
## Sorting
|
||||
|
||||
*Applies to both posts and comments*
|
||||
|
||||
Type | Description
|
||||
--- | ---
|
||||
Hot | Shows *trending* posts, based on the score, and the most recent comment time.
|
||||
New | Newest posts.
|
||||
Top | Shows the highest scoring posts in the given time frame.
|
||||
|
||||
For more detail, check the [Post and Comment Ranking details](about_ranking.md).
|
||||
|
||||
## Markdown Guide
|
||||
|
||||
Type | Or | … to Get
|
||||
--- | --- | ---
|
||||
\*Italic\* | \_Italic\_ | _Italic_
|
||||
\*\*Bold\*\* | \_\_Bold\_\_ | **Bold**
|
||||
\# Heading 1 | Heading 1 <br> ========= | <h4>Heading 1</h4>
|
||||
\## Heading 2 | Heading 2 <br>--------- | <h5>Heading 2</h5>
|
||||
\[Link\](http://a.com) | \[Link\]\[1\]<br>⋮ <br>\[1\]: http://b.org | [Link](https://commonmark.org/)
|
||||
!\[Image\](http://url/a.png) | !\[Image\]\[1\]<br>⋮ <br>\[1\]: http://url/b.jpg | ![Markdown](https://commonmark.org/help/images/favicon.png)
|
||||
\> Blockquote | | <blockquote>Blockquote</blockquote>
|
||||
\* List <br>\* List <br>\* List | \- List <br>\- List <br>\- List <br> | * List <br>* List <br>* List <br>
|
||||
1\. One <br>2\. Two <br>3\. Three | 1) One<br>2) Two<br>3) Three | 1. One<br>2. Two<br>3. Three
|
||||
Horizontal Rule <br>\--- | Horizontal Rule<br>\*\*\* | Horizontal Rule <br><hr>
|
||||
\`Inline code\` with backticks | |`Inline code` with backticks
|
||||
\`\`\`<br>\# code block <br>print '3 backticks or'<br>print 'indent 4 spaces' <br>\`\`\` | ····\# code block<br>····print '3 backticks or'<br>····print 'indent 4 spaces' | \# code block <br>print '3 backticks or'<br>print 'indent 4 spaces'
|
||||
::: spoiler hidden or nsfw stuff<br>*a bunch of spoilers here*<br>::: | | <details><summary> hidden or nsfw stuff </summary><p><em>a bunch of spoilers here</em></p></details>
|
||||
|
||||
[CommonMark Tutorial](https://commonmark.org/help/tutorial/)
|
||||
|
2
docs/src/about_ranking.md
vendored
2
docs/src/about_ranking.md
vendored
|
@ -18,7 +18,7 @@ Score = Upvotes - Downvotes
|
|||
Time = time since submission (in hours)
|
||||
Gravity = Decay gravity, 1.8 is default
|
||||
```
|
||||
|
||||
- For posts, in order to bring up active posts, it uses the latest comment time (limited to a max creation age of a month ago)
|
||||
- Use Max(1, score) to make sure all comments are affected by time decay.
|
||||
- Add 3 to the score, so that everything that has less than 3 downvotes will seem new. Otherwise all new comments would stay at zero, near the bottom.
|
||||
- The sign and abs of the score are necessary for dealing with the log of negative scores.
|
||||
|
|
1
docs/src/administration_install_docker.md
vendored
1
docs/src/administration_install_docker.md
vendored
|
@ -7,6 +7,7 @@ mkdir lemmy/
|
|||
cd lemmy/
|
||||
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml
|
||||
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/lemmy.hjson
|
||||
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/iframely.config.local.js
|
||||
# Edit lemmy.hjson, and docker-compose.yml to do more configuration (like adding a custom password)
|
||||
docker-compose up -d
|
||||
```
|
||||
|
|
18
docs/src/contributing_theming.md
vendored
Normal file
18
docs/src/contributing_theming.md
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Theming Guide
|
||||
|
||||
Lemmy uses [Bootstrap v4](https://getbootstrap.com/), and very few custom css classes, so any bootstrap v4 compatible theme should work fine.
|
||||
|
||||
## Creating
|
||||
|
||||
- Use a tool like [bootstrap.build](https://bootstrap.build/) to create a bootstrap v4 theme. Export the `bootstrap.min.css` once you're done, and save the `_variables.scss` too.
|
||||
|
||||
## Testing
|
||||
|
||||
- To test out a theme, you can either use your browser's web tools, or a plugin like stylus to copy-paste a theme, when viewing Lemmy.
|
||||
|
||||
## Adding
|
||||
|
||||
1. Copy `{my-theme-name}.min.css` to `ui/assets/css/themes`. (You can also copy the `_variables.scss` here if you want).
|
||||
1. Go to `ui/src/utils.ts` and add `{my-theme-name}` to the themes list.
|
||||
1. Test locally
|
||||
1. Do a pull request with those changes.
|
53
docs/src/lemmy_council.md
vendored
Normal file
53
docs/src/lemmy_council.md
vendored
Normal file
|
@ -0,0 +1,53 @@
|
|||
# Lemmy Council
|
||||
|
||||
- A group of lemmy developers and users that use a well-defined democratic process to steer the project in a positive direction, keep it aligned to community goals, and resolve conflicts.
|
||||
|
||||
## Voting / Decision-Making
|
||||
|
||||
### Process
|
||||
- Anything is open for discussion
|
||||
- Voting done through matrix chat reacts (thumbs up/thumbs down)
|
||||
- Require a simple majority for votes. (Maybe 2/3rds for more debated decisions).
|
||||
- Once a decision is reached democratically, the dicision is binding and all group members have to follow it
|
||||
- All members of the Lemmy council have equal voting power.
|
||||
- Voting must stay open for at least 2 days.
|
||||
|
||||
### What gets voted on
|
||||
- Membership (joining, removing)
|
||||
- Coding direction
|
||||
- Priorities / Emphasis
|
||||
- Controversial features (For example, an unpopular feature should be removed)
|
||||
- Communication mediums
|
||||
- Conflict resolution
|
||||
- dev.lemmy.ml (domain and server)
|
||||
- lemmy.ml and subdomains (excluding communism.lemmy.ml)
|
||||
- git repo including mirrors (on github, gitea, etc)
|
||||
- Any official accounts of the Lemmy project, for example the Mastodon account or the Liberapay account
|
||||
- Changes to these rules
|
||||
|
||||
## Joining
|
||||
- We use the following process: anyone who is active around Lemmy can recommend any other active person to join the council. This has to be approved by a majority of the council.
|
||||
- Active users are defined as those who contribute to Lemmy in some way for at least an hour per week on average, doing things like reporting bugs, discussing rules and features, translating, promoting, developing, or doing other things that aim to improve Lemmy as a whole.
|
||||
-> people should have joined at least a month ago.
|
||||
- The member list is public.
|
||||
- Note: we would like to have a process where community members can elect candidates for the council, but this is not realistic because a single user could easily create multiple accounts and cheat the vote.
|
||||
- Limit growth to one new member per month at most.
|
||||
|
||||
## Removing members
|
||||
- Inactive members should be removed from the council after a few months of inactivity, and after receiving a notification about this.
|
||||
- Members that dont follow binding council decisions should be removed.
|
||||
- Any member can be removed in a vote.
|
||||
|
||||
## Goals
|
||||
- We encourage the membership of groups such as LGBT, religious or ethnic minorities, abuse victims, etc etc, and strive to create a safe space for them to express their opinions. We also support measures to increase participation by the previously mentioned groups.
|
||||
- The following are banned, and will always be harshly punished: fascism, abuse, racism, sexism, etc etc,
|
||||
|
||||
## Communication
|
||||
- A private Matrix chat for all council members.
|
||||
- (Once private communities are done) A private community on dev.lemmy.ml for issues.
|
||||
|
||||
## Member List / Contact Info
|
||||
General Contact [@LemmyDev Mastodon](https://mastodon.social/@LemmyDev)
|
||||
|
||||
- Dessalines [Matrix](https://matrix.to/#/@happydooby:matrix.org)
|
||||
- Nutomic [Matrix](https://matrix.to/#/@nutomic:matrix.org), [Mastodon](https://radical.town/@felix)
|
2
install.sh
vendored
2
install.sh
vendored
|
@ -37,7 +37,7 @@ yarn build
|
|||
|
||||
# Build and run the backend
|
||||
cd ../server
|
||||
cargo run
|
||||
RUST_LOG=debug cargo run
|
||||
|
||||
# For live coding, where both the front and back end, automagically reload on any save, do:
|
||||
# cd ui && yarn start
|
||||
|
|
1779
server/Cargo.lock
generated
vendored
1779
server/Cargo.lock
generated
vendored
File diff suppressed because it is too large
Load diff
9
server/Cargo.toml
vendored
9
server/Cargo.toml
vendored
|
@ -5,11 +5,11 @@ authors = ["Dessalines <happydooby@gmail.com>"]
|
|||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
diesel = { version = "1.4.2", features = ["postgres","chrono", "r2d2"] }
|
||||
diesel = { version = "1.4.2", features = ["postgres","chrono", "r2d2", "64-column-tables"] }
|
||||
diesel_migrations = "1.4.0"
|
||||
dotenv = "0.15.0"
|
||||
bcrypt = "0.6.1"
|
||||
activitypub = "0.2.0"
|
||||
activitystreams = "0.5.0-alpha.10"
|
||||
chrono = { version = "0.4.7", features = ["serde"] }
|
||||
failure = "0.1.5"
|
||||
serde_json = { version = "1.0.45", features = ["preserve_order"]}
|
||||
|
@ -19,6 +19,7 @@ actix-web = "2.0.0"
|
|||
actix-files = "0.2.1"
|
||||
actix-web-actors = "2.0.0"
|
||||
actix-rt = "1.0.0"
|
||||
log = "0.4.0"
|
||||
env_logger = "0.7.1"
|
||||
rand = "0.7.3"
|
||||
strum = "0.17.1"
|
||||
|
@ -33,4 +34,6 @@ rss = "1.9.0"
|
|||
htmlescape = "0.3.1"
|
||||
config = "0.10.1"
|
||||
hjson = "0.8.2"
|
||||
reqwest = "0.9.24"
|
||||
url = "2.1.1"
|
||||
percent-encoding = "2.1.0"
|
||||
chttp = "0.5.5"
|
||||
|
|
14
server/config/defaults.hjson
vendored
14
server/config/defaults.hjson
vendored
|
@ -24,11 +24,6 @@
|
|||
jwt_secret: "changeme"
|
||||
# The dir for the front end
|
||||
front_end_dir: "../ui/dist"
|
||||
# whether to enable activitypub federation. this feature is in alpha, do not enable in production.
|
||||
federation_enabled: false
|
||||
// another instance to federate with. this should be a list, but it seems like lists cant be set from environment
|
||||
// https://github.com/mehcode/config-rs/issues/117
|
||||
federated_instance: null
|
||||
# rate limits for various user actions, by user ip
|
||||
rate_limit: {
|
||||
# maximum number of messages created in interval
|
||||
|
@ -44,6 +39,15 @@
|
|||
# interval length for registration limit
|
||||
register_per_second: 3600
|
||||
}
|
||||
# settings related to activitypub federation
|
||||
federation: {
|
||||
# whether to enable activitypub federation. this feature is in alpha, do not enable in production.
|
||||
enabled: false
|
||||
# comma seperated list of instances to follow
|
||||
followed_instances: ""
|
||||
# whether tls is required for activitypub. only disable this for debugging, never for producion.
|
||||
tls_enabled: true
|
||||
}
|
||||
# # email sending configuration
|
||||
# email: {
|
||||
# # hostname of the smtp server
|
||||
|
|
0
server/db-init.sh
vendored
Normal file → Executable file
0
server/db-init.sh
vendored
Normal file → Executable file
112
server/migrations/2020-03-06-202329_add_post_iframely_data/down.sql
vendored
Normal file
112
server/migrations/2020-03-06-202329_add_post_iframely_data/down.sql
vendored
Normal file
|
@ -0,0 +1,112 @@
|
|||
-- Adds a newest_activity_time for the post_views, in order to sort by newest comment
|
||||
drop view post_view;
|
||||
drop view post_mview;
|
||||
drop materialized view post_aggregates_mview;
|
||||
drop view post_aggregates_view;
|
||||
|
||||
-- Drop the columns
|
||||
alter table post drop column embed_title;
|
||||
alter table post drop column embed_description;
|
||||
alter table post drop column embed_html;
|
||||
alter table post drop column thumbnail_url;
|
||||
|
||||
-- regen post view
|
||||
create view post_aggregates_view as
|
||||
select
|
||||
p.*,
|
||||
(select u.banned from user_ u where p.creator_id = u.id) as banned,
|
||||
(select cb.id::bool from community_user_ban cb where p.creator_id = cb.user_id and p.community_id = cb.community_id) as banned_from_community,
|
||||
(select name from user_ where p.creator_id = user_.id) as creator_name,
|
||||
(select avatar from user_ where p.creator_id = user_.id) as creator_avatar,
|
||||
(select name from community where p.community_id = community.id) as community_name,
|
||||
(select removed from community c where p.community_id = c.id) as community_removed,
|
||||
(select deleted from community c where p.community_id = c.id) as community_deleted,
|
||||
(select nsfw from community c where p.community_id = c.id) as community_nsfw,
|
||||
(select count(*) from comment where comment.post_id = p.id) as number_of_comments,
|
||||
coalesce(sum(pl.score), 0) as score,
|
||||
count (case when pl.score = 1 then 1 else null end) as upvotes,
|
||||
count (case when pl.score = -1 then 1 else null end) as downvotes,
|
||||
hot_rank(coalesce(sum(pl.score) , 0),
|
||||
(
|
||||
case when (p.published < ('now'::timestamp - '1 month'::interval)) then p.published -- Prevents necro-bumps
|
||||
else greatest(c.recent_comment_time, p.published)
|
||||
end
|
||||
)
|
||||
) as hot_rank,
|
||||
(
|
||||
case when (p.published < ('now'::timestamp - '1 month'::interval)) then p.published -- Prevents necro-bumps
|
||||
else greatest(c.recent_comment_time, p.published)
|
||||
end
|
||||
) as newest_activity_time
|
||||
from post p
|
||||
left join post_like pl on p.id = pl.post_id
|
||||
left join (
|
||||
select post_id,
|
||||
max(published) as recent_comment_time
|
||||
from comment
|
||||
group by 1
|
||||
) c on p.id = c.post_id
|
||||
group by p.id, c.recent_comment_time;
|
||||
|
||||
create materialized view post_aggregates_mview as select * from post_aggregates_view;
|
||||
|
||||
create unique index idx_post_aggregates_mview_id on post_aggregates_mview (id);
|
||||
|
||||
create view post_view as
|
||||
with all_post as (
|
||||
select
|
||||
pa.*
|
||||
from post_aggregates_view pa
|
||||
)
|
||||
select
|
||||
ap.*,
|
||||
u.id as user_id,
|
||||
coalesce(pl.score, 0) as my_vote,
|
||||
(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
|
||||
(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
|
||||
(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
|
||||
from user_ u
|
||||
cross join all_post ap
|
||||
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
|
||||
|
||||
union all
|
||||
|
||||
select
|
||||
ap.*,
|
||||
null as user_id,
|
||||
null as my_vote,
|
||||
null as subscribed,
|
||||
null as read,
|
||||
null as saved
|
||||
from all_post ap
|
||||
;
|
||||
|
||||
create view post_mview as
|
||||
with all_post as (
|
||||
select
|
||||
pa.*
|
||||
from post_aggregates_mview pa
|
||||
)
|
||||
select
|
||||
ap.*,
|
||||
u.id as user_id,
|
||||
coalesce(pl.score, 0) as my_vote,
|
||||
(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
|
||||
(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
|
||||
(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
|
||||
from user_ u
|
||||
cross join all_post ap
|
||||
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
|
||||
|
||||
union all
|
||||
|
||||
select
|
||||
ap.*,
|
||||
null as user_id,
|
||||
null as my_vote,
|
||||
null as subscribed,
|
||||
null as read,
|
||||
null as saved
|
||||
from all_post ap
|
||||
;
|
||||
|
115
server/migrations/2020-03-06-202329_add_post_iframely_data/up.sql
vendored
Normal file
115
server/migrations/2020-03-06-202329_add_post_iframely_data/up.sql
vendored
Normal file
|
@ -0,0 +1,115 @@
|
|||
-- Add the columns
|
||||
alter table post add column embed_title text;
|
||||
alter table post add column embed_description text;
|
||||
alter table post add column embed_html text;
|
||||
alter table post add column thumbnail_url text;
|
||||
|
||||
-- Regenerate the views
|
||||
|
||||
-- Adds a newest_activity_time for the post_views, in order to sort by newest comment
|
||||
drop view post_view;
|
||||
drop view post_mview;
|
||||
drop materialized view post_aggregates_mview;
|
||||
drop view post_aggregates_view;
|
||||
|
||||
-- regen post view
|
||||
create view post_aggregates_view as
|
||||
select
|
||||
p.*,
|
||||
(select u.banned from user_ u where p.creator_id = u.id) as banned,
|
||||
(select cb.id::bool from community_user_ban cb where p.creator_id = cb.user_id and p.community_id = cb.community_id) as banned_from_community,
|
||||
(select name from user_ where p.creator_id = user_.id) as creator_name,
|
||||
(select avatar from user_ where p.creator_id = user_.id) as creator_avatar,
|
||||
(select name from community where p.community_id = community.id) as community_name,
|
||||
(select removed from community c where p.community_id = c.id) as community_removed,
|
||||
(select deleted from community c where p.community_id = c.id) as community_deleted,
|
||||
(select nsfw from community c where p.community_id = c.id) as community_nsfw,
|
||||
(select count(*) from comment where comment.post_id = p.id) as number_of_comments,
|
||||
coalesce(sum(pl.score), 0) as score,
|
||||
count (case when pl.score = 1 then 1 else null end) as upvotes,
|
||||
count (case when pl.score = -1 then 1 else null end) as downvotes,
|
||||
hot_rank(coalesce(sum(pl.score) , 0),
|
||||
(
|
||||
case when (p.published < ('now'::timestamp - '1 month'::interval)) then p.published -- Prevents necro-bumps
|
||||
else greatest(c.recent_comment_time, p.published)
|
||||
end
|
||||
)
|
||||
) as hot_rank,
|
||||
(
|
||||
case when (p.published < ('now'::timestamp - '1 month'::interval)) then p.published -- Prevents necro-bumps
|
||||
else greatest(c.recent_comment_time, p.published)
|
||||
end
|
||||
) as newest_activity_time
|
||||
from post p
|
||||
left join post_like pl on p.id = pl.post_id
|
||||
left join (
|
||||
select post_id,
|
||||
max(published) as recent_comment_time
|
||||
from comment
|
||||
group by 1
|
||||
) c on p.id = c.post_id
|
||||
group by p.id, c.recent_comment_time;
|
||||
|
||||
create materialized view post_aggregates_mview as select * from post_aggregates_view;
|
||||
|
||||
create unique index idx_post_aggregates_mview_id on post_aggregates_mview (id);
|
||||
|
||||
create view post_view as
|
||||
with all_post as (
|
||||
select
|
||||
pa.*
|
||||
from post_aggregates_view pa
|
||||
)
|
||||
select
|
||||
ap.*,
|
||||
u.id as user_id,
|
||||
coalesce(pl.score, 0) as my_vote,
|
||||
(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
|
||||
(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
|
||||
(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
|
||||
from user_ u
|
||||
cross join all_post ap
|
||||
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
|
||||
|
||||
union all
|
||||
|
||||
select
|
||||
ap.*,
|
||||
null as user_id,
|
||||
null as my_vote,
|
||||
null as subscribed,
|
||||
null as read,
|
||||
null as saved
|
||||
from all_post ap
|
||||
;
|
||||
|
||||
create view post_mview as
|
||||
with all_post as (
|
||||
select
|
||||
pa.*
|
||||
from post_aggregates_mview pa
|
||||
)
|
||||
select
|
||||
ap.*,
|
||||
u.id as user_id,
|
||||
coalesce(pl.score, 0) as my_vote,
|
||||
(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
|
||||
(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
|
||||
(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
|
||||
from user_ u
|
||||
cross join all_post ap
|
||||
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
|
||||
|
||||
union all
|
||||
|
||||
select
|
||||
ap.*,
|
||||
null as user_id,
|
||||
null as my_vote,
|
||||
null as subscribed,
|
||||
null as read,
|
||||
null as saved
|
||||
from all_post ap
|
||||
;
|
||||
|
||||
|
|
@ -2,6 +2,7 @@ use super::*;
|
|||
use crate::send_email;
|
||||
use crate::settings::Settings;
|
||||
use diesel::PgConnection;
|
||||
use log::error;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
|
@ -128,7 +129,7 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
|
|||
// Let the uniqueness handle this fail
|
||||
match UserMention::create(&conn, &user_mention_form) {
|
||||
Ok(_mention) => (),
|
||||
Err(_e) => eprintln!("{}", &_e),
|
||||
Err(_e) => error!("{}", &_e),
|
||||
};
|
||||
|
||||
// Send an email to those users that have notifications on
|
||||
|
@ -145,7 +146,7 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
|
|||
);
|
||||
match send_email(subject, &mention_email, &mention_user.name, html) {
|
||||
Ok(_o) => _o,
|
||||
Err(e) => eprintln!("{}", e),
|
||||
Err(e) => error!("{}", e),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -174,7 +175,7 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
|
|||
);
|
||||
match send_email(subject, &comment_reply_email, &parent_user.name, html) {
|
||||
Ok(_o) => _o,
|
||||
Err(e) => eprintln!("{}", e),
|
||||
Err(e) => error!("{}", e),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -199,7 +200,7 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
|
|||
);
|
||||
match send_email(subject, &post_reply_email, &parent_user.name, html) {
|
||||
Ok(_o) => _o,
|
||||
Err(e) => eprintln!("{}", e),
|
||||
Err(e) => error!("{}", e),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -318,7 +319,7 @@ impl Perform<CommentResponse> for Oper<EditComment> {
|
|||
// Let the uniqueness handle this fail
|
||||
match UserMention::create(&conn, &user_mention_form) {
|
||||
Ok(_mention) => (),
|
||||
Err(_e) => eprintln!("{}", &_e),
|
||||
Err(_e) => error!("{}", &_e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
use super::*;
|
||||
use crate::apub::puller::{get_all_communities, get_remote_community};
|
||||
use crate::settings::Settings;
|
||||
use diesel::PgConnection;
|
||||
use std::str::FromStr;
|
||||
|
||||
|
@ -32,12 +34,13 @@ pub struct CommunityResponse {
|
|||
pub community: CommunityView,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ListCommunities {
|
||||
sort: String,
|
||||
page: Option<i64>,
|
||||
limit: Option<i64>,
|
||||
auth: Option<String>,
|
||||
local_only: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
|
@ -117,6 +120,13 @@ impl Perform<GetCommunityResponse> for Oper<GetCommunity> {
|
|||
fn perform(&self, conn: &PgConnection) -> Result<GetCommunityResponse, Error> {
|
||||
let data: &GetCommunity = &self.data;
|
||||
|
||||
if data.name.is_some()
|
||||
&& Settings::get().federation.enabled
|
||||
&& data.name.as_ref().unwrap().contains('@')
|
||||
{
|
||||
return get_remote_community(data.name.as_ref().unwrap());
|
||||
}
|
||||
|
||||
let user_id: Option<i32> = match &data.auth {
|
||||
Some(auth) => match Claims::decode(&auth) {
|
||||
Ok(claims) => {
|
||||
|
@ -333,6 +343,13 @@ impl Perform<ListCommunitiesResponse> for Oper<ListCommunities> {
|
|||
fn perform(&self, conn: &PgConnection) -> Result<ListCommunitiesResponse, Error> {
|
||||
let data: &ListCommunities = &self.data;
|
||||
|
||||
let local_only = data.local_only.unwrap_or(false);
|
||||
if Settings::get().federation.enabled && !local_only {
|
||||
return Ok(ListCommunitiesResponse {
|
||||
communities: get_all_communities()?,
|
||||
});
|
||||
}
|
||||
|
||||
let user_claims: Option<Claims> = match &data.auth {
|
||||
Some(auth) => match Claims::decode(&auth) {
|
||||
Ok(claims) => Some(claims.claims),
|
||||
|
|
|
@ -18,7 +18,8 @@ use crate::db::user_mention_view::*;
|
|||
use crate::db::user_view::*;
|
||||
use crate::db::*;
|
||||
use crate::{
|
||||
extract_usernames, naive_from_unix, naive_now, remove_slurs, slur_check, slurs_vec_to_str,
|
||||
extract_usernames, fetch_iframely_and_pictshare_data, naive_from_unix, naive_now, remove_slurs,
|
||||
slur_check, slurs_vec_to_str,
|
||||
};
|
||||
use diesel::PgConnection;
|
||||
use failure::Error;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use super::*;
|
||||
use crate::settings::Settings;
|
||||
use diesel::PgConnection;
|
||||
use std::str::FromStr;
|
||||
|
||||
|
@ -33,7 +34,7 @@ pub struct GetPostResponse {
|
|||
pub online: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetPosts {
|
||||
type_: String,
|
||||
sort: String,
|
||||
|
@ -43,9 +44,9 @@ pub struct GetPosts {
|
|||
auth: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetPostsResponse {
|
||||
posts: Vec<PostView>,
|
||||
pub posts: Vec<PostView>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
|
@ -110,6 +111,10 @@ impl Perform<PostResponse> for Oper<CreatePost> {
|
|||
return Err(APIError::err("site_ban").into());
|
||||
}
|
||||
|
||||
// Fetch Iframely and Pictshare cached image
|
||||
let (iframely_title, iframely_description, iframely_html, pictshare_thumbnail) =
|
||||
fetch_iframely_and_pictshare_data(data.url.to_owned());
|
||||
|
||||
let post_form = PostForm {
|
||||
name: data.name.to_owned(),
|
||||
url: data.url.to_owned(),
|
||||
|
@ -122,6 +127,10 @@ impl Perform<PostResponse> for Oper<CreatePost> {
|
|||
locked: None,
|
||||
stickied: None,
|
||||
updated: None,
|
||||
embed_title: iframely_title,
|
||||
embed_description: iframely_description,
|
||||
embed_html: iframely_html,
|
||||
thumbnail_url: pictshare_thumbnail,
|
||||
};
|
||||
|
||||
let inserted_post = match Post::create(&conn, &post_form) {
|
||||
|
@ -212,6 +221,11 @@ impl Perform<GetPostsResponse> for Oper<GetPosts> {
|
|||
fn perform(&self, conn: &PgConnection) -> Result<GetPostsResponse, Error> {
|
||||
let data: &GetPosts = &self.data;
|
||||
|
||||
if Settings::get().federation.enabled {
|
||||
// TODO: intercept here (but the type is wrong)
|
||||
//get_remote_community_posts(get_posts.community_id.unwrap())
|
||||
}
|
||||
|
||||
let user_claims: Option<Claims> = match &data.auth {
|
||||
Some(auth) => match Claims::decode(&auth) {
|
||||
Ok(claims) => Some(claims.claims),
|
||||
|
@ -353,6 +367,10 @@ impl Perform<PostResponse> for Oper<EditPost> {
|
|||
return Err(APIError::err("site_ban").into());
|
||||
}
|
||||
|
||||
// Fetch Iframely and Pictshare cached image
|
||||
let (iframely_title, iframely_description, iframely_html, pictshare_thumbnail) =
|
||||
fetch_iframely_and_pictshare_data(data.url.to_owned());
|
||||
|
||||
let post_form = PostForm {
|
||||
name: data.name.to_owned(),
|
||||
url: data.url.to_owned(),
|
||||
|
@ -365,6 +383,10 @@ impl Perform<PostResponse> for Oper<EditPost> {
|
|||
locked: data.locked.to_owned(),
|
||||
stickied: data.stickied.to_owned(),
|
||||
updated: Some(naive_now()),
|
||||
embed_title: iframely_title,
|
||||
embed_description: iframely_description,
|
||||
embed_html: iframely_html,
|
||||
thumbnail_url: pictshare_thumbnail,
|
||||
};
|
||||
|
||||
let _updated_post = match Post::update(&conn, data.edit_id, &post_form) {
|
||||
|
|
|
@ -3,6 +3,7 @@ use crate::settings::Settings;
|
|||
use crate::{generate_random_string, send_email};
|
||||
use bcrypt::verify;
|
||||
use diesel::PgConnection;
|
||||
use log::error;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
|
@ -466,7 +467,7 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
|
|||
}
|
||||
};
|
||||
|
||||
let user_view = UserView::read(&conn, user_details_id)?;
|
||||
let mut user_view = UserView::read(&conn, user_details_id)?;
|
||||
|
||||
let mut posts_query = PostQueryBuilder::create(&conn)
|
||||
.sort(&sort)
|
||||
|
@ -502,6 +503,15 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
|
|||
let creator_user = admins.remove(creator_index);
|
||||
admins.insert(0, creator_user);
|
||||
|
||||
// If its not the same user, remove the email
|
||||
if let Some(user_id) = user_id {
|
||||
if user_details_id != user_id {
|
||||
user_view.email = None;
|
||||
}
|
||||
} else {
|
||||
user_view.email = None;
|
||||
}
|
||||
|
||||
// Return the jwt
|
||||
Ok(GetUserDetailsResponse {
|
||||
user: user_view,
|
||||
|
@ -874,6 +884,10 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
|
|||
locked: None,
|
||||
stickied: None,
|
||||
updated: Some(naive_now()),
|
||||
embed_title: None,
|
||||
embed_description: None,
|
||||
embed_html: None,
|
||||
thumbnail_url: None,
|
||||
};
|
||||
|
||||
let _updated_post = match Post::update(&conn, post.id, &post_form) {
|
||||
|
@ -995,7 +1009,7 @@ impl Perform<PrivateMessageResponse> for Oper<CreatePrivateMessage> {
|
|||
);
|
||||
match send_email(subject, &email, &recipient_user.name, html) {
|
||||
Ok(_o) => _o,
|
||||
Err(e) => eprintln!("{}", e),
|
||||
Err(e) => error!("{}", e),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,109 +1,119 @@
|
|||
use crate::apub::make_apub_endpoint;
|
||||
use crate::apub::{create_apub_response, make_apub_endpoint, EndpointType};
|
||||
use crate::convert_datetime;
|
||||
use crate::db::community::Community;
|
||||
use crate::db::community_view::CommunityFollowerView;
|
||||
use crate::db::establish_unpooled_connection;
|
||||
use crate::to_datetime_utc;
|
||||
use activitypub::{actor::Group, collection::UnorderedCollection, context};
|
||||
use crate::db::post_view::{PostQueryBuilder, PostView};
|
||||
use activitystreams::collection::OrderedCollection;
|
||||
use activitystreams::{
|
||||
actor::{properties::ApActorProperties, Group},
|
||||
collection::UnorderedCollection,
|
||||
context,
|
||||
ext::Extensible,
|
||||
object::properties::ObjectProperties,
|
||||
};
|
||||
use actix_web::body::Body;
|
||||
use actix_web::web::Path;
|
||||
use actix_web::HttpResponse;
|
||||
use actix_web::{web, Result};
|
||||
use diesel::r2d2::{ConnectionManager, Pool};
|
||||
use diesel::PgConnection;
|
||||
use failure::Error;
|
||||
use serde::Deserialize;
|
||||
|
||||
impl Community {
|
||||
pub fn as_group(&self) -> Group {
|
||||
let base_url = make_apub_endpoint("c", &self.name);
|
||||
|
||||
let mut group = Group::default();
|
||||
|
||||
group.object_props.set_context_object(context()).ok();
|
||||
group.object_props.set_id_string(base_url.to_string()).ok();
|
||||
group
|
||||
.object_props
|
||||
.set_name_string(self.name.to_owned())
|
||||
.ok();
|
||||
group
|
||||
.object_props
|
||||
.set_published_utctime(to_datetime_utc(self.published))
|
||||
.ok();
|
||||
if let Some(updated) = self.updated {
|
||||
group
|
||||
.object_props
|
||||
.set_updated_utctime(to_datetime_utc(updated))
|
||||
.ok();
|
||||
}
|
||||
|
||||
if let Some(description) = &self.description {
|
||||
group
|
||||
.object_props
|
||||
.set_summary_string(description.to_string())
|
||||
.ok();
|
||||
}
|
||||
|
||||
group
|
||||
.ap_actor_props
|
||||
.set_inbox_string(format!("{}/inbox", &base_url))
|
||||
.ok();
|
||||
group
|
||||
.ap_actor_props
|
||||
.set_outbox_string(format!("{}/outbox", &base_url))
|
||||
.ok();
|
||||
group
|
||||
.ap_actor_props
|
||||
.set_followers_string(format!("{}/followers", &base_url))
|
||||
.ok();
|
||||
|
||||
group
|
||||
}
|
||||
|
||||
pub fn followers_as_collection(&self) -> UnorderedCollection {
|
||||
let base_url = make_apub_endpoint("c", &self.name);
|
||||
|
||||
let mut collection = UnorderedCollection::default();
|
||||
collection.object_props.set_context_object(context()).ok();
|
||||
collection.object_props.set_id_string(base_url).ok();
|
||||
|
||||
let connection = establish_unpooled_connection();
|
||||
//As we are an object, we validated that the community id was valid
|
||||
let community_followers = CommunityFollowerView::for_community(&connection, self.id).unwrap();
|
||||
|
||||
let ap_followers = community_followers
|
||||
.iter()
|
||||
.map(|follower| make_apub_endpoint("u", &follower.user_name))
|
||||
.collect();
|
||||
|
||||
collection
|
||||
.collection_props
|
||||
.set_items_string_vec(ap_followers)
|
||||
.unwrap();
|
||||
collection
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CommunityQuery {
|
||||
community_name: String,
|
||||
}
|
||||
|
||||
pub async fn get_apub_community(info: Path<CommunityQuery>) -> HttpResponse<Body> {
|
||||
let connection = establish_unpooled_connection();
|
||||
pub async fn get_apub_community(
|
||||
info: Path<CommunityQuery>,
|
||||
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
|
||||
) -> Result<HttpResponse<Body>, Error> {
|
||||
let community = Community::read_from_name(&&db.get()?, info.community_name.to_owned())?;
|
||||
let base_url = make_apub_endpoint(EndpointType::Community, &community.name);
|
||||
|
||||
if let Ok(community) = Community::read_from_name(&connection, info.community_name.to_owned()) {
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/activity+json")
|
||||
.body(serde_json::to_string(&community.as_group()).unwrap())
|
||||
} else {
|
||||
HttpResponse::NotFound().finish()
|
||||
let mut group = Group::default();
|
||||
let oprops: &mut ObjectProperties = group.as_mut();
|
||||
|
||||
oprops
|
||||
.set_context_xsd_any_uri(context())?
|
||||
.set_id(base_url.to_owned())?
|
||||
.set_name_xsd_string(community.title.to_owned())?
|
||||
.set_published(convert_datetime(community.published))?
|
||||
.set_attributed_to_xsd_any_uri(make_apub_endpoint(
|
||||
EndpointType::User,
|
||||
&community.creator_id.to_string(),
|
||||
))?;
|
||||
|
||||
if let Some(u) = community.updated.to_owned() {
|
||||
oprops.set_updated(convert_datetime(u))?;
|
||||
}
|
||||
if let Some(d) = community.description {
|
||||
oprops.set_summary_xsd_string(d)?;
|
||||
}
|
||||
|
||||
let mut actor_props = ApActorProperties::default();
|
||||
|
||||
actor_props
|
||||
.set_inbox(format!("{}/inbox", &base_url))?
|
||||
.set_outbox(format!("{}/outbox", &base_url))?
|
||||
.set_followers(format!("{}/followers", &base_url))?;
|
||||
|
||||
Ok(create_apub_response(&group.extend(actor_props)))
|
||||
}
|
||||
|
||||
pub async fn get_apub_community_followers(info: Path<CommunityQuery>) -> HttpResponse<Body> {
|
||||
let connection = establish_unpooled_connection();
|
||||
pub async fn get_apub_community_followers(
|
||||
info: Path<CommunityQuery>,
|
||||
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
|
||||
) -> Result<HttpResponse<Body>, Error> {
|
||||
let community = Community::read_from_name(&&db.get()?, info.community_name.to_owned())?;
|
||||
let base_url = make_apub_endpoint(EndpointType::Community, &community.name);
|
||||
|
||||
if let Ok(community) = Community::read_from_name(&connection, info.community_name.to_owned()) {
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/activity+json")
|
||||
.body(serde_json::to_string(&community.followers_as_collection()).unwrap())
|
||||
} else {
|
||||
HttpResponse::NotFound().finish()
|
||||
}
|
||||
let connection = establish_unpooled_connection();
|
||||
//As we are an object, we validated that the community id was valid
|
||||
let community_followers =
|
||||
CommunityFollowerView::for_community(&connection, community.id).unwrap();
|
||||
|
||||
let mut collection = UnorderedCollection::default();
|
||||
let oprops: &mut ObjectProperties = collection.as_mut();
|
||||
oprops
|
||||
.set_context_xsd_any_uri(context())?
|
||||
.set_id(base_url)?;
|
||||
collection
|
||||
.collection_props
|
||||
.set_total_items(community_followers.len() as u64)?;
|
||||
Ok(create_apub_response(&collection))
|
||||
}
|
||||
|
||||
pub async fn get_apub_community_outbox(
|
||||
info: Path<CommunityQuery>,
|
||||
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
|
||||
) -> Result<HttpResponse<Body>, Error> {
|
||||
let community = Community::read_from_name(&&db.get()?, info.community_name.to_owned())?;
|
||||
let base_url = make_apub_endpoint(EndpointType::Community, &community.name);
|
||||
|
||||
let connection = establish_unpooled_connection();
|
||||
//As we are an object, we validated that the community id was valid
|
||||
let community_posts: Vec<PostView> = PostQueryBuilder::create(&connection)
|
||||
.for_community_id(community.id)
|
||||
.list()
|
||||
.unwrap();
|
||||
|
||||
let mut collection = OrderedCollection::default();
|
||||
let oprops: &mut ObjectProperties = collection.as_mut();
|
||||
oprops
|
||||
.set_context_xsd_any_uri(context())?
|
||||
.set_id(base_url)?;
|
||||
collection
|
||||
.collection_props
|
||||
.set_many_items_object_boxs(
|
||||
community_posts
|
||||
.iter()
|
||||
.map(|c| c.as_page().unwrap())
|
||||
.collect(),
|
||||
)?
|
||||
.set_total_items(community_posts.len() as u64)?;
|
||||
|
||||
Ok(create_apub_response(&collection))
|
||||
}
|
||||
|
|
|
@ -4,101 +4,46 @@ pub mod puller;
|
|||
pub mod user;
|
||||
use crate::Settings;
|
||||
|
||||
use std::fmt::Display;
|
||||
use actix_web::body::Body;
|
||||
use actix_web::HttpResponse;
|
||||
use url::Url;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::db::community::Community;
|
||||
use crate::db::post::Post;
|
||||
use crate::db::user::User_;
|
||||
use crate::db::{ListingType, SortType};
|
||||
use crate::{naive_now, Settings};
|
||||
|
||||
#[test]
|
||||
fn test_person() {
|
||||
let user = User_ {
|
||||
id: 52,
|
||||
name: "thom".into(),
|
||||
fedi_name: "rrf".into(),
|
||||
preferred_username: None,
|
||||
password_encrypted: "here".into(),
|
||||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
published: naive_now(),
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
show_nsfw: false,
|
||||
theme: "darkly".into(),
|
||||
default_sort_type: SortType::Hot as i16,
|
||||
default_listing_type: ListingType::Subscribed as i16,
|
||||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
};
|
||||
|
||||
let person = user.as_person();
|
||||
assert_eq!(
|
||||
format!("https://{}/federation/u/thom", Settings::get().hostname),
|
||||
person.object_props.id_string().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_community() {
|
||||
let community = Community {
|
||||
id: 42,
|
||||
name: "Test".into(),
|
||||
title: "Test Title".into(),
|
||||
description: Some("Test community".into()),
|
||||
category_id: 32,
|
||||
creator_id: 52,
|
||||
removed: false,
|
||||
published: naive_now(),
|
||||
updated: Some(naive_now()),
|
||||
deleted: false,
|
||||
nsfw: false,
|
||||
};
|
||||
|
||||
let group = community.as_group();
|
||||
assert_eq!(
|
||||
format!("https://{}/federation/c/Test", Settings::get().hostname),
|
||||
group.object_props.id_string().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_post() {
|
||||
let post = Post {
|
||||
id: 62,
|
||||
name: "A test post".into(),
|
||||
url: None,
|
||||
body: None,
|
||||
creator_id: 52,
|
||||
community_id: 42,
|
||||
published: naive_now(),
|
||||
removed: false,
|
||||
locked: false,
|
||||
stickied: false,
|
||||
nsfw: false,
|
||||
deleted: false,
|
||||
updated: None,
|
||||
};
|
||||
|
||||
let page = post.as_page();
|
||||
assert_eq!(
|
||||
format!("https://{}/federation/post/62", Settings::get().hostname),
|
||||
page.object_props.id_string().unwrap()
|
||||
);
|
||||
}
|
||||
fn create_apub_response<T>(json: &T) -> HttpResponse<Body>
|
||||
where
|
||||
T: serde::ser::Serialize,
|
||||
{
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/activity+json")
|
||||
.json(json)
|
||||
}
|
||||
|
||||
pub fn make_apub_endpoint<S: Display, T: Display>(point: S, value: T) -> String {
|
||||
format!(
|
||||
"https://{}/federation/{}/{}",
|
||||
enum EndpointType {
|
||||
Community,
|
||||
User,
|
||||
Post,
|
||||
}
|
||||
|
||||
fn make_apub_endpoint(endpoint_type: EndpointType, name: &str) -> Url {
|
||||
let point = match endpoint_type {
|
||||
EndpointType::Community => "c",
|
||||
EndpointType::User => "u",
|
||||
EndpointType::Post => "p",
|
||||
};
|
||||
|
||||
Url::parse(&format!(
|
||||
"{}://{}/federation/{}/{}",
|
||||
get_apub_protocol_string(),
|
||||
Settings::get().hostname,
|
||||
point,
|
||||
value
|
||||
)
|
||||
name
|
||||
))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn get_apub_protocol_string() -> &'static str {
|
||||
if Settings::get().federation.tls_enabled {
|
||||
"https"
|
||||
} else {
|
||||
"http"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,38 +1,62 @@
|
|||
use crate::apub::make_apub_endpoint;
|
||||
use crate::db::post::Post;
|
||||
use crate::to_datetime_utc;
|
||||
use activitypub::{context, object::Page};
|
||||
use crate::apub::{create_apub_response, make_apub_endpoint, EndpointType};
|
||||
use crate::convert_datetime;
|
||||
use crate::db::post_view::PostView;
|
||||
use activitystreams::{object::properties::ObjectProperties, object::Page};
|
||||
use actix_web::body::Body;
|
||||
use actix_web::web::Path;
|
||||
use actix_web::{web, HttpResponse};
|
||||
use diesel::r2d2::{ConnectionManager, Pool};
|
||||
use diesel::PgConnection;
|
||||
use failure::Error;
|
||||
use serde::Deserialize;
|
||||
|
||||
impl Post {
|
||||
pub fn as_page(&self) -> Page {
|
||||
let base_url = make_apub_endpoint("post", self.id);
|
||||
#[derive(Deserialize)]
|
||||
pub struct PostQuery {
|
||||
post_id: String,
|
||||
}
|
||||
|
||||
pub async fn get_apub_post(
|
||||
info: Path<PostQuery>,
|
||||
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
|
||||
) -> Result<HttpResponse<Body>, Error> {
|
||||
let id = info.post_id.parse::<i32>()?;
|
||||
// TODO: shows error: missing field `user_name`
|
||||
let post = PostView::read(&&db.get()?, id, None)?;
|
||||
Ok(create_apub_response(&post.as_page()?))
|
||||
}
|
||||
|
||||
impl PostView {
|
||||
pub fn as_page(&self) -> Result<Page, Error> {
|
||||
let base_url = make_apub_endpoint(EndpointType::Post, &self.id.to_string());
|
||||
let mut page = Page::default();
|
||||
let oprops: &mut ObjectProperties = page.as_mut();
|
||||
|
||||
page.object_props.set_context_object(context()).ok();
|
||||
page.object_props.set_id_string(base_url).ok();
|
||||
page.object_props.set_name_string(self.name.to_owned()).ok();
|
||||
oprops
|
||||
// Not needed when the Post is embedded in a collection (like for community outbox)
|
||||
//.set_context_xsd_any_uri(context())?
|
||||
.set_id(base_url)?
|
||||
.set_name_xsd_string(self.name.to_owned())?
|
||||
.set_published(convert_datetime(self.published))?
|
||||
.set_attributed_to_xsd_any_uri(make_apub_endpoint(
|
||||
EndpointType::User,
|
||||
&self.creator_id.to_string(),
|
||||
))?;
|
||||
|
||||
if let Some(body) = &self.body {
|
||||
page.object_props.set_content_string(body.to_owned()).ok();
|
||||
oprops.set_content_xsd_string(body.to_owned())?;
|
||||
}
|
||||
|
||||
if let Some(url) = &self.url {
|
||||
page.object_props.set_url_string(url.to_owned()).ok();
|
||||
// TODO: hacky code because we get self.url == Some("")
|
||||
// https://github.com/dessalines/lemmy/issues/602
|
||||
let url = self.url.as_ref().filter(|u| !u.is_empty());
|
||||
if let Some(u) = url {
|
||||
oprops.set_url_xsd_any_uri(u.to_owned())?;
|
||||
}
|
||||
|
||||
//page.object_props.set_attributed_to_string
|
||||
|
||||
page
|
||||
.object_props
|
||||
.set_published_utctime(to_datetime_utc(self.published))
|
||||
.ok();
|
||||
if let Some(updated) = self.updated {
|
||||
page
|
||||
.object_props
|
||||
.set_updated_utctime(to_datetime_utc(updated))
|
||||
.ok();
|
||||
if let Some(u) = self.updated {
|
||||
oprops.set_updated(convert_datetime(u))?;
|
||||
}
|
||||
|
||||
page
|
||||
Ok(page)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,47 @@
|
|||
extern crate reqwest;
|
||||
|
||||
use self::reqwest::Error;
|
||||
use crate::api::community::{GetCommunityResponse, ListCommunitiesResponse};
|
||||
use crate::api::post::GetPosts;
|
||||
use crate::api::post::GetPostsResponse;
|
||||
use crate::apub::get_apub_protocol_string;
|
||||
use crate::db::community_view::CommunityView;
|
||||
use crate::db::post_view::PostView;
|
||||
use crate::naive_now;
|
||||
use crate::routes::nodeinfo::{NodeInfo, NodeInfoWellKnown};
|
||||
use crate::settings::Settings;
|
||||
use activitypub::actor::Group;
|
||||
use activitystreams::actor::{properties::ApActorProperties, Group};
|
||||
use activitystreams::collection::{OrderedCollection, UnorderedCollection};
|
||||
use activitystreams::ext::Ext;
|
||||
use activitystreams::object::ObjectBox;
|
||||
use activitystreams::object::Page;
|
||||
use chttp::prelude::*;
|
||||
use failure::Error;
|
||||
use log::warn;
|
||||
use serde::Deserialize;
|
||||
|
||||
// TODO: right now all of the data is requested on demand, for production we will need to store
|
||||
// things in the local database to not ruin the performance
|
||||
fn fetch_node_info(domain: &str) -> Result<NodeInfo, Error> {
|
||||
let well_known_uri = format!(
|
||||
"{}://{}/.well-known/nodeinfo",
|
||||
get_apub_protocol_string(),
|
||||
domain
|
||||
);
|
||||
let well_known = fetch_remote_object::<NodeInfoWellKnown>(&well_known_uri)?;
|
||||
Ok(fetch_remote_object::<NodeInfo>(&well_known.links.href)?)
|
||||
}
|
||||
|
||||
fn fetch_communities_from_instance(domain: &str) -> Result<Vec<CommunityView>, Error> {
|
||||
// TODO: check nodeinfo to make sure we are dealing with a lemmy instance
|
||||
// -> means we need proper nodeinfo json classes instead of inline generation
|
||||
let node_info = fetch_node_info(domain)?;
|
||||
if node_info.software.name != "lemmy" {
|
||||
return Err(format_err!(
|
||||
"{} is not a Lemmy instance, federation is not supported",
|
||||
domain
|
||||
));
|
||||
}
|
||||
|
||||
// TODO: follow pagination (seems like page count is missing?)
|
||||
// TODO: see if there is any standard for discovering remote actors, so we dont have to rely on lemmy apis
|
||||
let communities_uri = format!("http://{}/api/v1/communities/list?sort=Hot", domain);
|
||||
let communities1: ListCommunitiesResponse = reqwest::get(&communities_uri)?.json()?;
|
||||
let communities_uri = format!(
|
||||
"http://{}/api/v1/communities/list?sort=Hot&local_only=true",
|
||||
domain
|
||||
);
|
||||
let communities1 = fetch_remote_object::<ListCommunitiesResponse>(&communities_uri)?;
|
||||
let mut communities2 = communities1.communities;
|
||||
for c in &mut communities2 {
|
||||
c.name = format_community_name(&c.name, domain);
|
||||
|
@ -25,43 +49,148 @@ fn fetch_communities_from_instance(domain: &str) -> Result<Vec<CommunityView>, E
|
|||
Ok(communities2)
|
||||
}
|
||||
|
||||
pub fn get_remote_community_posts(name: String) -> Result<GetPosts, Error> {
|
||||
// TODO: this is for urls like /c/!main@example.com, activitypub exposes it through the outbox
|
||||
// https://www.w3.org/TR/activitypub/#outbox
|
||||
dbg!(name);
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn get_remote_community(identifier: String) -> Result<GetCommunityResponse, Error> {
|
||||
fn get_remote_community_uri(identifier: &str) -> String {
|
||||
let x: Vec<&str> = identifier.split('@').collect();
|
||||
let name = x[0].replace("!", "");
|
||||
let instance = x[1];
|
||||
let community_uri = format!("http://{}/federation/c/{}", instance, name);
|
||||
let community: Group = reqwest::get(&community_uri)?.json()?;
|
||||
format!("http://{}/federation/c/{}", instance, name)
|
||||
}
|
||||
|
||||
fn fetch_remote_object<Response>(uri: &str) -> Result<Response, Error>
|
||||
where
|
||||
Response: for<'de> Deserialize<'de>,
|
||||
{
|
||||
if Settings::get().federation.tls_enabled && !uri.starts_with("https") {
|
||||
return Err(format_err!("Activitypub uri is insecure: {}", uri));
|
||||
}
|
||||
// TODO: should cache responses here when we are in production
|
||||
// TODO: this function should return a future
|
||||
let text = chttp::get(uri)?.text()?;
|
||||
let res: Response = serde_json::from_str(&text)?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn get_remote_community_posts(identifier: &str) -> Result<GetPostsResponse, Error> {
|
||||
let community =
|
||||
fetch_remote_object::<Ext<Group, ApActorProperties>>(&get_remote_community_uri(identifier))?;
|
||||
let outbox_uri = &community.extension.get_outbox().to_string();
|
||||
let outbox = fetch_remote_object::<OrderedCollection>(outbox_uri)?;
|
||||
let items = outbox.collection_props.get_many_items_object_boxs();
|
||||
|
||||
let posts: Vec<PostView> = items
|
||||
.unwrap()
|
||||
.map(|obox: &ObjectBox| {
|
||||
let page: Page = obox.clone().to_concrete::<Page>().unwrap();
|
||||
PostView {
|
||||
id: -1,
|
||||
name: page.object_props.get_name_xsd_string().unwrap().to_string(),
|
||||
url: page
|
||||
.object_props
|
||||
.get_url_xsd_any_uri()
|
||||
.map(|u| u.to_string()),
|
||||
body: page
|
||||
.object_props
|
||||
.get_content_xsd_string()
|
||||
.map(|c| c.to_string()),
|
||||
creator_id: -1,
|
||||
community_id: -1,
|
||||
removed: false,
|
||||
locked: false,
|
||||
published: page
|
||||
.object_props
|
||||
.get_published()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.naive_local()
|
||||
.to_owned(),
|
||||
updated: page
|
||||
.object_props
|
||||
.get_updated()
|
||||
.map(|u| u.as_ref().to_owned().naive_local()),
|
||||
deleted: false,
|
||||
nsfw: false,
|
||||
stickied: false,
|
||||
embed_title: None,
|
||||
embed_description: None,
|
||||
embed_html: None,
|
||||
thumbnail_url: None,
|
||||
banned: false,
|
||||
banned_from_community: false,
|
||||
creator_name: "".to_string(),
|
||||
creator_avatar: None,
|
||||
community_name: "".to_string(),
|
||||
community_removed: false,
|
||||
community_deleted: false,
|
||||
community_nsfw: false,
|
||||
number_of_comments: -1,
|
||||
score: -1,
|
||||
upvotes: -1,
|
||||
downvotes: -1,
|
||||
hot_rank: -1,
|
||||
newest_activity_time: naive_now(),
|
||||
user_id: None,
|
||||
my_vote: None,
|
||||
subscribed: None,
|
||||
read: None,
|
||||
saved: None,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(GetPostsResponse { posts })
|
||||
}
|
||||
|
||||
pub fn get_remote_community(identifier: &str) -> Result<GetCommunityResponse, failure::Error> {
|
||||
let community =
|
||||
fetch_remote_object::<Ext<Group, ApActorProperties>>(&get_remote_community_uri(identifier))?;
|
||||
let followers_uri = &community.extension.get_followers().unwrap().to_string();
|
||||
let outbox_uri = &community.extension.get_outbox().to_string();
|
||||
let outbox = fetch_remote_object::<OrderedCollection>(outbox_uri)?;
|
||||
let followers = fetch_remote_object::<UnorderedCollection>(followers_uri)?;
|
||||
// TODO: this is only for testing until we can call that function from GetPosts
|
||||
// (once string ids are supported)
|
||||
//dbg!(get_remote_community_posts(identifier)?);
|
||||
|
||||
// TODO: looks like a bunch of data is missing from the activitypub response
|
||||
// TODO: i dont think simple numeric ids are going to work, we probably need something like uuids
|
||||
// TODO: why are the Group properties not typed?
|
||||
Ok(GetCommunityResponse {
|
||||
moderators: vec![],
|
||||
admins: vec![],
|
||||
community: CommunityView {
|
||||
id: -1,
|
||||
name: identifier.clone(),
|
||||
title: identifier,
|
||||
description: community.object_props.summary.map(|c| c.to_string()),
|
||||
// TODO: we need to merge id and name into a single thing (stuff like @user@instance.com)
|
||||
id: 1337, //community.object_props.get_id()
|
||||
name: identifier.to_string(),
|
||||
title: community
|
||||
.as_ref()
|
||||
.get_name_xsd_string()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
description: community
|
||||
.as_ref()
|
||||
.get_summary_xsd_string()
|
||||
.map(|s| s.to_string()),
|
||||
category_id: -1,
|
||||
creator_id: -1,
|
||||
creator_id: -1, //community.object_props.get_attributed_to_xsd_any_uri()
|
||||
removed: false,
|
||||
published: naive_now(), // TODO: community.object_props.published
|
||||
updated: Some(naive_now()), // TODO: community.object_props.updated
|
||||
published: community
|
||||
.as_ref()
|
||||
.get_published()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.naive_local()
|
||||
.to_owned(),
|
||||
updated: community
|
||||
.as_ref()
|
||||
.get_updated()
|
||||
.map(|u| u.as_ref().to_owned().naive_local()),
|
||||
deleted: false,
|
||||
nsfw: false,
|
||||
creator_name: "".to_string(),
|
||||
creator_avatar: None,
|
||||
category_name: "".to_string(),
|
||||
number_of_subscribers: -1,
|
||||
number_of_posts: -1,
|
||||
number_of_subscribers: *followers
|
||||
.collection_props
|
||||
.get_total_items()
|
||||
.unwrap()
|
||||
.as_ref() as i64, // TODO: need to use the same type
|
||||
number_of_posts: *outbox.collection_props.get_total_items().unwrap().as_ref() as i64,
|
||||
number_of_comments: -1,
|
||||
hot_rank: -1,
|
||||
user_id: None,
|
||||
|
@ -71,23 +200,27 @@ pub fn get_remote_community(identifier: String) -> Result<GetCommunityResponse,
|
|||
})
|
||||
}
|
||||
|
||||
pub fn get_following_instances() -> Result<Vec<String>, Error> {
|
||||
let instance_list = match Settings::get().federated_instance.clone() {
|
||||
Some(f) => vec![f, Settings::get().hostname.clone()],
|
||||
None => vec![Settings::get().hostname.clone()],
|
||||
};
|
||||
Ok(instance_list)
|
||||
pub fn get_following_instances() -> Vec<&'static str> {
|
||||
Settings::get()
|
||||
.federation
|
||||
.followed_instances
|
||||
.split(',')
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_all_communities() -> Result<Vec<CommunityView>, Error> {
|
||||
let mut communities_list: Vec<CommunityView> = vec![];
|
||||
for instance in &get_following_instances()? {
|
||||
communities_list.append(fetch_communities_from_instance(instance)?.as_mut());
|
||||
for instance in &get_following_instances() {
|
||||
match fetch_communities_from_instance(instance) {
|
||||
Ok(mut c) => communities_list.append(c.as_mut()),
|
||||
Err(e) => warn!("Failed to fetch instance list from remote instance: {}", e),
|
||||
};
|
||||
}
|
||||
Ok(communities_list)
|
||||
}
|
||||
|
||||
/// If community is on local instance, don't include the @instance part
|
||||
/// If community is on local instance, don't include the @instance part. This is only for displaying
|
||||
/// to the user and should never be used otherwise.
|
||||
pub fn format_community_name(name: &str, instance: &str) -> String {
|
||||
if instance == Settings::get().hostname {
|
||||
format!("!{}", name)
|
||||
|
|
|
@ -1,74 +1,55 @@
|
|||
use crate::apub::make_apub_endpoint;
|
||||
use crate::db::establish_unpooled_connection;
|
||||
use crate::apub::{create_apub_response, make_apub_endpoint, EndpointType};
|
||||
use crate::convert_datetime;
|
||||
use crate::db::user::User_;
|
||||
use crate::to_datetime_utc;
|
||||
use activitypub::{actor::Person, context};
|
||||
use activitystreams::{
|
||||
actor::{properties::ApActorProperties, Person},
|
||||
context,
|
||||
ext::Extensible,
|
||||
object::properties::ObjectProperties,
|
||||
};
|
||||
use actix_web::body::Body;
|
||||
use actix_web::web::Path;
|
||||
use actix_web::HttpResponse;
|
||||
use actix_web::{web, Result};
|
||||
use diesel::r2d2::{ConnectionManager, Pool};
|
||||
use diesel::PgConnection;
|
||||
use failure::Error;
|
||||
use serde::Deserialize;
|
||||
|
||||
impl User_ {
|
||||
pub fn as_person(&self) -> Person {
|
||||
let base_url = make_apub_endpoint("u", &self.name);
|
||||
let mut person = Person::default();
|
||||
person.object_props.set_context_object(context()).ok();
|
||||
person.object_props.set_id_string(base_url.to_string()).ok();
|
||||
person
|
||||
.object_props
|
||||
.set_name_string(self.name.to_owned())
|
||||
.ok();
|
||||
person
|
||||
.object_props
|
||||
.set_published_utctime(to_datetime_utc(self.published))
|
||||
.ok();
|
||||
if let Some(updated) = self.updated {
|
||||
person
|
||||
.object_props
|
||||
.set_updated_utctime(to_datetime_utc(updated))
|
||||
.ok();
|
||||
}
|
||||
|
||||
person
|
||||
.ap_actor_props
|
||||
.set_inbox_string(format!("{}/inbox", &base_url))
|
||||
.ok();
|
||||
person
|
||||
.ap_actor_props
|
||||
.set_outbox_string(format!("{}/outbox", &base_url))
|
||||
.ok();
|
||||
person
|
||||
.ap_actor_props
|
||||
.set_following_string(format!("{}/following", &base_url))
|
||||
.ok();
|
||||
person
|
||||
.ap_actor_props
|
||||
.set_liked_string(format!("{}/liked", &base_url))
|
||||
.ok();
|
||||
if let Some(i) = &self.preferred_username {
|
||||
person
|
||||
.ap_actor_props
|
||||
.set_preferred_username_string(i.to_string())
|
||||
.ok();
|
||||
}
|
||||
|
||||
person
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UserQuery {
|
||||
user_name: String,
|
||||
}
|
||||
|
||||
pub async fn get_apub_user(info: Path<UserQuery>) -> HttpResponse<Body> {
|
||||
let connection = establish_unpooled_connection();
|
||||
pub async fn get_apub_user(
|
||||
info: Path<UserQuery>,
|
||||
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
|
||||
) -> Result<HttpResponse<Body>, Error> {
|
||||
let user = User_::find_by_email_or_username(&&db.get()?, &info.user_name)?;
|
||||
let base_url = make_apub_endpoint(EndpointType::User, &user.name);
|
||||
|
||||
if let Ok(user) = User_::find_by_email_or_username(&connection, &info.user_name) {
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/activity+json")
|
||||
.body(serde_json::to_string(&user.as_person()).unwrap())
|
||||
} else {
|
||||
HttpResponse::NotFound().finish()
|
||||
let mut person = Person::default();
|
||||
let oprops: &mut ObjectProperties = person.as_mut();
|
||||
oprops
|
||||
.set_context_xsd_any_uri(context())?
|
||||
.set_id(base_url.to_string())?
|
||||
.set_published(convert_datetime(user.published))?;
|
||||
|
||||
if let Some(u) = user.updated {
|
||||
oprops.set_updated(convert_datetime(u))?;
|
||||
}
|
||||
|
||||
if let Some(i) = &user.preferred_username {
|
||||
oprops.set_name_xsd_string(i.to_owned())?;
|
||||
}
|
||||
|
||||
let mut actor_props = ApActorProperties::default();
|
||||
|
||||
actor_props
|
||||
.set_inbox(format!("{}/inbox", &base_url))?
|
||||
.set_outbox(format!("{}/outbox", &base_url))?
|
||||
.set_following(format!("{}/following", &base_url))?
|
||||
.set_liked(format!("{}/liked", &base_url))?;
|
||||
|
||||
Ok(create_apub_response(&person.extend(actor_props)))
|
||||
}
|
||||
|
|
|
@ -216,6 +216,10 @@ mod tests {
|
|||
stickied: None,
|
||||
updated: None,
|
||||
nsfw: false,
|
||||
embed_title: None,
|
||||
embed_description: None,
|
||||
embed_html: None,
|
||||
thumbnail_url: None,
|
||||
};
|
||||
|
||||
let inserted_post = Post::create(&conn, &new_post).unwrap();
|
||||
|
|
|
@ -480,6 +480,10 @@ mod tests {
|
|||
stickied: None,
|
||||
updated: None,
|
||||
nsfw: false,
|
||||
embed_title: None,
|
||||
embed_description: None,
|
||||
embed_html: None,
|
||||
thumbnail_url: None,
|
||||
};
|
||||
|
||||
let inserted_post = Post::create(&conn, &new_post).unwrap();
|
||||
|
|
|
@ -180,7 +180,11 @@ impl<'a> CommunityQueryBuilder<'a> {
|
|||
let mut query = self.query;
|
||||
|
||||
if let Some(search_term) = self.search_term {
|
||||
query = query.filter(name.ilike(fuzzy_search(&search_term)));
|
||||
let searcher = fuzzy_search(&search_term);
|
||||
query = query
|
||||
.filter(name.ilike(searcher.to_owned()))
|
||||
.or_filter(title.ilike(searcher.to_owned()))
|
||||
.or_filter(description.ilike(searcher));
|
||||
};
|
||||
|
||||
// The view lets you pass a null user_id, if you're not logged in
|
||||
|
|
|
@ -506,6 +506,10 @@ mod tests {
|
|||
stickied: None,
|
||||
updated: None,
|
||||
nsfw: false,
|
||||
embed_title: None,
|
||||
embed_description: None,
|
||||
embed_html: None,
|
||||
thumbnail_url: None,
|
||||
};
|
||||
|
||||
let inserted_post = Post::create(&conn, &new_post).unwrap();
|
||||
|
|
|
@ -17,6 +17,10 @@ pub struct Post {
|
|||
pub deleted: bool,
|
||||
pub nsfw: bool,
|
||||
pub stickied: bool,
|
||||
pub embed_title: Option<String>,
|
||||
pub embed_description: Option<String>,
|
||||
pub embed_html: Option<String>,
|
||||
pub thumbnail_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Insertable, AsChangeset, Clone)]
|
||||
|
@ -33,6 +37,10 @@ pub struct PostForm {
|
|||
pub deleted: Option<bool>,
|
||||
pub nsfw: bool,
|
||||
pub stickied: Option<bool>,
|
||||
pub embed_title: Option<String>,
|
||||
pub embed_description: Option<String>,
|
||||
pub embed_html: Option<String>,
|
||||
pub thumbnail_url: Option<String>,
|
||||
}
|
||||
|
||||
impl Crud<PostForm> for Post {
|
||||
|
@ -229,6 +237,10 @@ mod tests {
|
|||
stickied: None,
|
||||
nsfw: false,
|
||||
updated: None,
|
||||
embed_title: None,
|
||||
embed_description: None,
|
||||
embed_html: None,
|
||||
thumbnail_url: None,
|
||||
};
|
||||
|
||||
let inserted_post = Post::create(&conn, &new_post).unwrap();
|
||||
|
@ -247,6 +259,10 @@ mod tests {
|
|||
nsfw: false,
|
||||
deleted: false,
|
||||
updated: None,
|
||||
embed_title: None,
|
||||
embed_description: None,
|
||||
embed_html: None,
|
||||
thumbnail_url: None,
|
||||
};
|
||||
|
||||
// Post Like
|
||||
|
|
|
@ -17,9 +17,54 @@ table! {
|
|||
updated -> Nullable<Timestamp>,
|
||||
deleted -> Bool,
|
||||
nsfw -> Bool,
|
||||
stickied -> Bool,
|
||||
embed_title -> Nullable<Text>,
|
||||
embed_description -> Nullable<Text>,
|
||||
embed_html -> Nullable<Text>,
|
||||
thumbnail_url -> Nullable<Text>,
|
||||
banned -> Bool,
|
||||
banned_from_community -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
community_name -> Varchar,
|
||||
community_removed -> Bool,
|
||||
community_deleted -> Bool,
|
||||
community_nsfw -> Bool,
|
||||
number_of_comments -> BigInt,
|
||||
score -> BigInt,
|
||||
upvotes -> BigInt,
|
||||
downvotes -> BigInt,
|
||||
hot_rank -> Int4,
|
||||
newest_activity_time -> Timestamp,
|
||||
user_id -> Nullable<Int4>,
|
||||
my_vote -> Nullable<Int4>,
|
||||
subscribed -> Nullable<Bool>,
|
||||
read -> Nullable<Bool>,
|
||||
saved -> Nullable<Bool>,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
post_mview (id) {
|
||||
id -> Int4,
|
||||
name -> Varchar,
|
||||
url -> Nullable<Text>,
|
||||
body -> Nullable<Text>,
|
||||
creator_id -> Int4,
|
||||
community_id -> Int4,
|
||||
removed -> Bool,
|
||||
locked -> Bool,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
deleted -> Bool,
|
||||
nsfw -> Bool,
|
||||
stickied -> Bool,
|
||||
embed_title -> Nullable<Text>,
|
||||
embed_description -> Nullable<Text>,
|
||||
embed_html -> Nullable<Text>,
|
||||
thumbnail_url -> Nullable<Text>,
|
||||
banned -> Bool,
|
||||
banned_from_community -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
community_name -> Varchar,
|
||||
|
@ -57,9 +102,13 @@ pub struct PostView {
|
|||
pub updated: Option<chrono::NaiveDateTime>,
|
||||
pub deleted: bool,
|
||||
pub nsfw: bool,
|
||||
pub stickied: bool,
|
||||
pub embed_title: Option<String>,
|
||||
pub embed_description: Option<String>,
|
||||
pub embed_html: Option<String>,
|
||||
pub thumbnail_url: Option<String>,
|
||||
pub banned: bool,
|
||||
pub banned_from_community: bool,
|
||||
pub stickied: bool,
|
||||
pub creator_name: String,
|
||||
pub creator_avatar: Option<String>,
|
||||
pub community_name: String,
|
||||
|
@ -79,44 +128,6 @@ pub struct PostView {
|
|||
pub saved: Option<bool>,
|
||||
}
|
||||
|
||||
// The faked schema since diesel doesn't do views
|
||||
table! {
|
||||
post_mview (id) {
|
||||
id -> Int4,
|
||||
name -> Varchar,
|
||||
url -> Nullable<Text>,
|
||||
body -> Nullable<Text>,
|
||||
creator_id -> Int4,
|
||||
community_id -> Int4,
|
||||
removed -> Bool,
|
||||
locked -> Bool,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
deleted -> Bool,
|
||||
nsfw -> Bool,
|
||||
banned -> Bool,
|
||||
banned_from_community -> Bool,
|
||||
stickied -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
community_name -> Varchar,
|
||||
community_removed -> Bool,
|
||||
community_deleted -> Bool,
|
||||
community_nsfw -> Bool,
|
||||
number_of_comments -> BigInt,
|
||||
score -> BigInt,
|
||||
upvotes -> BigInt,
|
||||
downvotes -> BigInt,
|
||||
hot_rank -> Int4,
|
||||
newest_activity_time -> Timestamp,
|
||||
user_id -> Nullable<Int4>,
|
||||
my_vote -> Nullable<Int4>,
|
||||
subscribed -> Nullable<Bool>,
|
||||
read -> Nullable<Bool>,
|
||||
saved -> Nullable<Bool>,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PostQueryBuilder<'a> {
|
||||
conn: &'a PgConnection,
|
||||
query: BoxedQuery<'a, Pg>,
|
||||
|
@ -394,6 +405,10 @@ mod tests {
|
|||
stickied: None,
|
||||
updated: None,
|
||||
nsfw: false,
|
||||
embed_title: None,
|
||||
embed_description: None,
|
||||
embed_html: None,
|
||||
thumbnail_url: None,
|
||||
};
|
||||
|
||||
let inserted_post = Post::create(&conn, &new_post).unwrap();
|
||||
|
@ -454,6 +469,10 @@ mod tests {
|
|||
read: None,
|
||||
saved: None,
|
||||
nsfw: false,
|
||||
embed_title: None,
|
||||
embed_description: None,
|
||||
embed_html: None,
|
||||
thumbnail_url: None,
|
||||
};
|
||||
|
||||
let expected_post_listing_with_user = PostView {
|
||||
|
@ -489,6 +508,10 @@ mod tests {
|
|||
read: None,
|
||||
saved: None,
|
||||
nsfw: false,
|
||||
embed_title: None,
|
||||
embed_description: None,
|
||||
embed_html: None,
|
||||
thumbnail_url: None,
|
||||
};
|
||||
|
||||
let read_post_listings_with_user = PostQueryBuilder::create(&conn)
|
||||
|
|
|
@ -132,6 +132,10 @@ mod tests {
|
|||
stickied: None,
|
||||
updated: None,
|
||||
nsfw: false,
|
||||
embed_title: None,
|
||||
embed_description: None,
|
||||
embed_html: None,
|
||||
thumbnail_url: None,
|
||||
};
|
||||
|
||||
let inserted_post = Post::create(&conn, &new_post).unwrap();
|
||||
|
|
|
@ -15,6 +15,7 @@ pub extern crate dotenv;
|
|||
pub extern crate jsonwebtoken;
|
||||
pub extern crate lettre;
|
||||
pub extern crate lettre_email;
|
||||
extern crate log;
|
||||
pub extern crate rand;
|
||||
pub extern crate regex;
|
||||
pub extern crate serde;
|
||||
|
@ -32,19 +33,19 @@ pub mod version;
|
|||
pub mod websocket;
|
||||
|
||||
use crate::settings::Settings;
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use chrono::{DateTime, FixedOffset, Local, NaiveDateTime};
|
||||
use chttp::prelude::*;
|
||||
use lettre::smtp::authentication::{Credentials, Mechanism};
|
||||
use lettre::smtp::extension::ClientId;
|
||||
use lettre::smtp::ConnectionReuseParameters;
|
||||
use lettre::{ClientSecurity, SmtpClient, Transport};
|
||||
use lettre_email::Email;
|
||||
use log::error;
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
use rand::distributions::Alphanumeric;
|
||||
use rand::{thread_rng, Rng};
|
||||
use regex::{Regex, RegexBuilder};
|
||||
|
||||
pub fn to_datetime_utc(ndt: NaiveDateTime) -> DateTime<Utc> {
|
||||
DateTime::<Utc>::from_utc(ndt, Utc)
|
||||
}
|
||||
use serde::Deserialize;
|
||||
|
||||
pub fn naive_now() -> NaiveDateTime {
|
||||
chrono::prelude::Utc::now().naive_utc()
|
||||
|
@ -54,6 +55,11 @@ pub fn naive_from_unix(time: i64) -> NaiveDateTime {
|
|||
NaiveDateTime::from_timestamp(time, 0)
|
||||
}
|
||||
|
||||
pub fn convert_datetime(datetime: NaiveDateTime) -> DateTime<FixedOffset> {
|
||||
let now = Local::now();
|
||||
DateTime::<FixedOffset>::from_utc(datetime, *now.offset())
|
||||
}
|
||||
|
||||
pub fn is_email_regex(test: &str) -> bool {
|
||||
EMAIL_REGEX.is_match(test)
|
||||
}
|
||||
|
@ -143,6 +149,77 @@ pub fn send_email(
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct IframelyResponse {
|
||||
title: Option<String>,
|
||||
description: Option<String>,
|
||||
thumbnail_url: Option<String>,
|
||||
html: Option<String>,
|
||||
}
|
||||
|
||||
pub fn fetch_iframely(url: &str) -> Result<IframelyResponse, failure::Error> {
|
||||
let fetch_url = format!("http://iframely/oembed?url={}", url);
|
||||
let text = chttp::get(&fetch_url)?.text()?;
|
||||
let res: IframelyResponse = serde_json::from_str(&text)?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct PictshareResponse {
|
||||
status: String,
|
||||
url: String,
|
||||
}
|
||||
|
||||
pub fn fetch_pictshare(image_url: &str) -> Result<PictshareResponse, failure::Error> {
|
||||
let fetch_url = format!(
|
||||
"http://pictshare/api/geturl.php?url={}",
|
||||
utf8_percent_encode(image_url, NON_ALPHANUMERIC)
|
||||
);
|
||||
let text = chttp::get(&fetch_url)?.text()?;
|
||||
let res: PictshareResponse = serde_json::from_str(&text)?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn fetch_iframely_and_pictshare_data(
|
||||
url: Option<String>,
|
||||
) -> (
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
) {
|
||||
// Fetch iframely data
|
||||
let (iframely_title, iframely_description, iframely_thumbnail_url, iframely_html) = match url {
|
||||
Some(url) => match fetch_iframely(&url) {
|
||||
Ok(res) => (res.title, res.description, res.thumbnail_url, res.html),
|
||||
Err(e) => {
|
||||
error!("iframely err: {}", e);
|
||||
(None, None, None, None)
|
||||
}
|
||||
},
|
||||
None => (None, None, None, None),
|
||||
};
|
||||
|
||||
// Fetch pictshare thumbnail
|
||||
let pictshare_thumbnail = match iframely_thumbnail_url {
|
||||
Some(iframely_thumbnail_url) => match fetch_pictshare(&iframely_thumbnail_url) {
|
||||
Ok(res) => Some(res.url),
|
||||
Err(e) => {
|
||||
error!("pictshare err: {}", e);
|
||||
None
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
|
||||
(
|
||||
iframely_title,
|
||||
iframely_description,
|
||||
iframely_html,
|
||||
pictshare_thumbnail,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{extract_usernames, is_email_regex, remove_slurs, slur_check, slurs_vec_to_str};
|
||||
|
@ -188,6 +265,21 @@ mod tests {
|
|||
assert_eq!(usernames, expected);
|
||||
}
|
||||
|
||||
// These helped with testing
|
||||
// #[test]
|
||||
// fn test_iframely() {
|
||||
// let res = fetch_iframely("https://www.redspark.nu/?p=15341");
|
||||
// assert!(res.is_ok());
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_pictshare() {
|
||||
// let res = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpg");
|
||||
// assert!(res.is_ok());
|
||||
// let res_other = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpgaoeu");
|
||||
// assert!(res_other.is_err());
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_send_email() {
|
||||
// let result = send_email("not a subject", "test_email@gmail.com", "ur user", "<h1>HI there</h1>");
|
||||
|
|
|
@ -9,7 +9,7 @@ use diesel::r2d2::{ConnectionManager, Pool};
|
|||
use diesel::PgConnection;
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
if Settings::get().federation_enabled {
|
||||
if Settings::get().federation.enabled {
|
||||
println!("federation enabled, host is {}", Settings::get().hostname);
|
||||
cfg
|
||||
.route(
|
||||
|
@ -20,11 +20,19 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
|||
"/federation/c/{community_name}/followers",
|
||||
web::get().to(apub::community::get_apub_community_followers),
|
||||
)
|
||||
.route(
|
||||
"/federation/c/{community_name}/outbox",
|
||||
web::get().to(apub::community::get_apub_community_outbox),
|
||||
)
|
||||
.route(
|
||||
"/federation/u/{user_name}",
|
||||
web::get().to(apub::user::get_apub_user),
|
||||
)
|
||||
// TODO: this is a very quick and dirty implementation for http api calls
|
||||
.route(
|
||||
"/federation/p/{post_id}",
|
||||
web::get().to(apub::user::get_apub_user),
|
||||
)
|
||||
// TODO: we should be able to remove this but somehow that breaks the remote community list
|
||||
.route(
|
||||
"/api/v1/communities/list",
|
||||
web::get().to(
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
extern crate lazy_static;
|
||||
use crate::apub::get_apub_protocol_string;
|
||||
use crate::db::site_view::SiteView;
|
||||
use crate::version;
|
||||
use crate::Settings;
|
||||
|
@ -7,7 +8,8 @@ use actix_web::web;
|
|||
use actix_web::HttpResponse;
|
||||
use diesel::r2d2::{ConnectionManager, Pool};
|
||||
use diesel::PgConnection;
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Debug;
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg
|
||||
|
@ -19,7 +21,11 @@ async fn node_info_well_known() -> HttpResponse<Body> {
|
|||
let node_info = NodeInfoWellKnown {
|
||||
links: NodeInfoWellKnownLinks {
|
||||
rel: "http://nodeinfo.diaspora.software/ns/schema/2.0".to_string(),
|
||||
href: format!("https://{}/nodeinfo/2.0.json", Settings::get().hostname),
|
||||
href: format!(
|
||||
"{}://{}/nodeinfo/2.0.json",
|
||||
get_apub_protocol_string(),
|
||||
Settings::get().hostname
|
||||
),
|
||||
},
|
||||
};
|
||||
HttpResponse::Ok().json(node_info)
|
||||
|
@ -34,7 +40,7 @@ async fn node_info(
|
|||
Ok(site_view) => site_view,
|
||||
Err(_) => return Err(format_err!("not_found")),
|
||||
};
|
||||
let protocols = if Settings::get().federation_enabled {
|
||||
let protocols = if Settings::get().federation.enabled {
|
||||
vec!["activitypub".to_string()]
|
||||
} else {
|
||||
vec![]
|
||||
|
@ -62,41 +68,41 @@ async fn node_info(
|
|||
Ok(res)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct NodeInfoWellKnown {
|
||||
links: NodeInfoWellKnownLinks,
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct NodeInfoWellKnown {
|
||||
pub links: NodeInfoWellKnownLinks,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct NodeInfoWellKnownLinks {
|
||||
rel: String,
|
||||
href: String,
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct NodeInfoWellKnownLinks {
|
||||
pub rel: String,
|
||||
pub href: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct NodeInfo {
|
||||
version: String,
|
||||
software: NodeInfoSoftware,
|
||||
protocols: Vec<String>,
|
||||
usage: NodeInfoUsage,
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct NodeInfo {
|
||||
pub version: String,
|
||||
pub software: NodeInfoSoftware,
|
||||
pub protocols: Vec<String>,
|
||||
pub usage: NodeInfoUsage,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct NodeInfoSoftware {
|
||||
name: String,
|
||||
version: String,
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct NodeInfoSoftware {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct NodeInfoUsage {
|
||||
users: NodeInfoUsers,
|
||||
local_posts: i64,
|
||||
local_comments: i64,
|
||||
open_registrations: bool,
|
||||
pub struct NodeInfoUsage {
|
||||
pub users: NodeInfoUsers,
|
||||
pub local_posts: i64,
|
||||
pub local_comments: i64,
|
||||
pub open_registrations: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct NodeInfoUsers {
|
||||
total: i64,
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct NodeInfoUsers {
|
||||
pub total: i64,
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ pub struct Params {
|
|||
}
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
if Settings::get().federation_enabled {
|
||||
if Settings::get().federation.enabled {
|
||||
cfg.route(
|
||||
".well-known/webfinger",
|
||||
web::get().to(get_webfinger_response),
|
||||
|
@ -46,13 +46,9 @@ async fn get_webfinger_response(
|
|||
|
||||
let regex_parsed = WEBFINGER_COMMUNITY_REGEX
|
||||
.captures(&info.resource)
|
||||
.map(|c| c.get(1));
|
||||
// TODO: replace this with .flatten() once we are running rust 1.40
|
||||
let regex_parsed_flattened = match regex_parsed {
|
||||
Some(s) => s,
|
||||
None => None,
|
||||
};
|
||||
let community_name = match regex_parsed_flattened {
|
||||
.map(|c| c.get(1))
|
||||
.flatten();
|
||||
let community_name = match regex_parsed {
|
||||
Some(c) => c.as_str(),
|
||||
None => return Err(format_err!("not_found")),
|
||||
};
|
||||
|
@ -79,7 +75,6 @@ async fn get_webfinger_response(
|
|||
{
|
||||
"rel": "self",
|
||||
"type": "application/activity+json",
|
||||
// Yes this is correct, this link doesn't include the `.json` extension
|
||||
"href": community_url
|
||||
}
|
||||
// TODO: this also needs to return the subscribe link once that's implemented
|
||||
|
|
|
@ -3,6 +3,7 @@ use actix::prelude::*;
|
|||
use actix_web::web;
|
||||
use actix_web::*;
|
||||
use actix_web_actors::ws;
|
||||
use log::{error, info};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
|
@ -99,7 +100,6 @@ impl Handler<WSMessage> for WSSession {
|
|||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: WSMessage, ctx: &mut Self::Context) {
|
||||
// println!("id: {} msg: {}", self.id, msg.0);
|
||||
ctx.text(msg.0);
|
||||
}
|
||||
}
|
||||
|
@ -107,11 +107,10 @@ impl Handler<WSMessage> for WSSession {
|
|||
/// WebSocket message handler
|
||||
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WSSession {
|
||||
fn handle(&mut self, result: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
|
||||
// println!("WEBSOCKET MESSAGE: {:?} from id: {}", msg, self.id);
|
||||
let message = match result {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
println!("{}", e);
|
||||
error!("{}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
@ -125,7 +124,7 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WSSession {
|
|||
}
|
||||
ws::Message::Text(text) => {
|
||||
let m = text.trim().to_owned();
|
||||
println!("WEBSOCKET MESSAGE: {:?} from id: {}", &m, self.id);
|
||||
info!("Message received: {:?} from id: {}", &m, self.id);
|
||||
|
||||
self
|
||||
.cs_addr
|
||||
|
@ -138,14 +137,14 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WSSession {
|
|||
match res {
|
||||
Ok(res) => ctx.text(res),
|
||||
Err(e) => {
|
||||
eprintln!("{}", &e);
|
||||
error!("{}", &e);
|
||||
}
|
||||
}
|
||||
actix::fut::ready(())
|
||||
})
|
||||
.wait(ctx);
|
||||
}
|
||||
ws::Message::Binary(_bin) => println!("Unexpected binary"),
|
||||
ws::Message::Binary(_bin) => info!("Unexpected binary"),
|
||||
ws::Message::Close(_) => {
|
||||
ctx.stop();
|
||||
}
|
||||
|
@ -163,7 +162,7 @@ impl WSSession {
|
|||
// check client heartbeats
|
||||
if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT {
|
||||
// heartbeat timed out
|
||||
println!("Websocket Client heartbeat failed, disconnecting!");
|
||||
error!("Websocket Client heartbeat failed, disconnecting!");
|
||||
|
||||
// notify chat server
|
||||
act.cs_addr.do_send(Disconnect {
|
||||
|
|
|
@ -207,6 +207,10 @@ table! {
|
|||
deleted -> Bool,
|
||||
nsfw -> Bool,
|
||||
stickied -> Bool,
|
||||
embed_title -> Nullable<Text>,
|
||||
embed_description -> Nullable<Text>,
|
||||
embed_html -> Nullable<Text>,
|
||||
thumbnail_url -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,8 +17,7 @@ pub struct Settings {
|
|||
pub front_end_dir: String,
|
||||
pub rate_limit: RateLimitConfig,
|
||||
pub email: Option<EmailConfig>,
|
||||
pub federation_enabled: bool,
|
||||
pub federated_instance: Option<String>,
|
||||
pub federation: Federation,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
@ -50,6 +49,13 @@ pub struct Database {
|
|||
pub pool_size: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Federation {
|
||||
pub enabled: bool,
|
||||
pub followed_instances: String,
|
||||
pub tls_enabled: bool,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref SETTINGS: Settings = {
|
||||
match Settings::init() {
|
||||
|
|
|
@ -1 +1 @@
|
|||
pub const VERSION: &str = "v0.6.17";
|
||||
pub const VERSION: &str = "v0.6.39";
|
||||
|
|
|
@ -6,6 +6,7 @@ use actix::prelude::*;
|
|||
use diesel::r2d2::{ConnectionManager, Pool, PooledConnection};
|
||||
use diesel::PgConnection;
|
||||
use failure::Error;
|
||||
use log::{error, info, warn};
|
||||
use rand::{rngs::ThreadRng, Rng};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
@ -20,7 +21,6 @@ use crate::api::post::*;
|
|||
use crate::api::site::*;
|
||||
use crate::api::user::*;
|
||||
use crate::api::*;
|
||||
use crate::apub::puller::*;
|
||||
use crate::websocket::UserOperation;
|
||||
use crate::Settings;
|
||||
|
||||
|
@ -344,7 +344,7 @@ impl ChatServer {
|
|||
}
|
||||
|
||||
if rate_limit.allowance < 1.0 {
|
||||
println!(
|
||||
warn!(
|
||||
"Rate limited IP: {}, time_passed: {}, allowance: {}",
|
||||
&info.ip, time_passed, rate_limit.allowance
|
||||
);
|
||||
|
@ -388,7 +388,7 @@ impl Handler<Connect> for ChatServer {
|
|||
fn handle(&mut self, msg: Connect, _ctx: &mut Context<Self>) -> Self::Result {
|
||||
// register session with random id
|
||||
let id = self.rng.gen::<usize>();
|
||||
println!("{} joined", &msg.ip);
|
||||
info!("{} joined", &msg.ip);
|
||||
|
||||
self.sessions.insert(
|
||||
id,
|
||||
|
@ -449,13 +449,16 @@ impl Handler<StandardMessage> for ChatServer {
|
|||
type Result = MessageResult<StandardMessage>;
|
||||
|
||||
fn handle(&mut self, msg: StandardMessage, _: &mut Context<Self>) -> Self::Result {
|
||||
let msg_out = match parse_json_message(self, msg) {
|
||||
Ok(m) => m,
|
||||
Err(e) => e.to_string(),
|
||||
};
|
||||
|
||||
println!("Message Sent: {}", msg_out);
|
||||
MessageResult(msg_out)
|
||||
match parse_json_message(self, msg) {
|
||||
Ok(m) => {
|
||||
info!("Message Sent: {}", m);
|
||||
MessageResult(m)
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error during message handling {}", e);
|
||||
MessageResult(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -502,9 +505,6 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
|||
|
||||
let user_operation: UserOperation = UserOperation::from_str(&op)?;
|
||||
|
||||
// TODO: none of the chat messages are going to work if stuff is submitted via http api,
|
||||
// need to move that handling elsewhere
|
||||
|
||||
// A DDOS check
|
||||
chat.check_rate_limit_message(msg.id, false)?;
|
||||
|
||||
|
@ -552,22 +552,7 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
|||
UserOperation::GetCommunity => {
|
||||
let get_community: GetCommunity = serde_json::from_str(data)?;
|
||||
|
||||
let mut res = if Settings::get().federation_enabled {
|
||||
if let Some(community_name) = get_community.name.to_owned() {
|
||||
if community_name.contains('@') {
|
||||
// TODO: need to support sort, filter etc for remote communities
|
||||
get_remote_community(community_name)?
|
||||
// TODO what is this about
|
||||
// get_community.name = Some(name.replace("!", ""));
|
||||
} else {
|
||||
Oper::new(get_community).perform(&conn)?
|
||||
}
|
||||
} else {
|
||||
Oper::new(get_community).perform(&conn)?
|
||||
}
|
||||
} else {
|
||||
Oper::new(get_community).perform(&conn)?
|
||||
};
|
||||
let mut res = Oper::new(get_community).perform(&conn)?;
|
||||
|
||||
let community_id = res.community.id;
|
||||
|
||||
|
@ -582,14 +567,8 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
|||
to_json_string(&user_operation, &res)
|
||||
}
|
||||
UserOperation::ListCommunities => {
|
||||
if Settings::get().federation_enabled {
|
||||
let res = get_all_communities()?;
|
||||
let val = ListCommunitiesResponse { communities: res };
|
||||
to_json_string(&user_operation, &val)
|
||||
} else {
|
||||
do_user_operation::<ListCommunities, ListCommunitiesResponse>(user_operation, data, &conn)
|
||||
}
|
||||
}
|
||||
UserOperation::CreateCommunity => {
|
||||
chat.check_rate_limit_register(msg.id, true)?;
|
||||
let create_community: CreateCommunity = serde_json::from_str(data)?;
|
||||
|
@ -649,6 +628,7 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
|||
}
|
||||
UserOperation::GetPosts => {
|
||||
let get_posts: GetPosts = serde_json::from_str(data)?;
|
||||
|
||||
if get_posts.community_id.is_none() {
|
||||
// 0 is the "all" community
|
||||
chat.join_community_room(0, msg.id);
|
||||
|
|
3
ui/.gitignore
vendored
3
ui/.gitignore
vendored
|
@ -6,14 +6,11 @@ _site
|
|||
.git
|
||||
build
|
||||
.build
|
||||
.git
|
||||
.history
|
||||
.idea
|
||||
.jshintrc
|
||||
.nyc_output
|
||||
.sass-cache
|
||||
.vscode
|
||||
build
|
||||
coverage
|
||||
jsconfig.json
|
||||
Gemfile.lock
|
||||
|
|
85
ui/assets/css/main.css
vendored
85
ui/assets/css/main.css
vendored
|
@ -74,10 +74,6 @@
|
|||
border-top: 2px solid var(--dark);
|
||||
}
|
||||
|
||||
.comment-node {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.vote-bar {
|
||||
margin-top: -6.5px;
|
||||
}
|
||||
|
@ -95,8 +91,17 @@
|
|||
fill: currentColor;
|
||||
vertical-align: middle;
|
||||
align-self: center;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.icon-inline {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation: spins 2s linear infinite;
|
||||
|
@ -112,7 +117,7 @@
|
|||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 3px solid #ccc;
|
||||
border-left: 2px solid var(--secondary);
|
||||
margin: 0.5em 5px;
|
||||
padding: 0.1em 5px;
|
||||
}
|
||||
|
@ -131,8 +136,13 @@ blockquote {
|
|||
}
|
||||
|
||||
.thumbnail {
|
||||
max-height: 62px;
|
||||
max-width: 400px;
|
||||
object-fit: cover;
|
||||
max-height: 80px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
svg.thumbnail {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.no-s-hows {
|
||||
|
@ -181,3 +191,64 @@ hr {
|
|||
-webkit-transform: scale(1.2);
|
||||
-ms-transform: scale(1.2);
|
||||
}
|
||||
|
||||
.selectr-selected, .selectr-options-container {
|
||||
background-color: var(--secondary);
|
||||
color: var(--white);
|
||||
border: unset;
|
||||
}
|
||||
|
||||
.mini-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 2px;
|
||||
height: 1.5em;
|
||||
width: 1.5em;
|
||||
background: rgba(0,0,0,.4);
|
||||
border-bottom-left-radius: 0.25rem !important;
|
||||
border-top-right-radius: 0.25rem !important;
|
||||
}
|
||||
|
||||
.link-overlay:hover {
|
||||
transition: .1s;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.link-overlay {
|
||||
transition: opacity .1s ease-in-out;
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background: rgba(0,0,0,.6);
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.unselectable {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.list-inline-item-action {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.list-inline-item-action:not(:last-child) {
|
||||
margin-right: 1.2rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-break: keep-all;
|
||||
}
|
||||
|
|
7
ui/assets/css/selectr.min.css
vendored
Normal file
7
ui/assets/css/selectr.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
ui/assets/css/tippy.css
vendored
Normal file
1
ui/assets/css/tippy.css
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}.tippy-iOS{cursor:pointer!important;-webkit-tap-highlight-color:transparent}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{position:relative;background-color:#333;color:#fff;border-radius:4px;font-size:14px;line-height:1.4;outline:0;transition-property:transform,visibility,opacity}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{border-width:8px 8px 0;border-top-color:#333;bottom:-7px;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-7px;border-width:0 8px 8px;border-bottom-color:#333;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:#333;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-7px;border-width:8px 8px 8px 0;border-right-color:#333;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{width:16px;height:16px}.tippy-arrow:before{content:"";position:absolute;border-color:transparent;border-style:solid}.tippy-content{position:relative;padding:5px 9px;z-index:1}
|
25
ui/generate_translations.js
vendored
Normal file
25
ui/generate_translations.js
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
fs = require('fs');
|
||||
|
||||
fs.mkdirSync('src/translations/', { recursive: true });
|
||||
fs.readdir('translations', (err, files) => {
|
||||
files.forEach(filename => {
|
||||
const lang = filename.split('.')[0];
|
||||
try {
|
||||
const json = JSON.parse(
|
||||
fs.readFileSync('translations/' + filename, 'utf8')
|
||||
);
|
||||
var data = `export const ${lang} = {\n translation: {`;
|
||||
for (var key in json) {
|
||||
if (key in json) {
|
||||
const value = json[key].replace(/"/g, '\\"');
|
||||
data = `${data}\n ${key}: "${value}",`;
|
||||
}
|
||||
}
|
||||
data += '\n },\n};';
|
||||
const target = 'src/translations/' + lang + '.ts';
|
||||
fs.writeFileSync(target, data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
});
|
18
ui/package.json
vendored
18
ui/package.json
vendored
|
@ -1,13 +1,15 @@
|
|||
{
|
||||
"name": "lemmy",
|
||||
"description": "A simple UI for lemmy",
|
||||
"description": "The official Lemmy UI",
|
||||
"version": "1.0.0",
|
||||
"author": "Dessalines",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "node fuse prod",
|
||||
"lint": "tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src",
|
||||
"prebuild": "node generate_translations.js",
|
||||
"prestart": "node generate_translations.js",
|
||||
"start": "node fuse dev"
|
||||
},
|
||||
"keywords": [],
|
||||
|
@ -25,21 +27,23 @@
|
|||
"emoji-short-name": "^1.0.0",
|
||||
"husky": "^4.2.1",
|
||||
"i18next": "^19.0.3",
|
||||
"inferno": "^7.0.1",
|
||||
"inferno": "^7.4.2",
|
||||
"inferno-i18next": "nimbusec-oss/inferno-i18next",
|
||||
"inferno-router": "^7.0.1",
|
||||
"inferno-router": "^7.4.2",
|
||||
"js-cookie": "^2.2.0",
|
||||
"jwt-decode": "^2.2.0",
|
||||
"markdown-it": "^10.0.0",
|
||||
"markdown-it-container": "^2.0.0",
|
||||
"markdown-it-emoji": "^1.4.0",
|
||||
"mobius1-selectr": "^2.4.13",
|
||||
"moment": "^2.24.0",
|
||||
"prettier": "^1.18.2",
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
"rxjs": "^6.4.0",
|
||||
"terser": "^4.6.3",
|
||||
"tippy.js": "^6.0.0",
|
||||
"toastify-js": "^1.6.2",
|
||||
"tributejs": "^4.1.1",
|
||||
"tributejs": "^5.0.0",
|
||||
"twemoji": "^12.1.2",
|
||||
"ws": "^7.0.0"
|
||||
},
|
||||
|
@ -53,7 +57,7 @@
|
|||
"ts-node": "^8.6.2",
|
||||
"ts-transform-classcat": "^0.0.2",
|
||||
"ts-transform-inferno": "^4.0.2",
|
||||
"typescript": "^3.7.5"
|
||||
"typescript": "^3.8.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.9.0"
|
||||
|
@ -61,7 +65,7 @@
|
|||
"engineStrict": true,
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "yarn run ts-node translation_report.ts && git add ../README.md && cargo clippy --manifest-path ../server/Cargo.toml --all-targets --all-features -- -D warnings && lint-staged"
|
||||
"pre-commit": "cargo clippy --manifest-path ../server/Cargo.toml --all-targets --all-features -- -D warnings && lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
|
|
100
ui/src/components/comment-form.tsx
vendored
100
ui/src/components/comment-form.tsx
vendored
|
@ -1,7 +1,13 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||
import { Prompt } from 'inferno-router';
|
||||
import {
|
||||
CommentNode as CommentNodeI,
|
||||
CommentForm as CommentFormI,
|
||||
WebSocketJsonResponse,
|
||||
UserOperation,
|
||||
CommentResponse,
|
||||
} from '../interfaces';
|
||||
import {
|
||||
capitalizeFirstLetter,
|
||||
|
@ -10,6 +16,7 @@ import {
|
|||
markdownHelpUrl,
|
||||
toast,
|
||||
setupTribute,
|
||||
wsJsonToRes,
|
||||
} from '../utils';
|
||||
import { WebSocketService, UserService } from '../services';
|
||||
import autosize from 'autosize';
|
||||
|
@ -28,12 +35,15 @@ interface CommentFormState {
|
|||
commentForm: CommentFormI;
|
||||
buttonTitle: string;
|
||||
previewMode: boolean;
|
||||
loading: boolean;
|
||||
imageLoading: boolean;
|
||||
}
|
||||
|
||||
export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||
private id = `comment-form-${randomStr()}`;
|
||||
private id = `comment-textarea-${randomStr()}`;
|
||||
private formId = `comment-form-${randomStr()}`;
|
||||
private tribute: Tribute;
|
||||
private subscription: Subscription;
|
||||
private emptyState: CommentFormState = {
|
||||
commentForm: {
|
||||
auth: null,
|
||||
|
@ -51,6 +61,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||
? capitalizeFirstLetter(i18n.t('edit'))
|
||||
: capitalizeFirstLetter(i18n.t('reply')),
|
||||
previewMode: false,
|
||||
loading: false,
|
||||
imageLoading: false,
|
||||
};
|
||||
|
||||
|
@ -71,6 +82,14 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||
this.state.commentForm.parent_id = this.props.node.comment.id;
|
||||
}
|
||||
}
|
||||
|
||||
this.subscription = WebSocketService.Instance.subject
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.subscribe(
|
||||
msg => this.parseMessage(msg),
|
||||
err => console.error(err),
|
||||
() => console.log('complete')
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -84,10 +103,21 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class="mb-3">
|
||||
<form onSubmit={linkEvent(this, this.handleCommentSubmit)}>
|
||||
<Prompt
|
||||
when={this.state.commentForm.content}
|
||||
message={i18n.t('block_leaving')}
|
||||
/>
|
||||
<form
|
||||
id={this.formId}
|
||||
onSubmit={linkEvent(this, this.handleCommentSubmit)}
|
||||
>
|
||||
<div class="form-group row">
|
||||
<div className={`col-sm-12`}>
|
||||
<textarea
|
||||
|
@ -118,7 +148,13 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||
class="btn btn-sm btn-secondary mr-2"
|
||||
disabled={this.props.disabled}
|
||||
>
|
||||
{this.state.buttonTitle}
|
||||
{this.state.loading ? (
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
</svg>
|
||||
) : (
|
||||
<span>{this.state.buttonTitle}</span>
|
||||
)}
|
||||
</button>
|
||||
{this.state.commentForm.content && (
|
||||
<button
|
||||
|
@ -141,16 +177,22 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||
<a
|
||||
href={markdownHelpUrl}
|
||||
target="_blank"
|
||||
class="d-inline-block float-right text-muted small font-weight-bold"
|
||||
class="d-inline-block float-right text-muted font-weight-bold"
|
||||
title={i18n.t('formatting_help')}
|
||||
>
|
||||
{i18n.t('formatting_help')}
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-help-circle"></use>
|
||||
</svg>
|
||||
</a>
|
||||
<form class="d-inline-block mr-2 float-right text-muted small font-weight-bold">
|
||||
<form class="d-inline-block mr-3 float-right text-muted font-weight-bold">
|
||||
<label
|
||||
htmlFor={`file-upload-${this.id}`}
|
||||
className={`${UserService.Instance.user && 'pointer'}`}
|
||||
data-tippy-content={i18n.t('upload_image')}
|
||||
>
|
||||
{i18n.t('upload_image')}
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-image"></use>
|
||||
</svg>
|
||||
</label>
|
||||
<input
|
||||
id={`file-upload-${this.id}`}
|
||||
|
@ -174,6 +216,20 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||
);
|
||||
}
|
||||
|
||||
handleFinished() {
|
||||
this.state.previewMode = false;
|
||||
this.state.loading = false;
|
||||
this.state.commentForm.content = '';
|
||||
this.setState(this.state);
|
||||
let form: any = document.getElementById(this.formId);
|
||||
form.reset();
|
||||
if (this.props.node) {
|
||||
this.props.onReplyCancel();
|
||||
}
|
||||
autosize.update(document.querySelector('textarea'));
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handleCommentSubmit(i: CommentForm, event: any) {
|
||||
event.preventDefault();
|
||||
if (i.props.edit) {
|
||||
|
@ -182,15 +238,8 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||
WebSocketService.Instance.createComment(i.state.commentForm);
|
||||
}
|
||||
|
||||
i.state.previewMode = false;
|
||||
i.state.commentForm.content = undefined;
|
||||
event.target.reset();
|
||||
i.state.loading = true;
|
||||
i.setState(i.state);
|
||||
if (i.props.node) {
|
||||
i.props.onReplyCancel();
|
||||
}
|
||||
|
||||
autosize.update(document.querySelector('textarea'));
|
||||
}
|
||||
|
||||
handleCommentContentChange(i: CommentForm, event: any) {
|
||||
|
@ -245,7 +294,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||
i.state.commentForm.content = content;
|
||||
i.state.imageLoading = false;
|
||||
i.setState(i.state);
|
||||
var textarea: any = document.getElementById(i.id);
|
||||
let textarea: any = document.getElementById(i.id);
|
||||
autosize.update(textarea);
|
||||
})
|
||||
.catch(error => {
|
||||
|
@ -254,4 +303,23 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||
toast(error, 'danger');
|
||||
});
|
||||
}
|
||||
|
||||
parseMessage(msg: WebSocketJsonResponse) {
|
||||
let res = wsJsonToRes(msg);
|
||||
|
||||
// Only do the showing and hiding if logged in
|
||||
if (UserService.Instance.user) {
|
||||
if (res.op == UserOperation.CreateComment) {
|
||||
let data = res.data as CommentResponse;
|
||||
if (data.comment.creator_id == UserService.Instance.user.id) {
|
||||
this.handleFinished();
|
||||
}
|
||||
} else if (res.op == UserOperation.EditComment) {
|
||||
let data = res.data as CommentResponse;
|
||||
if (data.comment.creator_id == UserService.Instance.user.id) {
|
||||
this.handleFinished();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
372
ui/src/components/comment-node.tsx
vendored
372
ui/src/components/comment-node.tsx
vendored
|
@ -26,6 +26,8 @@ import {
|
|||
isMod,
|
||||
pictshareAvatarThumbnail,
|
||||
showAvatars,
|
||||
setupTippy,
|
||||
colorList,
|
||||
} from '../utils';
|
||||
import moment from 'moment';
|
||||
import { MomentTime } from './moment-time';
|
||||
|
@ -48,10 +50,12 @@ interface CommentNodeState {
|
|||
showConfirmAppointAsAdmin: boolean;
|
||||
collapsed: boolean;
|
||||
viewSource: boolean;
|
||||
showAdvanced: boolean;
|
||||
my_vote: number;
|
||||
score: number;
|
||||
upvotes: number;
|
||||
downvotes: number;
|
||||
borderColor: string;
|
||||
}
|
||||
|
||||
interface CommentNodeProps {
|
||||
|
@ -81,6 +85,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
banType: BanType.Community,
|
||||
collapsed: false,
|
||||
viewSource: false,
|
||||
showAdvanced: false,
|
||||
showConfirmTransferSite: false,
|
||||
showConfirmTransferCommunity: false,
|
||||
showConfirmAppointAsMod: false,
|
||||
|
@ -89,6 +94,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
score: this.props.node.comment.score,
|
||||
upvotes: this.props.node.comment.upvotes,
|
||||
downvotes: this.props.node.comment.downvotes,
|
||||
borderColor: this.props.node.comment.depth
|
||||
? colorList[this.props.node.comment.depth % colorList.length]
|
||||
: colorList[0],
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
|
@ -113,56 +121,44 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
return (
|
||||
<div
|
||||
className={`comment ${
|
||||
node.comment.parent_id && !this.props.noIndent ? 'ml-4' : ''
|
||||
node.comment.parent_id && !this.props.noIndent ? 'ml-2' : ''
|
||||
}`}
|
||||
>
|
||||
{!this.state.collapsed && (
|
||||
<div
|
||||
className={`vote-bar mr-2 float-left small text-center ${this.props
|
||||
.viewOnly && 'no-click'}`}
|
||||
>
|
||||
<button
|
||||
className={`vote-animate btn btn-link p-0 ${
|
||||
this.state.my_vote == 1 ? 'text-info' : 'text-muted'
|
||||
}`}
|
||||
onClick={linkEvent(node, this.handleCommentUpvote)}
|
||||
>
|
||||
<svg class="icon upvote">
|
||||
<use xlinkHref="#icon-arrow-up"></use>
|
||||
</svg>
|
||||
</button>
|
||||
<div class={`font-weight-bold text-muted`}>{this.state.score}</div>
|
||||
{WebSocketService.Instance.site.enable_downvotes && (
|
||||
<button
|
||||
className={`vote-animate btn btn-link p-0 ${
|
||||
this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
|
||||
}`}
|
||||
onClick={linkEvent(node, this.handleCommentDownvote)}
|
||||
>
|
||||
<svg class="icon downvote">
|
||||
<use xlinkHref="#icon-arrow-down"></use>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{!node.comment.parent_id && !this.props.noIndent && (
|
||||
<>
|
||||
<hr class="d-sm-none my-2" />
|
||||
<div class="d-none d-sm-block d-sm-none my-3" />
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
id={`comment-${node.comment.id}`}
|
||||
className={`details comment-node ml-4 ${
|
||||
className={`details comment-node mb-1 ${
|
||||
this.isCommentNew ? 'mark' : ''
|
||||
}`}
|
||||
style={
|
||||
!this.props.noIndent &&
|
||||
this.props.node.comment.parent_id &&
|
||||
`border-left: 2px ${this.state.borderColor} solid !important`
|
||||
}
|
||||
>
|
||||
<ul class="list-inline mb-0 text-muted small">
|
||||
<div
|
||||
class={`${!this.props.noIndent &&
|
||||
this.props.node.comment.parent_id &&
|
||||
'ml-2'}`}
|
||||
>
|
||||
<ul class="list-inline mb-1 text-muted small">
|
||||
<li className="list-inline-item">
|
||||
<Link
|
||||
className="text-info"
|
||||
className="text-body font-weight-bold"
|
||||
to={`/u/${node.comment.creator_name}`}
|
||||
>
|
||||
{node.comment.creator_avatar && showAvatars() && (
|
||||
<img
|
||||
height="32"
|
||||
width="32"
|
||||
src={pictshareAvatarThumbnail(node.comment.creator_avatar)}
|
||||
src={pictshareAvatarThumbnail(
|
||||
node.comment.creator_avatar
|
||||
)}
|
||||
class="rounded-circle mr-1"
|
||||
/>
|
||||
)}
|
||||
|
@ -189,14 +185,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
{i18n.t('banned')}
|
||||
</li>
|
||||
)}
|
||||
<li className="list-inline-item">
|
||||
<span>
|
||||
(<span className="text-info">+{this.state.upvotes}</span>
|
||||
<span> | </span>
|
||||
<span className="text-danger">-{this.state.downvotes}</span>
|
||||
<span>) </span>
|
||||
</span>
|
||||
</li>
|
||||
{this.props.showCommunity && (
|
||||
<li className="list-inline-item">
|
||||
<span> {i18n.t('to')} </span>
|
||||
|
@ -205,6 +193,20 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
</Link>
|
||||
</li>
|
||||
)}
|
||||
<li className="list-inline-item">•</li>
|
||||
<li className="list-inline-item">
|
||||
<span
|
||||
className={`unselectable pointer ${this.scoreColor}`}
|
||||
onClick={linkEvent(node, this.handleCommentUpvote)}
|
||||
data-tippy-content={this.pointsTippy}
|
||||
>
|
||||
<svg class="icon icon-inline mr-1">
|
||||
<use xlinkHref="#icon-zap"></use>
|
||||
</svg>
|
||||
{this.state.score}
|
||||
</span>
|
||||
</li>
|
||||
<li className="list-inline-item">•</li>
|
||||
<li className="list-inline-item">
|
||||
<span>
|
||||
<MomentTime data={node.comment} />
|
||||
|
@ -212,10 +214,18 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
</li>
|
||||
<li className="list-inline-item">
|
||||
<div
|
||||
className="pointer text-monospace"
|
||||
className="unselectable pointer text-monospace"
|
||||
onClick={linkEvent(this, this.handleCommentCollapse)}
|
||||
>
|
||||
{this.state.collapsed ? '[+]' : '[-]'}
|
||||
{this.state.collapsed ? (
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-plus-square"></use>
|
||||
</svg>
|
||||
) : (
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-minus-square"></use>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -234,94 +244,201 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
) : (
|
||||
<div
|
||||
className="md-div"
|
||||
dangerouslySetInnerHTML={mdToHtml(this.commentUnlessRemoved)}
|
||||
dangerouslySetInnerHTML={mdToHtml(
|
||||
this.commentUnlessRemoved
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
||||
<ul class="list-inline mb-0 text-muted font-weight-bold h5">
|
||||
{this.props.markable && (
|
||||
<li className="list-inline-item">
|
||||
<li className="list-inline-item-action">
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleMarkRead)}
|
||||
>
|
||||
{node.comment.read
|
||||
data-tippy-content={
|
||||
node.comment.read
|
||||
? i18n.t('mark_as_unread')
|
||||
: i18n.t('mark_as_read')}
|
||||
: i18n.t('mark_as_read')
|
||||
}
|
||||
>
|
||||
<svg
|
||||
class={`icon icon-inline ${node.comment.read &&
|
||||
'text-success'}`}
|
||||
>
|
||||
<use xlinkHref="#icon-check"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
{UserService.Instance.user && !this.props.viewOnly && (
|
||||
<>
|
||||
<li className="list-inline-item">
|
||||
<li className="list-inline-item-action">
|
||||
<button
|
||||
className={`vote-animate btn btn-link p-0 mb-1 ${
|
||||
this.state.my_vote == 1 ? 'text-info' : 'text-muted'
|
||||
}`}
|
||||
onClick={linkEvent(node, this.handleCommentUpvote)}
|
||||
data-tippy-content={i18n.t('upvote')}
|
||||
>
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-arrow-up"></use>
|
||||
</svg>
|
||||
{this.state.upvotes !== this.state.score && (
|
||||
<span class="ml-1">{this.state.upvotes}</span>
|
||||
)}
|
||||
</button>
|
||||
</li>
|
||||
{WebSocketService.Instance.site.enable_downvotes && (
|
||||
<li className="list-inline-item-action">
|
||||
<button
|
||||
className={`vote-animate btn btn-link p-0 mb-1 ${
|
||||
this.state.my_vote == -1
|
||||
? 'text-danger'
|
||||
: 'text-muted'
|
||||
}`}
|
||||
onClick={linkEvent(
|
||||
node,
|
||||
this.handleCommentDownvote
|
||||
)}
|
||||
data-tippy-content={i18n.t('downvote')}
|
||||
>
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-arrow-down"></use>
|
||||
</svg>
|
||||
{this.state.upvotes !== this.state.score && (
|
||||
<span class="ml-1">{this.state.downvotes}</span>
|
||||
)}
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
<li className="list-inline-item-action">
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleReplyClick)}
|
||||
data-tippy-content={i18n.t('reply')}
|
||||
>
|
||||
{i18n.t('reply')}
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-reply1"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
<li className="list-inline-item mr-2">
|
||||
<li className="list-inline-item-action">
|
||||
<Link
|
||||
className="text-muted"
|
||||
to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
|
||||
title={i18n.t('link')}
|
||||
>
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-link"></use>
|
||||
</svg>
|
||||
</Link>
|
||||
</li>
|
||||
{!this.state.showAdvanced ? (
|
||||
<li className="list-inline-item-action">
|
||||
<span
|
||||
className="unselectable pointer"
|
||||
onClick={linkEvent(this, this.handleShowAdvanced)}
|
||||
data-tippy-content={i18n.t('more')}
|
||||
>
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-more-vertical"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
) : (
|
||||
<>
|
||||
{!this.myComment && (
|
||||
<li className="list-inline-item-action">
|
||||
<Link
|
||||
class="text-muted"
|
||||
to={`/create_private_message?recipient_id=${node.comment.creator_id}`}
|
||||
title={i18n.t('message').toLowerCase()}
|
||||
>
|
||||
<svg class="icon">
|
||||
<use xlinkHref="#icon-mail"></use>
|
||||
</svg>
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
<li className="list-inline-item-action">
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleSaveCommentClick)}
|
||||
onClick={linkEvent(
|
||||
this,
|
||||
this.handleSaveCommentClick
|
||||
)}
|
||||
data-tippy-content={
|
||||
node.comment.saved
|
||||
? i18n.t('unsave')
|
||||
: i18n.t('save')
|
||||
}
|
||||
>
|
||||
{node.comment.saved ? i18n.t('unsave') : i18n.t('save')}
|
||||
<svg
|
||||
class={`icon icon-inline ${node.comment.saved &&
|
||||
'text-warning'}`}
|
||||
>
|
||||
<use xlinkHref="#icon-star"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
<li className="list-inline-item-action">
|
||||
<span
|
||||
className="pointer"
|
||||
onClick={linkEvent(this, this.handleViewSource)}
|
||||
data-tippy-content={i18n.t('view_source')}
|
||||
>
|
||||
<svg
|
||||
class={`icon icon-inline ${this.state
|
||||
.viewSource && 'text-success'}`}
|
||||
>
|
||||
<use xlinkHref="#icon-file-text"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
{this.myComment && (
|
||||
<>
|
||||
<li className="list-inline-item">
|
||||
<li className="list-inline-item-action">•</li>
|
||||
<li className="list-inline-item-action">
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleEditClick)}
|
||||
onClick={linkEvent(
|
||||
this,
|
||||
this.handleEditClick
|
||||
)}
|
||||
data-tippy-content={i18n.t('edit')}
|
||||
>
|
||||
{i18n.t('edit')}
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-edit"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<li className="list-inline-item-action">
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleDeleteClick)}
|
||||
>
|
||||
{!node.comment.deleted
|
||||
onClick={linkEvent(
|
||||
this,
|
||||
this.handleDeleteClick
|
||||
)}
|
||||
data-tippy-content={
|
||||
!node.comment.deleted
|
||||
? i18n.t('delete')
|
||||
: i18n.t('restore')}
|
||||
: i18n.t('restore')
|
||||
}
|
||||
>
|
||||
<svg
|
||||
class={`icon icon-inline ${node.comment
|
||||
.deleted && 'text-danger'}`}
|
||||
>
|
||||
<use xlinkHref="#icon-trash"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
{!this.myComment && (
|
||||
<li className="list-inline-item">
|
||||
<Link
|
||||
class="text-muted"
|
||||
to={`/create_private_message?recipient_id=${node.comment.creator_id}`}
|
||||
>
|
||||
{i18n.t('message').toLowerCase()}
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
<li className="list-inline-item">•</li>
|
||||
<li className="list-inline-item">
|
||||
<span
|
||||
className="pointer"
|
||||
onClick={linkEvent(this, this.handleViewSource)}
|
||||
>
|
||||
{i18n.t('view_source')}
|
||||
</span>
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<Link
|
||||
className="text-muted"
|
||||
to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
|
||||
>
|
||||
{i18n.t('link')}
|
||||
</Link>
|
||||
</li>
|
||||
{/* Admins and mods can remove comments */}
|
||||
{(this.canMod || this.canAdmin) && (
|
||||
<>
|
||||
<li className="list-inline-item">•</li>
|
||||
<li className="list-inline-item">
|
||||
<li className="list-inline-item-action">
|
||||
{!node.comment.removed ? (
|
||||
<span
|
||||
class="pointer"
|
||||
|
@ -350,7 +467,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
{this.canMod && (
|
||||
<>
|
||||
{!this.isMod && (
|
||||
<li className="list-inline-item">
|
||||
<li className="list-inline-item-action">
|
||||
{!node.comment.banned_from_community ? (
|
||||
<span
|
||||
class="pointer"
|
||||
|
@ -375,7 +492,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
</li>
|
||||
)}
|
||||
{!node.comment.banned_from_community && (
|
||||
<li className="list-inline-item">
|
||||
<li className="list-inline-item-action">
|
||||
{!this.state.showConfirmAppointAsMod ? (
|
||||
<span
|
||||
class="pointer"
|
||||
|
@ -418,8 +535,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
</>
|
||||
)}
|
||||
{/* Community creators and admins can transfer community to another mod */}
|
||||
{(this.amCommunityCreator || this.canAdmin) && this.isMod && (
|
||||
<li className="list-inline-item">
|
||||
{(this.amCommunityCreator || this.canAdmin) &&
|
||||
this.isMod && (
|
||||
<li className="list-inline-item-action">
|
||||
{!this.state.showConfirmTransferCommunity ? (
|
||||
<span
|
||||
class="pointer"
|
||||
|
@ -448,7 +566,8 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
class="pointer d-inline-block"
|
||||
onClick={linkEvent(
|
||||
this,
|
||||
this.handleCancelShowConfirmTransferCommunity
|
||||
this
|
||||
.handleCancelShowConfirmTransferCommunity
|
||||
)}
|
||||
>
|
||||
{i18n.t('no')}
|
||||
|
@ -461,11 +580,14 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
{this.canAdmin && (
|
||||
<>
|
||||
{!this.isAdmin && (
|
||||
<li className="list-inline-item">
|
||||
<li className="list-inline-item-action">
|
||||
{!node.comment.banned ? (
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleModBanShow)}
|
||||
onClick={linkEvent(
|
||||
this,
|
||||
this.handleModBanShow
|
||||
)}
|
||||
>
|
||||
{i18n.t('ban_from_site')}
|
||||
</span>
|
||||
|
@ -483,7 +605,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
</li>
|
||||
)}
|
||||
{!node.comment.banned && (
|
||||
<li className="list-inline-item">
|
||||
<li className="list-inline-item-action">
|
||||
{!this.state.showConfirmAppointAsAdmin ? (
|
||||
<span
|
||||
class="pointer"
|
||||
|
@ -503,7 +625,10 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
</span>
|
||||
<span
|
||||
class="pointer d-inline-block mr-1"
|
||||
onClick={linkEvent(this, this.handleAddAdmin)}
|
||||
onClick={linkEvent(
|
||||
this,
|
||||
this.handleAddAdmin
|
||||
)}
|
||||
>
|
||||
{i18n.t('yes')}
|
||||
</span>
|
||||
|
@ -524,7 +649,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
)}
|
||||
{/* Site Creator can transfer to another admin */}
|
||||
{this.amSiteCreator && this.isAdmin && (
|
||||
<li className="list-inline-item">
|
||||
<li className="list-inline-item-action">
|
||||
{!this.state.showConfirmTransferSite ? (
|
||||
<span
|
||||
class="pointer"
|
||||
|
@ -542,7 +667,10 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
</span>
|
||||
<span
|
||||
class="pointer d-inline-block mr-1"
|
||||
onClick={linkEvent(this, this.handleTransferSite)}
|
||||
onClick={linkEvent(
|
||||
this,
|
||||
this.handleTransferSite
|
||||
)}
|
||||
>
|
||||
{i18n.t('yes')}
|
||||
</span>
|
||||
|
@ -561,10 +689,14 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* end of details */}
|
||||
{this.state.showRemoveDialog && (
|
||||
<form
|
||||
class="form-inline"
|
||||
|
@ -782,6 +914,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
|
||||
WebSocketService.Instance.likeComment(form);
|
||||
this.setState(this.state);
|
||||
setupTippy();
|
||||
}
|
||||
|
||||
handleCommentDownvote(i: CommentNodeI) {
|
||||
|
@ -809,6 +942,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
|
||||
WebSocketService.Instance.likeComment(form);
|
||||
this.setState(this.state);
|
||||
setupTippy();
|
||||
}
|
||||
|
||||
handleModRemoveShow(i: CommentNode) {
|
||||
|
@ -1016,4 +1150,36 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
i.state.viewSource = !i.state.viewSource;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleShowAdvanced(i: CommentNode) {
|
||||
i.state.showAdvanced = !i.state.showAdvanced;
|
||||
i.setState(i.state);
|
||||
setupTippy();
|
||||
}
|
||||
|
||||
get scoreColor() {
|
||||
if (this.state.my_vote == 1) {
|
||||
return 'text-info';
|
||||
} else if (this.state.my_vote == -1) {
|
||||
return 'text-danger';
|
||||
} else {
|
||||
return 'text-muted';
|
||||
}
|
||||
}
|
||||
|
||||
get pointsTippy(): string {
|
||||
let points = i18n.t('number_of_points', {
|
||||
count: this.state.score,
|
||||
});
|
||||
|
||||
let upvotes = i18n.t('number_of_upvotes', {
|
||||
count: this.state.upvotes,
|
||||
});
|
||||
|
||||
let downvotes = i18n.t('number_of_downvotes', {
|
||||
count: this.state.downvotes,
|
||||
});
|
||||
|
||||
return `${points} • ${upvotes} • ${downvotes}`;
|
||||
}
|
||||
}
|
||||
|
|
1
ui/src/components/comment-nodes.tsx
vendored
1
ui/src/components/comment-nodes.tsx
vendored
|
@ -38,6 +38,7 @@ export class CommentNodes extends Component<
|
|||
<div className="comments">
|
||||
{this.sorter().map(node => (
|
||||
<CommentNode
|
||||
key={node.comment.id}
|
||||
node={node}
|
||||
noIndent={this.props.noIndent}
|
||||
viewOnly={this.props.viewOnly}
|
||||
|
|
12
ui/src/components/community-form.tsx
vendored
12
ui/src/components/community-form.tsx
vendored
|
@ -1,4 +1,5 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Prompt } from 'inferno-router';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||
import {
|
||||
|
@ -105,6 +106,16 @@ export class CommunityForm extends Component<
|
|||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<Prompt
|
||||
when={
|
||||
!this.state.loading &&
|
||||
(this.state.communityForm.name ||
|
||||
this.state.communityForm.title ||
|
||||
this.state.communityForm.description)
|
||||
}
|
||||
message={i18n.t('block_leaving')}
|
||||
/>
|
||||
<form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}>
|
||||
<div class="form-group row">
|
||||
<label class="col-12 col-form-label" htmlFor="community-name">
|
||||
|
@ -219,6 +230,7 @@ export class CommunityForm extends Component<
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
14
ui/src/components/community.tsx
vendored
14
ui/src/components/community.tsx
vendored
|
@ -43,6 +43,7 @@ import {
|
|||
createPostLikeFindRes,
|
||||
editPostFindRes,
|
||||
commentsToFlatNodes,
|
||||
setupTippy,
|
||||
} from '../utils';
|
||||
import { i18n } from '../i18next';
|
||||
|
||||
|
@ -136,6 +137,7 @@ export class Community extends Component<any, State> {
|
|||
render() {
|
||||
return (
|
||||
<div class="container">
|
||||
{this.selects()}
|
||||
{this.state.loading ? (
|
||||
<h5>
|
||||
<svg class="icon icon-spinner spin">
|
||||
|
@ -158,7 +160,6 @@ export class Community extends Component<any, State> {
|
|||
</small>
|
||||
)}
|
||||
</h5>
|
||||
{this.selects()}
|
||||
{this.listings()}
|
||||
{this.paginator()}
|
||||
</div>
|
||||
|
@ -194,13 +195,14 @@ export class Community extends Component<any, State> {
|
|||
|
||||
selects() {
|
||||
return (
|
||||
<div class="mb-2">
|
||||
<div class="mb-3">
|
||||
<span class="mr-3">
|
||||
<DataTypeSelect
|
||||
type_={this.state.dataType}
|
||||
onChange={this.handleDataTypeChange}
|
||||
/>
|
||||
|
||||
<span class="mx-2">
|
||||
</span>
|
||||
<span class="mr-2">
|
||||
<SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
|
||||
</span>
|
||||
<a
|
||||
|
@ -208,8 +210,9 @@ export class Community extends Component<any, State> {
|
|||
SortType[this.state.sort]
|
||||
}`}
|
||||
target="_blank"
|
||||
title="RSS"
|
||||
>
|
||||
<svg class="icon mx-2 text-muted small">
|
||||
<svg class="icon text-muted small">
|
||||
<use xlinkHref="#icon-rss">#</use>
|
||||
</svg>
|
||||
</a>
|
||||
|
@ -339,6 +342,7 @@ export class Community extends Component<any, State> {
|
|||
this.state.posts = data.posts;
|
||||
this.state.loading = false;
|
||||
this.setState(this.state);
|
||||
setupTippy();
|
||||
} else if (res.op == UserOperation.EditPost) {
|
||||
let data = res.data as PostResponse;
|
||||
editPostFindRes(data, this.state.posts);
|
||||
|
|
99
ui/src/components/iframely-card.tsx
vendored
Normal file
99
ui/src/components/iframely-card.tsx
vendored
Normal file
|
@ -0,0 +1,99 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Post } from '../interfaces';
|
||||
import { mdToHtml } from '../utils';
|
||||
import { i18n } from '../i18next';
|
||||
|
||||
interface FramelyCardProps {
|
||||
post: Post;
|
||||
}
|
||||
|
||||
interface FramelyCardState {
|
||||
expanded: boolean;
|
||||
}
|
||||
|
||||
export class IFramelyCard extends Component<
|
||||
FramelyCardProps,
|
||||
FramelyCardState
|
||||
> {
|
||||
private emptyState: FramelyCardState = {
|
||||
expanded: false,
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
this.state = this.emptyState;
|
||||
}
|
||||
|
||||
render() {
|
||||
let post = this.props.post;
|
||||
return (
|
||||
<>
|
||||
{post.embed_title && !this.state.expanded && (
|
||||
<div class="card mt-3 mb-2">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title d-inline">
|
||||
{post.embed_html ? (
|
||||
<span
|
||||
class="unselectable pointer"
|
||||
onClick={linkEvent(this, this.handleIframeExpand)}
|
||||
data-tippy-content={i18n.t('expand_here')}
|
||||
>
|
||||
{post.embed_title}
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
<a class="text-body" target="_blank" href={post.url}>
|
||||
{post.embed_title}
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
</h5>
|
||||
<span class="d-inline-block ml-2 mb-2 small text-muted">
|
||||
<a
|
||||
class="text-muted font-italic"
|
||||
target="_blank"
|
||||
href={post.url}
|
||||
>
|
||||
{new URL(post.url).hostname}
|
||||
<svg class="ml-1 icon">
|
||||
<use xlinkHref="#icon-external-link"></use>
|
||||
</svg>
|
||||
</a>
|
||||
{post.embed_html && (
|
||||
<span
|
||||
class="ml-2 pointer text-monospace"
|
||||
onClick={linkEvent(this, this.handleIframeExpand)}
|
||||
data-tippy-content={i18n.t('expand_here')}
|
||||
>
|
||||
{this.state.expanded ? '[-]' : '[+]'}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{post.embed_description && (
|
||||
<div
|
||||
className="card-text small text-muted md-div"
|
||||
dangerouslySetInnerHTML={mdToHtml(post.embed_description)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{this.state.expanded && (
|
||||
<div
|
||||
class="mt-3 mb-2"
|
||||
dangerouslySetInnerHTML={{ __html: post.embed_html }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
handleIframeExpand(i: IFramelyCard) {
|
||||
i.state.expanded = !i.state.expanded;
|
||||
i.setState(i.state);
|
||||
}
|
||||
}
|
10
ui/src/components/inbox.tsx
vendored
10
ui/src/components/inbox.tsx
vendored
|
@ -28,6 +28,7 @@ import {
|
|||
saveCommentRes,
|
||||
createCommentLikeRes,
|
||||
commentsToFlatNodes,
|
||||
setupTippy,
|
||||
} from '../utils';
|
||||
import { CommentNodes } from './comment-nodes';
|
||||
import { PrivateMessage } from './private-message';
|
||||
|
@ -116,6 +117,7 @@ export class Inbox extends Component<any, InboxState> {
|
|||
<a
|
||||
href={`/feeds/inbox/${UserService.Instance.auth}.xml`}
|
||||
target="_blank"
|
||||
title="RSS"
|
||||
>
|
||||
<svg class="icon mx-2 text-muted small">
|
||||
<use xlinkHref="#icon-rss">#</use>
|
||||
|
@ -332,18 +334,21 @@ export class Inbox extends Component<any, InboxState> {
|
|||
this.sendUnreadCount();
|
||||
window.scrollTo(0, 0);
|
||||
this.setState(this.state);
|
||||
setupTippy();
|
||||
} else if (res.op == UserOperation.GetUserMentions) {
|
||||
let data = res.data as GetUserMentionsResponse;
|
||||
this.state.mentions = data.mentions;
|
||||
this.sendUnreadCount();
|
||||
window.scrollTo(0, 0);
|
||||
this.setState(this.state);
|
||||
setupTippy();
|
||||
} else if (res.op == UserOperation.GetPrivateMessages) {
|
||||
let data = res.data as PrivateMessagesResponse;
|
||||
this.state.messages = data.messages;
|
||||
this.sendUnreadCount();
|
||||
window.scrollTo(0, 0);
|
||||
this.setState(this.state);
|
||||
setupTippy();
|
||||
} else if (res.op == UserOperation.EditPrivateMessage) {
|
||||
let data = res.data as PrivateMessageResponse;
|
||||
let found: PrivateMessageI = this.state.messages.find(
|
||||
|
@ -364,6 +369,7 @@ export class Inbox extends Component<any, InboxState> {
|
|||
this.sendUnreadCount();
|
||||
window.scrollTo(0, 0);
|
||||
this.setState(this.state);
|
||||
setupTippy();
|
||||
} else if (res.op == UserOperation.MarkAllAsRead) {
|
||||
this.state.replies = [];
|
||||
this.state.mentions = [];
|
||||
|
@ -386,6 +392,7 @@ export class Inbox extends Component<any, InboxState> {
|
|||
}
|
||||
this.sendUnreadCount();
|
||||
this.setState(this.state);
|
||||
setupTippy();
|
||||
} else if (res.op == UserOperation.EditUserMention) {
|
||||
let data = res.data as UserMentionResponse;
|
||||
|
||||
|
@ -429,6 +436,7 @@ export class Inbox extends Component<any, InboxState> {
|
|||
let data = res.data as CommentResponse;
|
||||
saveCommentRes(data, this.state.replies);
|
||||
this.setState(this.state);
|
||||
setupTippy();
|
||||
} else if (res.op == UserOperation.CreateCommentLike) {
|
||||
let data = res.data as CommentResponse;
|
||||
createCommentLikeRes(data, this.state.replies);
|
||||
|
@ -443,9 +451,9 @@ export class Inbox extends Component<any, InboxState> {
|
|||
this.state.messages.filter(
|
||||
r => !r.read && r.creator_id !== UserService.Instance.user.id
|
||||
).length;
|
||||
UserService.Instance.user.unreadCount = count;
|
||||
UserService.Instance.sub.next({
|
||||
user: UserService.Instance.user,
|
||||
unreadCount: count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
45
ui/src/components/main.tsx
vendored
45
ui/src/components/main.tsx
vendored
|
@ -51,7 +51,7 @@ import {
|
|||
createPostLikeFindRes,
|
||||
editPostFindRes,
|
||||
commentsToFlatNodes,
|
||||
commentSortSortType,
|
||||
setupTippy,
|
||||
} from '../utils';
|
||||
import { i18n } from '../i18next';
|
||||
import { T } from 'inferno-i18next';
|
||||
|
@ -183,7 +183,7 @@ export class Main extends Component<any, MainState> {
|
|||
<h5>
|
||||
<T i18nKey="subscribed_to_communities">
|
||||
#
|
||||
<Link class="text-white" to="/communities">
|
||||
<Link class="text-body" to="/communities">
|
||||
#
|
||||
</Link>
|
||||
</T>
|
||||
|
@ -221,7 +221,7 @@ export class Main extends Component<any, MainState> {
|
|||
<h5>
|
||||
<T i18nKey="trending_communities">
|
||||
#
|
||||
<Link class="text-white" to="/communities">
|
||||
<Link class="text-body" to="/communities">
|
||||
#
|
||||
</Link>
|
||||
</T>
|
||||
|
@ -268,13 +268,16 @@ export class Main extends Component<any, MainState> {
|
|||
<div class="card-body">
|
||||
<h5 class="mb-0">{`${this.state.siteRes.site.name}`}</h5>
|
||||
{this.canAdmin && (
|
||||
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
||||
<li className="list-inline-item">
|
||||
<ul class="list-inline mb-1 text-muted font-weight-bold">
|
||||
<li className="list-inline-item-action">
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleEditClick)}
|
||||
data-tippy-content={i18n.t('edit')}
|
||||
>
|
||||
{i18n.t('edit')}
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-edit"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -313,7 +316,10 @@ export class Main extends Component<any, MainState> {
|
|||
<li class="list-inline-item">{i18n.t('admins')}:</li>
|
||||
{this.state.siteRes.admins.map(admin => (
|
||||
<li class="list-inline-item">
|
||||
<Link class="text-info" to={`/u/${admin.name}`}>
|
||||
<Link
|
||||
class="text-body font-weight-bold"
|
||||
to={`/u/${admin.name}`}
|
||||
>
|
||||
{admin.avatar && showAvatars() && (
|
||||
<img
|
||||
height="32"
|
||||
|
@ -386,6 +392,7 @@ export class Main extends Component<any, MainState> {
|
|||
posts() {
|
||||
return (
|
||||
<div class="main-content-wrapper">
|
||||
{this.selects()}
|
||||
{this.state.loading ? (
|
||||
<h5>
|
||||
<svg class="icon icon-spinner spin">
|
||||
|
@ -394,7 +401,6 @@ export class Main extends Component<any, MainState> {
|
|||
</h5>
|
||||
) : (
|
||||
<div>
|
||||
{this.selects()}
|
||||
{this.listings()}
|
||||
{this.paginator()}
|
||||
</div>
|
||||
|
@ -424,11 +430,13 @@ export class Main extends Component<any, MainState> {
|
|||
selects() {
|
||||
return (
|
||||
<div className="mb-3">
|
||||
<span class="mr-3">
|
||||
<DataTypeSelect
|
||||
type_={this.state.dataType}
|
||||
onChange={this.handleDataTypeChange}
|
||||
/>
|
||||
<span class="mx-2">
|
||||
</span>
|
||||
<span class="mr-3">
|
||||
<ListingTypeSelect
|
||||
type_={this.state.listingType}
|
||||
onChange={this.handleListingTypeChange}
|
||||
|
@ -441,8 +449,9 @@ export class Main extends Component<any, MainState> {
|
|||
<a
|
||||
href={`/feeds/all.xml?sort=${SortType[this.state.sort]}`}
|
||||
target="_blank"
|
||||
title="RSS"
|
||||
>
|
||||
<svg class="icon mx-1 text-muted small">
|
||||
<svg class="icon text-muted small">
|
||||
<use xlinkHref="#icon-rss">#</use>
|
||||
</svg>
|
||||
</a>
|
||||
|
@ -454,8 +463,9 @@ export class Main extends Component<any, MainState> {
|
|||
SortType[this.state.sort]
|
||||
}`}
|
||||
target="_blank"
|
||||
title="RSS"
|
||||
>
|
||||
<svg class="icon mx-1 text-muted small">
|
||||
<svg class="icon text-muted small">
|
||||
<use xlinkHref="#icon-rss">#</use>
|
||||
</svg>
|
||||
</a>
|
||||
|
@ -613,6 +623,7 @@ export class Main extends Component<any, MainState> {
|
|||
this.state.posts = data.posts;
|
||||
this.state.loading = false;
|
||||
this.setState(this.state);
|
||||
setupTippy();
|
||||
} else if (res.op == UserOperation.CreatePost) {
|
||||
let data = res.data as PostResponse;
|
||||
|
||||
|
@ -626,9 +637,19 @@ export class Main extends Component<any, MainState> {
|
|||
this.state.posts.unshift(data.post);
|
||||
}
|
||||
} else {
|
||||
// NSFW posts
|
||||
let nsfw = data.post.nsfw || data.post.community_nsfw;
|
||||
|
||||
// Don't push the post if its nsfw, and don't have that setting on
|
||||
if (
|
||||
!nsfw ||
|
||||
(nsfw &&
|
||||
UserService.Instance.user &&
|
||||
UserService.Instance.user.show_nsfw)
|
||||
) {
|
||||
this.state.posts.unshift(data.post);
|
||||
}
|
||||
|
||||
}
|
||||
this.setState(this.state);
|
||||
} else if (res.op == UserOperation.EditPost) {
|
||||
let data = res.data as PostResponse;
|
||||
|
|
2
ui/src/components/modlog.tsx
vendored
2
ui/src/components/modlog.tsx
vendored
|
@ -354,7 +354,7 @@ export class Modlog extends Component<any, ModlogState> {
|
|||
<h5>
|
||||
{this.state.communityName && (
|
||||
<Link
|
||||
className="text-white"
|
||||
className="text-body"
|
||||
to={`/c/${this.state.communityName}`}
|
||||
>
|
||||
/c/{this.state.communityName}{' '}
|
||||
|
|
31
ui/src/components/moment-time.tsx
vendored
31
ui/src/components/moment-time.tsx
vendored
|
@ -1,6 +1,6 @@
|
|||
import { Component } from 'inferno';
|
||||
import moment from 'moment';
|
||||
import { getMomentLanguage } from '../utils';
|
||||
import { getMomentLanguage, capitalizeFirstLetter } from '../utils';
|
||||
import { i18n } from '../i18next';
|
||||
|
||||
interface MomentTimeProps {
|
||||
|
@ -9,6 +9,7 @@ interface MomentTimeProps {
|
|||
when_?: string;
|
||||
updated?: string;
|
||||
};
|
||||
showAgo?: boolean;
|
||||
}
|
||||
|
||||
export class MomentTime extends Component<MomentTimeProps, any> {
|
||||
|
@ -23,13 +24,35 @@ export class MomentTime extends Component<MomentTimeProps, any> {
|
|||
render() {
|
||||
if (this.props.data.updated) {
|
||||
return (
|
||||
<span title={this.props.data.updated} className="font-italics">
|
||||
{i18n.t('modified')} {moment.utc(this.props.data.updated).fromNow()}
|
||||
<span
|
||||
data-tippy-content={`${capitalizeFirstLetter(
|
||||
i18n.t('modified')
|
||||
)} ${this.format(this.props.data.updated)}`}
|
||||
className="font-italics pointer unselectable"
|
||||
>
|
||||
<svg class="icon icon-inline mr-1">
|
||||
<use xlinkHref="#icon-edit-2"></use>
|
||||
</svg>
|
||||
{moment.utc(this.props.data.updated).fromNow(!this.props.showAgo)}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
let str = this.props.data.published || this.props.data.when_;
|
||||
return <span title={str}>{moment.utc(str).fromNow()}</span>;
|
||||
return (
|
||||
<span
|
||||
className="pointer unselectable"
|
||||
data-tippy-content={this.format(str)}
|
||||
>
|
||||
{moment.utc(str).fromNow(!this.props.showAgo)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
format(input: string): string {
|
||||
return moment
|
||||
.utc(input)
|
||||
.local()
|
||||
.format('LLLL');
|
||||
}
|
||||
}
|
||||
|
|
35
ui/src/components/navbar.tsx
vendored
35
ui/src/components/navbar.tsx
vendored
|
@ -60,8 +60,10 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
// Subscribe to user changes
|
||||
this.userSub = UserService.Instance.sub.subscribe(user => {
|
||||
this.state.isLoggedIn = user.user !== undefined;
|
||||
this.state.unreadCount = user.unreadCount;
|
||||
if (this.state.isLoggedIn) {
|
||||
this.state.unreadCount = user.user.unreadCount;
|
||||
this.requestNotificationPermission();
|
||||
}
|
||||
this.setState(this.state);
|
||||
});
|
||||
|
||||
|
@ -103,6 +105,7 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
type="button"
|
||||
aria-label="menu"
|
||||
onClick={linkEvent(this, this.expandNavbar)}
|
||||
data-tippy-content={i18n.t('expand_here')}
|
||||
>
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
@ -111,12 +114,16 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
>
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item">
|
||||
<Link class="nav-link" to="/communities">
|
||||
<Link
|
||||
class="nav-link"
|
||||
to="/communities"
|
||||
title={i18n.t('communities')}
|
||||
>
|
||||
{i18n.t('communities')}
|
||||
</Link>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<Link class="nav-link" to="/search">
|
||||
<Link class="nav-link" to="/search" title={i18n.t('search')}>
|
||||
{i18n.t('search')}
|
||||
</Link>
|
||||
</li>
|
||||
|
@ -127,12 +134,17 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
pathname: '/create_post',
|
||||
state: { prevPath: this.currentLocation },
|
||||
}}
|
||||
title={i18n.t('create_post')}
|
||||
>
|
||||
{i18n.t('create_post')}
|
||||
</Link>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<Link class="nav-link" to="/create_community">
|
||||
<Link
|
||||
class="nav-link"
|
||||
to="/create_community"
|
||||
title={i18n.t('create_community')}
|
||||
>
|
||||
{i18n.t('create_community')}
|
||||
</Link>
|
||||
</li>
|
||||
|
@ -152,9 +164,9 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
{this.state.isLoggedIn ? (
|
||||
<>
|
||||
<li className="nav-item mt-1">
|
||||
<Link class="nav-link" to="/inbox">
|
||||
<Link class="nav-link" to="/inbox" title={i18n.t('inbox')}>
|
||||
<svg class="icon">
|
||||
<use xlinkHref="#icon-mail"></use>
|
||||
<use xlinkHref="#icon-bell"></use>
|
||||
</svg>
|
||||
{this.state.unreadCount > 0 && (
|
||||
<span class="ml-1 badge badge-light">
|
||||
|
@ -167,6 +179,7 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
<Link
|
||||
class="nav-link"
|
||||
to={`/u/${UserService.Instance.user.username}`}
|
||||
title={i18n.t('settings')}
|
||||
>
|
||||
<span>
|
||||
{UserService.Instance.user.avatar && showAvatars() && (
|
||||
|
@ -185,7 +198,11 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
</li>
|
||||
</>
|
||||
) : (
|
||||
<Link class="nav-link" to="/login">
|
||||
<Link
|
||||
class="nav-link"
|
||||
to="/login"
|
||||
title={i18n.t('login_sign_up')}
|
||||
>
|
||||
{i18n.t('login_sign_up')}
|
||||
</Link>
|
||||
)}
|
||||
|
@ -261,7 +278,7 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
} else if (res.op == UserOperation.GetSite) {
|
||||
let data = res.data as GetSiteResponse;
|
||||
|
||||
if (data.site) {
|
||||
if (data.site && !this.state.siteName) {
|
||||
this.state.siteName = data.site.name;
|
||||
WebSocketService.Instance.site = data.site;
|
||||
this.setState(this.state);
|
||||
|
@ -304,9 +321,9 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
}
|
||||
|
||||
sendUnreadCount() {
|
||||
UserService.Instance.user.unreadCount = this.state.unreadCount;
|
||||
UserService.Instance.sub.next({
|
||||
user: UserService.Instance.user,
|
||||
unreadCount: this.state.unreadCount,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
38
ui/src/components/post-form.tsx
vendored
38
ui/src/components/post-form.tsx
vendored
|
@ -1,4 +1,5 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Prompt } from 'inferno-router';
|
||||
import { PostListings } from './post-listings';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||
|
@ -32,9 +33,11 @@ import {
|
|||
toast,
|
||||
randomStr,
|
||||
setupTribute,
|
||||
setupTippy,
|
||||
} from '../utils';
|
||||
import autosize from 'autosize';
|
||||
import Tribute from 'tributejs/src/Tribute.js';
|
||||
import Selectr from 'mobius1-selectr';
|
||||
import { i18n } from '../i18next';
|
||||
|
||||
const MAX_POST_TITLE_LENGTH = 200;
|
||||
|
@ -141,6 +144,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
this.setState(this.state);
|
||||
autosize.update(textarea);
|
||||
});
|
||||
setupTippy();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -150,6 +154,15 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Prompt
|
||||
when={
|
||||
!this.state.loading &&
|
||||
(this.state.postForm.name ||
|
||||
this.state.postForm.url ||
|
||||
this.state.postForm.body)
|
||||
}
|
||||
message={i18n.t('block_leaving')}
|
||||
/>
|
||||
<form onSubmit={linkEvent(this, this.handlePostSubmit)}>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label" htmlFor="post-url">
|
||||
|
@ -178,9 +191,12 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
<label
|
||||
htmlFor="file-upload"
|
||||
className={`${UserService.Instance.user &&
|
||||
'pointer'} d-inline-block mr-2 float-right text-muted small font-weight-bold`}
|
||||
'pointer'} d-inline-block float-right text-muted h6 font-weight-bold`}
|
||||
data-tippy-content={i18n.t('upload_image')}
|
||||
>
|
||||
{i18n.t('upload_image')}
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-image"></use>
|
||||
</svg>
|
||||
</label>
|
||||
<input
|
||||
id="file-upload"
|
||||
|
@ -278,9 +294,12 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
<a
|
||||
href={markdownHelpUrl}
|
||||
target="_blank"
|
||||
class="d-inline-block float-right text-muted small font-weight-bold"
|
||||
class="d-inline-block float-right text-muted h6 font-weight-bold"
|
||||
title={i18n.t('formatting_help')}
|
||||
>
|
||||
{i18n.t('formatting_help')}
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-help-circle"></use>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -478,7 +497,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
let url = `${window.location.origin}/pictshare/${res.url}`;
|
||||
let url = `${window.location.origin}/pictshare/${encodeURI(res.url)}`;
|
||||
if (res.filetype == 'mp4') {
|
||||
url += '/raw';
|
||||
}
|
||||
|
@ -514,6 +533,15 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
this.state.postForm.community_id = data.communities[0].id;
|
||||
}
|
||||
this.setState(this.state);
|
||||
|
||||
// Set up select searching
|
||||
let selectId: any = document.getElementById('post-community');
|
||||
if (selectId) {
|
||||
let selector = new Selectr(selectId, { nativeDropdown: false });
|
||||
selector.on('selectr.select', option => {
|
||||
this.state.postForm.community_id = Number(option.value);
|
||||
});
|
||||
}
|
||||
} else if (res.op == UserOperation.CreatePost) {
|
||||
let data = res.data as PostResponse;
|
||||
if (data.post.creator_id == UserService.Instance.user.id) {
|
||||
|
|
496
ui/src/components/post-listing.tsx
vendored
496
ui/src/components/post-listing.tsx
vendored
|
@ -18,6 +18,7 @@ import {
|
|||
} from '../interfaces';
|
||||
import { MomentTime } from './moment-time';
|
||||
import { PostForm } from './post-form';
|
||||
import { IFramelyCard } from './iframely-card';
|
||||
import {
|
||||
mdToHtml,
|
||||
canMod,
|
||||
|
@ -27,7 +28,8 @@ import {
|
|||
getUnixTime,
|
||||
pictshareAvatarThumbnail,
|
||||
showAvatars,
|
||||
imageThumbnailer,
|
||||
pictshareImage,
|
||||
setupTippy,
|
||||
} from '../utils';
|
||||
import { i18n } from '../i18next';
|
||||
|
||||
|
@ -43,6 +45,7 @@ interface PostListingState {
|
|||
showConfirmTransferCommunity: boolean;
|
||||
imageExpanded: boolean;
|
||||
viewSource: boolean;
|
||||
showAdvanced: boolean;
|
||||
my_vote: number;
|
||||
score: number;
|
||||
upvotes: number;
|
||||
|
@ -70,6 +73,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
showConfirmTransferCommunity: false,
|
||||
imageExpanded: false,
|
||||
viewSource: false,
|
||||
showAdvanced: false,
|
||||
my_vote: this.props.post.my_vote,
|
||||
score: this.props.post.score,
|
||||
upvotes: this.props.post.upvotes,
|
||||
|
@ -96,9 +100,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<div class="row">
|
||||
<div class="">
|
||||
{!this.state.showEdit ? (
|
||||
this.listing()
|
||||
<>
|
||||
{this.listing()}
|
||||
{this.body()}
|
||||
</>
|
||||
) : (
|
||||
<div class="col-12">
|
||||
<PostForm
|
||||
|
@ -112,28 +119,160 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
);
|
||||
}
|
||||
|
||||
body() {
|
||||
return (
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
{this.props.post.url &&
|
||||
this.props.showBody &&
|
||||
this.props.post.embed_title && (
|
||||
<IFramelyCard post={this.props.post} />
|
||||
)}
|
||||
{this.props.showBody && this.props.post.body && (
|
||||
<>
|
||||
{this.state.viewSource ? (
|
||||
<pre>{this.props.post.body}</pre>
|
||||
) : (
|
||||
<div
|
||||
className="md-div"
|
||||
dangerouslySetInnerHTML={mdToHtml(this.props.post.body)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
imgThumb(src: string) {
|
||||
let post = this.props.post;
|
||||
return (
|
||||
<img
|
||||
className={`img-fluid thumbnail rounded ${(post.nsfw ||
|
||||
post.community_nsfw) &&
|
||||
'img-blur'}`}
|
||||
src={src}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
getImage(thumbnail: boolean = false) {
|
||||
let post = this.props.post;
|
||||
if (isImage(post.url)) {
|
||||
if (post.url.includes('pictshare')) {
|
||||
return pictshareImage(post.url, thumbnail);
|
||||
} else {
|
||||
return post.url;
|
||||
}
|
||||
} else if (post.thumbnail_url) {
|
||||
return pictshareImage(post.thumbnail_url, thumbnail);
|
||||
}
|
||||
}
|
||||
|
||||
thumbnail() {
|
||||
let post = this.props.post;
|
||||
|
||||
if (isImage(post.url)) {
|
||||
return (
|
||||
<span
|
||||
class="text-body pointer"
|
||||
data-tippy-content={i18n.t('expand_here')}
|
||||
onClick={linkEvent(this, this.handleImageExpandClick)}
|
||||
>
|
||||
{this.imgThumb(this.getImage(true))}
|
||||
<svg class="icon mini-overlay">
|
||||
<use xlinkHref="#icon-image"></use>
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
} else if (post.thumbnail_url) {
|
||||
return (
|
||||
<a
|
||||
className="text-body"
|
||||
href={post.url}
|
||||
target="_blank"
|
||||
title={post.url}
|
||||
>
|
||||
{this.imgThumb(this.getImage(true))}
|
||||
<svg class="icon mini-overlay">
|
||||
<use xlinkHref="#icon-external-link"></use>
|
||||
</svg>
|
||||
</a>
|
||||
);
|
||||
} else if (post.url) {
|
||||
if (isVideo(post.url)) {
|
||||
return (
|
||||
<div class="embed-responsive embed-responsive-16by9">
|
||||
<video
|
||||
playsinline
|
||||
muted
|
||||
loop
|
||||
controls
|
||||
class="embed-responsive-item"
|
||||
>
|
||||
<source src={post.url} type="video/mp4" />
|
||||
</video>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<a
|
||||
className="text-body"
|
||||
href={post.url}
|
||||
target="_blank"
|
||||
title={post.url}
|
||||
>
|
||||
<svg class="icon thumbnail">
|
||||
<use xlinkHref="#icon-external-link"></use>
|
||||
</svg>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
<Link
|
||||
className="text-body"
|
||||
to={`/post/${post.id}`}
|
||||
title={i18n.t('comments')}
|
||||
>
|
||||
<svg class="icon thumbnail">
|
||||
<use xlinkHref="#icon-message-square"></use>
|
||||
</svg>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
listing() {
|
||||
let post = this.props.post;
|
||||
return (
|
||||
<div class="listing col-12">
|
||||
<div className={`vote-bar mr-2 float-left small text-center`}>
|
||||
<div class="row">
|
||||
<div className={`vote-bar col-1 pr-0 small text-center`}>
|
||||
<button
|
||||
className={`vote-animate btn btn-link p-0 ${
|
||||
this.state.my_vote == 1 ? 'text-info' : 'text-muted'
|
||||
}`}
|
||||
onClick={linkEvent(this, this.handlePostLike)}
|
||||
data-tippy-content={i18n.t('upvote')}
|
||||
>
|
||||
<svg class="icon upvote">
|
||||
<use xlinkHref="#icon-arrow-up"></use>
|
||||
</svg>
|
||||
</button>
|
||||
<div class={`font-weight-bold text-muted`}>{this.state.score}</div>
|
||||
<div
|
||||
class={`unselectable pointer font-weight-bold text-muted px-1`}
|
||||
data-tippy-content={this.pointsTippy}
|
||||
>
|
||||
{this.state.score}
|
||||
</div>
|
||||
{WebSocketService.Instance.site.enable_downvotes && (
|
||||
<button
|
||||
className={`vote-animate btn btn-link p-0 ${
|
||||
this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
|
||||
}`}
|
||||
onClick={linkEvent(this, this.handlePostDisLike)}
|
||||
data-tippy-content={i18n.t('downvote')}
|
||||
>
|
||||
<svg class="icon downvote">
|
||||
<use xlinkHref="#icon-arrow-down"></use>
|
||||
|
@ -141,37 +280,19 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
</button>
|
||||
)}
|
||||
</div>
|
||||
{post.url && isImage(post.url) && !this.state.imageExpanded && (
|
||||
<span
|
||||
title={i18n.t('expand_here')}
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleImageExpandClick)}
|
||||
>
|
||||
<img
|
||||
className={`mx-2 mt-1 float-left img-fluid thumbnail rounded ${(post.nsfw ||
|
||||
post.community_nsfw) &&
|
||||
'img-blur'}`}
|
||||
src={imageThumbnailer(post.url)}
|
||||
/>
|
||||
</span>
|
||||
{!this.state.imageExpanded && (
|
||||
<div class="col-3 col-sm-2 pr-0 mt-1">
|
||||
<div class="position-relative">{this.thumbnail()}</div>
|
||||
</div>
|
||||
)}
|
||||
{post.url && isVideo(post.url) && (
|
||||
<video
|
||||
playsinline
|
||||
muted
|
||||
loop
|
||||
controls
|
||||
class="mx-2 mt-1 float-left"
|
||||
height="100"
|
||||
width="150"
|
||||
<div
|
||||
class={`${this.state.imageExpanded ? 'col-12' : 'col-8 col-sm-9'}`}
|
||||
>
|
||||
<source src={post.url} type="video/mp4" />
|
||||
</video>
|
||||
)}
|
||||
<div className="ml-4">
|
||||
<div className="post-title text-wrap-truncate">
|
||||
<div class="row">
|
||||
<div className="col-12">
|
||||
<div className="post-title">
|
||||
<h5 className="mb-0 d-inline">
|
||||
{post.url ? (
|
||||
{this.props.showBody && post.url ? (
|
||||
<a
|
||||
className="text-body"
|
||||
href={post.url}
|
||||
|
@ -190,8 +311,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
</Link>
|
||||
)}
|
||||
</h5>
|
||||
{post.url && (
|
||||
<small>
|
||||
{post.url &&
|
||||
!(new URL(post.url).hostname == window.location.hostname) && (
|
||||
<small class="d-inline-block">
|
||||
<a
|
||||
className="ml-2 text-muted font-italic"
|
||||
href={post.url}
|
||||
|
@ -199,33 +321,46 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
title={post.url}
|
||||
>
|
||||
{new URL(post.url).hostname}
|
||||
<svg class="ml-1 icon icon-inline">
|
||||
<use xlinkHref="#icon-external-link"></use>
|
||||
</svg>
|
||||
</a>
|
||||
</small>
|
||||
)}
|
||||
{post.url && isImage(post.url) && (
|
||||
{(isImage(post.url) || this.props.post.thumbnail_url) && (
|
||||
<>
|
||||
{!this.state.imageExpanded ? (
|
||||
<span
|
||||
class="text-monospace pointer ml-2 text-muted small"
|
||||
title={i18n.t('expand_here')}
|
||||
class="text-monospace unselectable pointer ml-2 text-muted small"
|
||||
data-tippy-content={i18n.t('expand_here')}
|
||||
onClick={linkEvent(this, this.handleImageExpandClick)}
|
||||
>
|
||||
[+]
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-plus-square"></use>
|
||||
</svg>
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
<span
|
||||
class="text-monospace pointer ml-2 text-muted small"
|
||||
class="text-monospace unselectable pointer ml-2 text-muted small"
|
||||
onClick={linkEvent(this, this.handleImageExpandClick)}
|
||||
>
|
||||
[-]
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-minus-square"></use>
|
||||
</svg>
|
||||
</span>
|
||||
<div>
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleImageExpandClick)}
|
||||
onClick={linkEvent(
|
||||
this,
|
||||
this.handleImageExpandClick
|
||||
)}
|
||||
>
|
||||
<img class="img-fluid img-expanded" src={post.url} />
|
||||
<img
|
||||
class="img-fluid img-expanded"
|
||||
src={this.getImage()}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
|
@ -238,18 +373,33 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
</small>
|
||||
)}
|
||||
{post.deleted && (
|
||||
<small className="ml-2 text-muted font-italic">
|
||||
{i18n.t('deleted')}
|
||||
<small
|
||||
className="unselectable pointer ml-2 text-muted font-italic"
|
||||
data-tippy-content={i18n.t('deleted')}
|
||||
>
|
||||
<svg class={`icon icon-inline text-danger`}>
|
||||
<use xlinkHref="#icon-trash"></use>
|
||||
</svg>
|
||||
</small>
|
||||
)}
|
||||
{post.locked && (
|
||||
<small className="ml-2 text-muted font-italic">
|
||||
{i18n.t('locked')}
|
||||
<small
|
||||
className="unselectable pointer ml-2 text-muted font-italic"
|
||||
data-tippy-content={i18n.t('locked')}
|
||||
>
|
||||
<svg class={`icon icon-inline text-danger`}>
|
||||
<use xlinkHref="#icon-lock"></use>
|
||||
</svg>
|
||||
</small>
|
||||
)}
|
||||
{post.stickied && (
|
||||
<small className="ml-2 text-muted font-italic">
|
||||
{i18n.t('stickied')}
|
||||
<small
|
||||
className="unselectable pointer ml-2 text-muted font-italic"
|
||||
data-tippy-content={i18n.t('stickied')}
|
||||
>
|
||||
<svg class={`icon icon-inline text-success`}>
|
||||
<use xlinkHref="#icon-pin"></use>
|
||||
</svg>
|
||||
</small>
|
||||
)}
|
||||
{post.nsfw && (
|
||||
|
@ -259,11 +409,16 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="details ml-4">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div className="details col-12">
|
||||
<ul class="list-inline mb-0 text-muted small">
|
||||
<li className="list-inline-item">
|
||||
<span>{i18n.t('by')} </span>
|
||||
<Link className="text-info" to={`/u/${post.creator_name}`}>
|
||||
<Link
|
||||
className="text-body font-weight-bold"
|
||||
to={`/u/${post.creator_name}`}
|
||||
>
|
||||
{post.creator_avatar && showAvatars() && (
|
||||
<img
|
||||
height="32"
|
||||
|
@ -275,7 +430,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
<span>{post.creator_name}</span>
|
||||
</Link>
|
||||
{this.isMod && (
|
||||
<span className="mx-1 badge badge-light">{i18n.t('mod')}</span>
|
||||
<span className="mx-1 badge badge-light">
|
||||
{i18n.t('mod')}
|
||||
</span>
|
||||
)}
|
||||
{this.isAdmin && (
|
||||
<span className="mx-1 badge badge-light">
|
||||
|
@ -296,28 +453,55 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
</span>
|
||||
)}
|
||||
</li>
|
||||
<li className="list-inline-item">•</li>
|
||||
<li className="list-inline-item">
|
||||
<span>
|
||||
<MomentTime data={post} />
|
||||
</span>
|
||||
</li>
|
||||
<li className="list-inline-item">•</li>
|
||||
{this.state.upvotes !== this.state.score && (
|
||||
<>
|
||||
<span
|
||||
class="unselectable pointer mr-2"
|
||||
data-tippy-content={this.pointsTippy}
|
||||
>
|
||||
<li className="list-inline-item">
|
||||
<span>
|
||||
(<span className="text-info">+{this.state.upvotes}</span>
|
||||
<span> | </span>
|
||||
<span className="text-danger">-{this.state.downvotes}</span>
|
||||
<span>) </span>
|
||||
<span className="text-muted">
|
||||
<svg class="small icon icon-inline mr-1">
|
||||
<use xlinkHref="#icon-arrow-up"></use>
|
||||
</svg>
|
||||
{this.state.upvotes}
|
||||
</span>
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<Link className="text-muted" to={`/post/${post.id}`}>
|
||||
{i18n.t('number_of_comments', {
|
||||
<span className="text-muted">
|
||||
<svg class="small icon icon-inline mr-1">
|
||||
<use xlinkHref="#icon-arrow-down"></use>
|
||||
</svg>
|
||||
{this.state.downvotes}
|
||||
</span>
|
||||
</li>
|
||||
</span>
|
||||
<li className="list-inline-item">•</li>
|
||||
</>
|
||||
)}
|
||||
<li className="list-inline-item">
|
||||
<Link
|
||||
className="text-muted"
|
||||
title={i18n.t('number_of_comments', {
|
||||
count: post.number_of_comments,
|
||||
})}
|
||||
to={`/post/${post.id}`}
|
||||
>
|
||||
<svg class="mr-1 icon icon-inline">
|
||||
<use xlinkHref="#icon-message-square"></use>
|
||||
</svg>
|
||||
{post.number_of_comments}
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="list-inline mb-1 text-muted small">
|
||||
<ul class="list-inline mb-1 small text-muted">
|
||||
{this.props.post.duplicates && (
|
||||
<>
|
||||
<li className="list-inline-item mr-2">
|
||||
|
@ -325,71 +509,148 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
</li>
|
||||
{this.props.post.duplicates.map(post => (
|
||||
<li className="list-inline-item mr-2">
|
||||
<Link to={`/post/${post.id}`}>{post.community_name}</Link>
|
||||
<Link to={`/post/${post.id}`}>
|
||||
{post.community_name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
||||
<ul class="list-inline mb-1 text-muted h5 font-weight-bold">
|
||||
{UserService.Instance.user && (
|
||||
<>
|
||||
{this.props.showBody && (
|
||||
<>
|
||||
<li className="list-inline-item mr-2">
|
||||
<li className="list-inline-item-action">
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleSavePostClick)}
|
||||
data-tippy-content={
|
||||
post.saved ? i18n.t('unsave') : i18n.t('save')
|
||||
}
|
||||
>
|
||||
{post.saved ? i18n.t('unsave') : i18n.t('save')}
|
||||
<svg
|
||||
class={`icon icon-inline ${post.saved &&
|
||||
'text-warning'}`}
|
||||
>
|
||||
<use xlinkHref="#icon-star"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
<li className="list-inline-item mr-2">
|
||||
<li className="list-inline-item-action">
|
||||
<Link
|
||||
className="text-muted"
|
||||
to={`/create_post${this.crossPostParams}`}
|
||||
title={i18n.t('cross_post')}
|
||||
>
|
||||
{i18n.t('cross_post')}
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-copy"></use>
|
||||
</svg>
|
||||
</Link>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
{this.myPost && this.props.showBody && (
|
||||
<>
|
||||
<li className="list-inline-item">
|
||||
<li className="list-inline-item-action">
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleEditClick)}
|
||||
data-tippy-content={i18n.t('edit')}
|
||||
>
|
||||
{i18n.t('edit')}
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-edit"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
<li className="list-inline-item mr-2">
|
||||
<li className="list-inline-item-action">
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleDeleteClick)}
|
||||
data-tippy-content={
|
||||
!post.deleted
|
||||
? i18n.t('delete')
|
||||
: i18n.t('restore')
|
||||
}
|
||||
>
|
||||
{!post.deleted ? i18n.t('delete') : i18n.t('restore')}
|
||||
<svg
|
||||
class={`icon icon-inline ${post.deleted &&
|
||||
'text-danger'}`}
|
||||
>
|
||||
<use xlinkHref="#icon-trash"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!this.state.showAdvanced && this.props.showBody ? (
|
||||
<li className="list-inline-item-action">
|
||||
<span
|
||||
className="pointer"
|
||||
onClick={linkEvent(this, this.handleShowAdvanced)}
|
||||
data-tippy-content={i18n.t('more')}
|
||||
>
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-more-vertical"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
) : (
|
||||
<>
|
||||
{this.props.showBody && post.body && (
|
||||
<li className="list-inline-item-action">
|
||||
<span
|
||||
className="pointer"
|
||||
onClick={linkEvent(this, this.handleViewSource)}
|
||||
data-tippy-content={i18n.t('view_source')}
|
||||
>
|
||||
<svg
|
||||
class={`icon icon-inline ${this.state
|
||||
.viewSource && 'text-success'}`}
|
||||
>
|
||||
<use xlinkHref="#icon-file-text"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
{this.canModOnSelf && (
|
||||
<>
|
||||
<li className="list-inline-item">
|
||||
<li className="list-inline-item-action">
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleModLock)}
|
||||
data-tippy-content={
|
||||
post.locked
|
||||
? i18n.t('unlock')
|
||||
: i18n.t('lock')
|
||||
}
|
||||
>
|
||||
{post.locked ? i18n.t('unlock') : i18n.t('lock')}
|
||||
<svg
|
||||
class={`icon icon-inline ${post.locked &&
|
||||
'text-danger'}`}
|
||||
>
|
||||
<use xlinkHref="#icon-lock"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<li className="list-inline-item-action">
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleModSticky)}
|
||||
data-tippy-content={
|
||||
post.stickied
|
||||
? i18n.t('unsticky')
|
||||
: i18n.t('sticky')
|
||||
}
|
||||
>
|
||||
{post.stickied ? i18n.t('unsticky') : i18n.t('sticky')}
|
||||
<svg
|
||||
class={`icon icon-inline ${post.stickied &&
|
||||
'text-success'}`}
|
||||
>
|
||||
<use xlinkHref="#icon-pin"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
</>
|
||||
|
@ -400,14 +661,20 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
{!post.removed ? (
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleModRemoveShow)}
|
||||
onClick={linkEvent(
|
||||
this,
|
||||
this.handleModRemoveShow
|
||||
)}
|
||||
>
|
||||
{i18n.t('remove')}
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleModRemoveSubmit)}
|
||||
onClick={linkEvent(
|
||||
this,
|
||||
this.handleModRemoveSubmit
|
||||
)}
|
||||
>
|
||||
{i18n.t('restore')}
|
||||
</span>
|
||||
|
@ -459,7 +726,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
</>
|
||||
)}
|
||||
{/* Community creators and admins can transfer community to another mod */}
|
||||
{(this.amCommunityCreator || this.canAdmin) && this.isMod && (
|
||||
{(this.amCommunityCreator || this.canAdmin) &&
|
||||
this.isMod && (
|
||||
<li className="list-inline-item">
|
||||
{!this.state.showConfirmTransferCommunity ? (
|
||||
<span
|
||||
|
@ -489,7 +757,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
class="pointer d-inline-block"
|
||||
onClick={linkEvent(
|
||||
this,
|
||||
this.handleCancelShowConfirmTransferCommunity
|
||||
this
|
||||
.handleCancelShowConfirmTransferCommunity
|
||||
)}
|
||||
>
|
||||
{i18n.t('no')}
|
||||
|
@ -506,14 +775,20 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
{!post.banned ? (
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleModBanShow)}
|
||||
onClick={linkEvent(
|
||||
this,
|
||||
this.handleModBanShow
|
||||
)}
|
||||
>
|
||||
{i18n.t('ban_from_site')}
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleModBanSubmit)}
|
||||
onClick={linkEvent(
|
||||
this,
|
||||
this.handleModBanSubmit
|
||||
)}
|
||||
>
|
||||
{i18n.t('unban_from_site')}
|
||||
</span>
|
||||
|
@ -554,7 +829,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
</span>
|
||||
<span
|
||||
class="pointer d-inline-block mr-1"
|
||||
onClick={linkEvent(this, this.handleTransferSite)}
|
||||
onClick={linkEvent(
|
||||
this,
|
||||
this.handleTransferSite
|
||||
)}
|
||||
>
|
||||
{i18n.t('yes')}
|
||||
</span>
|
||||
|
@ -573,15 +851,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
)}
|
||||
</>
|
||||
)}
|
||||
{this.props.showBody && post.body && (
|
||||
<li className="list-inline-item">
|
||||
<span
|
||||
className="pointer"
|
||||
onClick={linkEvent(this, this.handleViewSource)}
|
||||
>
|
||||
{i18n.t('view_source')}
|
||||
</span>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
{this.state.showRemoveDialog && (
|
||||
|
@ -628,18 +898,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
</div>
|
||||
</form>
|
||||
)}
|
||||
{this.props.showBody && post.body && (
|
||||
<>
|
||||
{this.state.viewSource ? (
|
||||
<pre>{post.body}</pre>
|
||||
) : (
|
||||
<div
|
||||
className="md-div"
|
||||
dangerouslySetInnerHTML={mdToHtml(post.body)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -758,6 +1018,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
|
||||
WebSocketService.Instance.likePost(form);
|
||||
i.setState(i.state);
|
||||
setupTippy();
|
||||
}
|
||||
|
||||
handlePostDisLike(i: PostListing) {
|
||||
|
@ -784,6 +1045,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
|
||||
WebSocketService.Instance.likePost(form);
|
||||
i.setState(i.state);
|
||||
setupTippy();
|
||||
}
|
||||
|
||||
handleEditClick(i: PostListing) {
|
||||
|
@ -829,8 +1091,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
|
||||
get crossPostParams(): string {
|
||||
let params = `?title=${this.props.post.name}`;
|
||||
if (this.props.post.url) {
|
||||
params += `&url=${this.props.post.url}`;
|
||||
let post = this.props.post;
|
||||
|
||||
if (post.url) {
|
||||
params += `&url=${post.url}`;
|
||||
}
|
||||
if (this.props.post.body) {
|
||||
params += `&body=${this.props.post.body}`;
|
||||
|
@ -1019,4 +1283,26 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
i.state.viewSource = !i.state.viewSource;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleShowAdvanced(i: PostListing) {
|
||||
i.state.showAdvanced = !i.state.showAdvanced;
|
||||
i.setState(i.state);
|
||||
setupTippy();
|
||||
}
|
||||
|
||||
get pointsTippy(): string {
|
||||
let points = i18n.t('number_of_points', {
|
||||
count: this.state.score,
|
||||
});
|
||||
|
||||
let upvotes = i18n.t('number_of_upvotes', {
|
||||
count: this.state.upvotes,
|
||||
});
|
||||
|
||||
let downvotes = i18n.t('number_of_downvotes', {
|
||||
count: this.state.downvotes,
|
||||
});
|
||||
|
||||
return `${points} • ${upvotes} • ${downvotes}`;
|
||||
}
|
||||
}
|
||||
|
|
2
ui/src/components/post-listings.tsx
vendored
2
ui/src/components/post-listings.tsx
vendored
|
@ -53,7 +53,7 @@ export class PostListings extends Component<PostListingsProps, any> {
|
|||
}
|
||||
|
||||
if (this.props.sort !== undefined) {
|
||||
postSort(out, this.props.sort);
|
||||
postSort(out, this.props.sort, this.props.showCommunity == undefined);
|
||||
}
|
||||
|
||||
return out;
|
||||
|
|
28
ui/src/components/post.tsx
vendored
28
ui/src/components/post.tsx
vendored
|
@ -37,6 +37,7 @@ import {
|
|||
createCommentLikeRes,
|
||||
createPostLikeRes,
|
||||
commentsToFlatNodes,
|
||||
setupTippy,
|
||||
} from '../utils';
|
||||
import { PostListing } from './post-listing';
|
||||
import { PostListings } from './post-listings';
|
||||
|
@ -156,6 +157,10 @@ export class Post extends Component<any, PostState> {
|
|||
auth: null,
|
||||
};
|
||||
WebSocketService.Instance.editComment(form);
|
||||
UserService.Instance.user.unreadCount--;
|
||||
UserService.Instance.sub.next({
|
||||
user: UserService.Instance.user,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -206,7 +211,7 @@ export class Post extends Component<any, PostState> {
|
|||
|
||||
sortRadios() {
|
||||
return (
|
||||
<div class="btn-group btn-group-toggle mb-3">
|
||||
<div class="btn-group btn-group-toggle">
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer ${this.state
|
||||
.commentSort === CommentSortType.Hot && 'active'}`}
|
||||
|
@ -295,7 +300,7 @@ export class Post extends Component<any, PostState> {
|
|||
i.setState(i.state);
|
||||
}
|
||||
|
||||
private buildCommentsTree(): Array<CommentNodeI> {
|
||||
buildCommentsTree(): Array<CommentNodeI> {
|
||||
let map = new Map<number, CommentNodeI>();
|
||||
for (let comment of this.state.comments) {
|
||||
let node: CommentNodeI = {
|
||||
|
@ -306,16 +311,27 @@ export class Post extends Component<any, PostState> {
|
|||
}
|
||||
let tree: Array<CommentNodeI> = [];
|
||||
for (let comment of this.state.comments) {
|
||||
let child = map.get(comment.id);
|
||||
if (comment.parent_id) {
|
||||
map.get(comment.parent_id).children.push(map.get(comment.id));
|
||||
let parent_ = map.get(comment.parent_id);
|
||||
parent_.children.push(child);
|
||||
} else {
|
||||
tree.push(map.get(comment.id));
|
||||
tree.push(child);
|
||||
}
|
||||
|
||||
this.setDepth(child);
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
setDepth(node: CommentNodeI, i: number = 0): void {
|
||||
for (let child of node.children) {
|
||||
child.comment.depth = i;
|
||||
this.setDepth(child, i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
commentsTree() {
|
||||
let nodes = this.buildCommentsTree();
|
||||
return (
|
||||
|
@ -366,6 +382,7 @@ export class Post extends Component<any, PostState> {
|
|||
}
|
||||
|
||||
this.setState(this.state);
|
||||
setupTippy();
|
||||
} else if (res.op == UserOperation.CreateComment) {
|
||||
let data = res.data as CommentResponse;
|
||||
|
||||
|
@ -382,6 +399,7 @@ export class Post extends Component<any, PostState> {
|
|||
let data = res.data as CommentResponse;
|
||||
saveCommentRes(data, this.state.comments);
|
||||
this.setState(this.state);
|
||||
setupTippy();
|
||||
} else if (res.op == UserOperation.CreateCommentLike) {
|
||||
let data = res.data as CommentResponse;
|
||||
createCommentLikeRes(data, this.state.comments);
|
||||
|
@ -394,10 +412,12 @@ export class Post extends Component<any, PostState> {
|
|||
let data = res.data as PostResponse;
|
||||
this.state.post = data.post;
|
||||
this.setState(this.state);
|
||||
setupTippy();
|
||||
} else if (res.op == UserOperation.SavePost) {
|
||||
let data = res.data as PostResponse;
|
||||
this.state.post = data.post;
|
||||
this.setState(this.state);
|
||||
setupTippy();
|
||||
} else if (res.op == UserOperation.EditCommunity) {
|
||||
let data = res.data as CommunityResponse;
|
||||
this.state.community = data.community;
|
||||
|
|
27
ui/src/components/private-message-form.tsx
vendored
27
ui/src/components/private-message-form.tsx
vendored
|
@ -1,4 +1,5 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Prompt } from 'inferno-router';
|
||||
import { Link } from 'inferno-router';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||
|
@ -26,6 +27,7 @@ import {
|
|||
toast,
|
||||
randomStr,
|
||||
setupTribute,
|
||||
setupTippy,
|
||||
} from '../utils';
|
||||
import Tribute from 'tributejs/src/Tribute.js';
|
||||
import autosize from 'autosize';
|
||||
|
@ -107,6 +109,7 @@ export class PrivateMessageForm extends Component<
|
|||
this.setState(this.state);
|
||||
autosize.update(textarea);
|
||||
});
|
||||
setupTippy();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -116,6 +119,10 @@ export class PrivateMessageForm extends Component<
|
|||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Prompt
|
||||
when={!this.state.loading && this.state.privateMessageForm.content}
|
||||
message={i18n.t('block_leaving')}
|
||||
/>
|
||||
<form onSubmit={linkEvent(this, this.handlePrivateMessageSubmit)}>
|
||||
{!this.props.privateMessage && (
|
||||
<div class="form-group row">
|
||||
|
@ -126,7 +133,7 @@ export class PrivateMessageForm extends Component<
|
|||
{this.state.recipient && (
|
||||
<div class="col-sm-10 form-control-plaintext">
|
||||
<Link
|
||||
className="text-info"
|
||||
className="text-body font-weight-bold"
|
||||
to={`/u/${this.state.recipient.name}`}
|
||||
>
|
||||
{this.state.recipient.avatar && showAvatars() && (
|
||||
|
@ -165,18 +172,28 @@ export class PrivateMessageForm extends Component<
|
|||
/>
|
||||
)}
|
||||
|
||||
<ul class="float-right list-inline mb-1 text-muted small font-weight-bold">
|
||||
<ul class="float-right list-inline mb-1 text-muted font-weight-bold">
|
||||
<li class="list-inline-item">
|
||||
<span
|
||||
onClick={linkEvent(this, this.handleShowDisclaimer)}
|
||||
class="pointer"
|
||||
data-tippy-content={i18n.t('disclaimer')}
|
||||
>
|
||||
{i18n.t('disclaimer')}
|
||||
<svg class={`icon icon-inline`}>
|
||||
<use xlinkHref="#icon-alert-triangle"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a href={markdownHelpUrl} target="_blank" class="text-muted">
|
||||
{i18n.t('formatting_help')}
|
||||
<a
|
||||
href={markdownHelpUrl}
|
||||
target="_blank"
|
||||
class="text-muted"
|
||||
title={i18n.t('formatting_help')}
|
||||
>
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-help-circle"></use>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
71
ui/src/components/private-message.tsx
vendored
71
ui/src/components/private-message.tsx
vendored
|
@ -63,7 +63,7 @@ export class PrivateMessage extends Component<
|
|||
</li>
|
||||
<li className="list-inline-item">
|
||||
<Link
|
||||
className="text-info"
|
||||
className="text-body font-weight-bold"
|
||||
to={
|
||||
this.mine
|
||||
? `/u/${message.recipient_name}`
|
||||
|
@ -100,7 +100,15 @@ export class PrivateMessage extends Component<
|
|||
className="pointer text-monospace"
|
||||
onClick={linkEvent(this, this.handleMessageCollapse)}
|
||||
>
|
||||
{this.state.collapsed ? '[+]' : '[-]'}
|
||||
{this.state.collapsed ? (
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-plus-square"></use>
|
||||
</svg>
|
||||
) : (
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-minus-square"></use>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -121,58 +129,85 @@ export class PrivateMessage extends Component<
|
|||
dangerouslySetInnerHTML={mdToHtml(this.messageUnlessRemoved)}
|
||||
/>
|
||||
)}
|
||||
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
||||
<ul class="list-inline mb-1 text-muted h5 font-weight-bold">
|
||||
{!this.mine && (
|
||||
<>
|
||||
<li className="list-inline-item">
|
||||
<li className="list-inline-item-action">
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleMarkRead)}
|
||||
>
|
||||
{message.read
|
||||
data-tippy-content={
|
||||
message.read
|
||||
? i18n.t('mark_as_unread')
|
||||
: i18n.t('mark_as_read')}
|
||||
: i18n.t('mark_as_read')
|
||||
}
|
||||
>
|
||||
<svg
|
||||
class={`icon icon-inline ${message.read &&
|
||||
'text-success'}`}
|
||||
>
|
||||
<use xlinkHref="#icon-check"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<li className="list-inline-item-action">
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleReplyClick)}
|
||||
data-tippy-content={i18n.t('reply')}
|
||||
>
|
||||
{i18n.t('reply')}
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-reply1"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
{this.mine && (
|
||||
<>
|
||||
<li className="list-inline-item">
|
||||
<li className="list-inline-item-action">
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleEditClick)}
|
||||
data-tippy-content={i18n.t('edit')}
|
||||
>
|
||||
{i18n.t('edit')}
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-edit"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<li className="list-inline-item-action">
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleDeleteClick)}
|
||||
>
|
||||
{!message.deleted
|
||||
data-tippy-content={
|
||||
!message.deleted
|
||||
? i18n.t('delete')
|
||||
: i18n.t('restore')}
|
||||
: i18n.t('restore')
|
||||
}
|
||||
>
|
||||
<svg
|
||||
class={`icon icon-inline ${message.deleted &&
|
||||
'text-danger'}`}
|
||||
>
|
||||
<use xlinkHref="#icon-trash"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
<li className="list-inline-item">•</li>
|
||||
<li className="list-inline-item">
|
||||
<li className="list-inline-item-action">
|
||||
<span
|
||||
className="pointer"
|
||||
onClick={linkEvent(this, this.handleViewSource)}
|
||||
data-tippy-content={i18n.t('view_source')}
|
||||
>
|
||||
{i18n.t('view_source')}
|
||||
<svg
|
||||
class={`icon icon-inline ${this.state.viewSource &&
|
||||
'text-success'}`}
|
||||
>
|
||||
<use xlinkHref="#icon-file-text"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
30
ui/src/components/sidebar.tsx
vendored
30
ui/src/components/sidebar.tsx
vendored
|
@ -84,26 +84,37 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||
<Link className="text-muted" to={`/c/${community.name}`}>
|
||||
/c/{community.name}
|
||||
</Link>
|
||||
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
||||
<ul class="list-inline mb-1 text-muted font-weight-bold">
|
||||
{this.canMod && (
|
||||
<>
|
||||
<li className="list-inline-item">
|
||||
<li className="list-inline-item-action">
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleEditClick)}
|
||||
data-tippy-content={i18n.t('edit')}
|
||||
>
|
||||
{i18n.t('edit')}
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-edit"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
{this.amCreator && (
|
||||
<li className="list-inline-item">
|
||||
<li className="list-inline-item-action">
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleDeleteClick)}
|
||||
>
|
||||
{!community.deleted
|
||||
data-tippy-content={
|
||||
!community.deleted
|
||||
? i18n.t('delete')
|
||||
: i18n.t('restore')}
|
||||
: i18n.t('restore')
|
||||
}
|
||||
>
|
||||
<svg
|
||||
class={`icon icon-inline ${community.deleted &&
|
||||
'text-danger'}`}
|
||||
>
|
||||
<use xlinkHref="#icon-trash"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
|
@ -193,7 +204,10 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||
<li class="list-inline-item">{i18n.t('mods')}: </li>
|
||||
{this.props.moderators.map(mod => (
|
||||
<li class="list-inline-item">
|
||||
<Link class="text-info" to={`/u/${mod.user_name}`}>
|
||||
<Link
|
||||
class="text-body font-weight-bold"
|
||||
to={`/u/${mod.user_name}`}
|
||||
>
|
||||
{mod.avatar && showAvatars() && (
|
||||
<img
|
||||
height="32"
|
||||
|
|
20
ui/src/components/site-form.tsx
vendored
20
ui/src/components/site-form.tsx
vendored
|
@ -1,4 +1,5 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Prompt } from 'inferno-router';
|
||||
import { Site, SiteForm as SiteFormI } from '../interfaces';
|
||||
import { WebSocketService } from '../services';
|
||||
import { capitalizeFirstLetter, randomStr, setupTribute } from '../utils';
|
||||
|
@ -59,6 +60,14 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<Prompt
|
||||
when={
|
||||
!this.state.loading &&
|
||||
(this.state.siteForm.name || this.state.siteForm.description)
|
||||
}
|
||||
message={i18n.t('block_leaving')}
|
||||
/>
|
||||
<form onSubmit={linkEvent(this, this.handleCreateSiteSubmit)}>
|
||||
<h5>{`${
|
||||
this.props.site
|
||||
|
@ -105,7 +114,10 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
|||
id="create-site-downvotes"
|
||||
type="checkbox"
|
||||
checked={this.state.siteForm.enable_downvotes}
|
||||
onChange={linkEvent(this, this.handleSiteEnableDownvotesChange)}
|
||||
onChange={linkEvent(
|
||||
this,
|
||||
this.handleSiteEnableDownvotesChange
|
||||
)}
|
||||
/>
|
||||
<label class="form-check-label" htmlFor="create-site-downvotes">
|
||||
{i18n.t('enable_downvotes')}
|
||||
|
@ -123,7 +135,10 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
|||
checked={this.state.siteForm.enable_nsfw}
|
||||
onChange={linkEvent(this, this.handleSiteEnableNsfwChange)}
|
||||
/>
|
||||
<label class="form-check-label" htmlFor="create-site-enable-nsfw">
|
||||
<label
|
||||
class="form-check-label"
|
||||
htmlFor="create-site-enable-nsfw"
|
||||
>
|
||||
{i18n.t('enable_nsfw')}
|
||||
</label>
|
||||
</div>
|
||||
|
@ -176,6 +191,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
15
ui/src/components/sort-select.tsx
vendored
15
ui/src/components/sort-select.tsx
vendored
|
@ -1,5 +1,6 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { SortType } from '../interfaces';
|
||||
import { sortingHelpUrl } from '../utils';
|
||||
import { i18n } from '../i18next';
|
||||
|
||||
interface SortSelectProps {
|
||||
|
@ -24,10 +25,11 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<select
|
||||
value={this.state.sort}
|
||||
onChange={linkEvent(this, this.handleSortChange)}
|
||||
class="custom-select custom-select-sm w-auto"
|
||||
class="custom-select custom-select-sm w-auto mr-2"
|
||||
>
|
||||
<option disabled>{i18n.t('sort_type')}</option>
|
||||
{!this.props.hideHot && (
|
||||
|
@ -41,6 +43,17 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
|
|||
<option value={SortType.TopYear}>{i18n.t('year')}</option>
|
||||
<option value={SortType.TopAll}>{i18n.t('all')}</option>
|
||||
</select>
|
||||
<a
|
||||
className="text-muted"
|
||||
href={sortingHelpUrl}
|
||||
target="_blank"
|
||||
title={i18n.t('sorting_help')}
|
||||
>
|
||||
<svg class={`icon icon-inline`}>
|
||||
<use xlinkHref="#icon-help-circle"></use>
|
||||
</svg>
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
81
ui/src/components/symbols.tsx
vendored
81
ui/src/components/symbols.tsx
vendored
|
@ -15,26 +15,90 @@ export class Symbols extends Component<any, any> {
|
|||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<defs>
|
||||
<symbol id="icon-alert-triangle" viewBox="0 0 24 24">
|
||||
<path d="M11.148 4.374c0.073-0.123 0.185-0.242 0.334-0.332 0.236-0.143 0.506-0.178 0.756-0.116s0.474 0.216 0.614 0.448l8.466 14.133c0.070 0.12 0.119 0.268 0.128 0.434-0.015 0.368-0.119 0.591-0.283 0.759-0.18 0.184-0.427 0.298-0.693 0.301l-16.937-0.001c-0.152-0.001-0.321-0.041-0.481-0.134-0.239-0.138-0.399-0.359-0.466-0.607s-0.038-0.519 0.092-0.745zM9.432 3.346l-8.47 14.14c-0.422 0.731-0.506 1.55-0.308 2.29s0.68 1.408 1.398 1.822c0.464 0.268 0.976 0.4 1.475 0.402h16.943c0.839-0.009 1.587-0.354 2.123-0.902s0.864-1.303 0.855-2.131c-0.006-0.536-0.153-1.044-0.406-1.474l-8.474-14.147c-0.432-0.713-1.11-1.181-1.854-1.363s-1.561-0.081-2.269 0.349c-0.429 0.26-0.775 0.615-1.012 1.014zM11 9v4c0 0.552 0.448 1 1 1s1-0.448 1-1v-4c0-0.552-0.448-1-1-1s-1 0.448-1 1zM12 18c0.552 0 1-0.448 1-1s-0.448-1-1-1-1 0.448-1 1 0.448 1 1 1z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-zap" viewBox="0 0 24 24">
|
||||
<path d="M11.585 5.26l-0.577 4.616c0.033 0.716 0.465 1.124 0.992 1.124h6.865l-6.45 7.74 0.577-4.616c-0.033-0.716-0.465-1.124-0.992-1.124h-6.865zM12.232 1.36l-10 12c-0.354 0.424-0.296 1.055 0.128 1.408 0.187 0.157 0.415 0.233 0.64 0.232h7.867l-0.859 6.876c-0.069 0.548 0.32 1.048 0.868 1.116 0.349 0.044 0.678-0.098 0.892-0.352l10-12c0.354-0.424 0.296-1.055-0.128-1.408-0.187-0.157-0.415-0.233-0.64-0.232h-7.867l0.859-6.876c0.069-0.548-0.32-1.048-0.868-1.116-0.349-0.044-0.678 0.098-0.892 0.352z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-heart" viewBox="0 0 24 24">
|
||||
<path d="M20.133 5.317c0.88 0.881 1.319 2.031 1.319 3.184s-0.44 2.303-1.319 3.182l-8.133 8.133-8.133-8.133c-0.879-0.879-1.318-2.029-1.318-3.183s0.439-2.304 1.318-3.183 2.029-1.318 3.183-1.318 2.304 0.439 3.183 1.318l1.060 1.060c0.391 0.391 1.024 0.391 1.414 0l1.062-1.062c0.879-0.879 2.029-1.318 3.182-1.317s2.303 0.44 3.182 1.319zM21.547 3.903c-1.269-1.269-2.934-1.904-4.596-1.905s-3.327 0.634-4.597 1.903l-0.354 0.355-0.353-0.353c-1.269-1.269-2.935-1.904-4.597-1.904s-3.328 0.635-4.597 1.904-1.904 2.935-1.904 4.597 0.635 3.328 1.904 4.597l8.84 8.84c0.391 0.391 1.024 0.391 1.414 0l8.84-8.84c1.269-1.269 1.904-2.934 1.905-4.596s-0.634-3.327-1.905-4.598z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-link" viewBox="0 0 24 24">
|
||||
<path d="M9.199 13.599c0.992 1.327 2.43 2.126 3.948 2.345s3.123-0.142 4.45-1.134c0.239-0.179 0.465-0.375 0.655-0.568l2.995-2.995c1.163-1.204 1.722-2.751 1.696-4.285s-0.639-3.061-1.831-4.211c-1.172-1.132-2.688-1.692-4.199-1.683-1.492 0.008-2.984 0.571-4.137 1.683l-1.731 1.721c-0.392 0.389-0.394 1.023-0.004 1.414s1.023 0.394 1.414 0.004l1.709-1.699c0.77-0.742 1.763-1.117 2.76-1.123 1.009-0.006 2.016 0.367 2.798 1.122 0.795 0.768 1.203 1.783 1.221 2.808s-0.355 2.054-1.11 2.836l-3.005 3.005c-0.114 0.116-0.263 0.247-0.428 0.37-0.885 0.662-1.952 0.902-2.967 0.756s-1.971-0.678-2.632-1.563c-0.331-0.442-0.957-0.533-1.4-0.202s-0.533 0.957-0.202 1.4zM14.801 10.401c-0.992-1.327-2.43-2.126-3.948-2.345s-3.124 0.142-4.451 1.134c-0.239 0.179-0.464 0.375-0.655 0.568l-2.995 2.995c-1.163 1.204-1.722 2.751-1.696 4.285s0.639 3.061 1.831 4.211c1.172 1.132 2.688 1.692 4.199 1.683 1.492-0.008 2.984-0.571 4.137-1.683l1.723-1.723c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0l-1.696 1.698c-0.77 0.742-1.763 1.117-2.76 1.123-1.009 0.006-2.016-0.367-2.798-1.122-0.795-0.768-1.203-1.783-1.221-2.808s0.355-2.054 1.11-2.836l3.005-3.005c0.114-0.116 0.263-0.247 0.428-0.37 0.885-0.662 1.952-0.902 2.967-0.756s1.971 0.678 2.632 1.563c0.331 0.442 0.957 0.533 1.4 0.202s0.533-0.957 0.202-1.4z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-minus-square" viewBox="0 0 24 24">
|
||||
<path d="M5 2c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v14c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h14c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-14c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879zM5 4h14c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707v14c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293h-14c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-14c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293zM8 13h8c0.552 0 1-0.448 1-1s-0.448-1-1-1h-8c-0.552 0-1 0.448-1 1s0.448 1 1 1z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-plus-square" viewBox="0 0 24 24">
|
||||
<path d="M5 2c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v14c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h14c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-14c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879zM5 4h14c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707v14c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293h-14c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-14c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293zM8 13h3v3c0 0.552 0.448 1 1 1s1-0.448 1-1v-3h3c0.552 0 1-0.448 1-1s-0.448-1-1-1h-3v-3c0-0.552-0.448-1-1-1s-1 0.448-1 1v3h-3c-0.552 0-1 0.448-1 1s0.448 1 1 1z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-help-circle" viewBox="0 0 24 24">
|
||||
<path d="M23 12c0-3.037-1.232-5.789-3.222-7.778s-4.741-3.222-7.778-3.222-5.789 1.232-7.778 3.222-3.222 4.741-3.222 7.778 1.232 5.789 3.222 7.778 4.741 3.222 7.778 3.222 5.789-1.232 7.778-3.222 3.222-4.741 3.222-7.778zM21 12c0 2.486-1.006 4.734-2.636 6.364s-3.878 2.636-6.364 2.636-4.734-1.006-6.364-2.636-2.636-3.878-2.636-6.364 1.006-4.734 2.636-6.364 3.878-2.636 6.364-2.636 4.734 1.006 6.364 2.636 2.636 3.878 2.636 6.364zM10.033 9.332c0.183-0.521 0.559-0.918 1.022-1.14s1.007-0.267 1.528-0.083c0.458 0.161 0.819 0.47 1.050 0.859 0.183 0.307 0.284 0.665 0.286 1.037 0 0.155-0.039 0.309-0.117 0.464-0.080 0.16-0.203 0.325-0.368 0.49-0.709 0.709-1.831 1.092-1.831 1.092-0.524 0.175-0.807 0.741-0.632 1.265s0.741 0.807 1.265 0.632c0 0 1.544-0.506 2.613-1.575 0.279-0.279 0.545-0.614 0.743-1.010 0.2-0.4 0.328-0.858 0.328-1.369-0.004-0.731-0.204-1.437-0.567-2.049-0.463-0.778-1.19-1.402-2.105-1.724-1.042-0.366-2.135-0.275-3.057 0.167s-1.678 1.238-2.044 2.28c-0.184 0.521 0.090 1.092 0.611 1.275s1.092-0.091 1.275-0.611zM12 18c0.552 0 1-0.448 1-1s-0.448-1-1-1-1 0.448-1 1 0.448 1 1 1z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-pin" viewBox="0 0 18 18">
|
||||
<path d="M15 2v-1h-12v1c0 0.552 0.448 1 1 1v8c-0.552 0-1 0.448-1 1v1h5v3c0 0.552 0.448 1 1 1s1-0.448 1-1v-3h5v-1c0-0.552-0.448-1-1-1v-8c0.552 0 1-0.448 1-1zM12 11h-6v-8h6v8z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-lock" viewBox="0 0 24 24">
|
||||
<path d="M5 12h14c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707v7c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293h-14c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-7c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293zM18 10v-3c0-1.657-0.673-3.158-1.757-4.243s-2.586-1.757-4.243-1.757-3.158 0.673-4.243 1.757-1.757 2.586-1.757 4.243v3h-1c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v7c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h14c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-7c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879zM8 10v-3c0-1.105 0.447-2.103 1.172-2.828s1.723-1.172 2.828-1.172 2.103 0.447 2.828 1.172 1.172 1.723 1.172 2.828v3z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-check" viewBox="0 0 24 24">
|
||||
<path d="M19.293 5.293l-10.293 10.293-4.293-4.293c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414l5 5c0.391 0.391 1.024 0.391 1.414 0l11-11c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-copy" viewBox="0 0 24 24">
|
||||
<path d="M11 8c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v9c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h9c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-9c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879zM11 10h9c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707v9c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293h-9c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-9c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293zM5 14h-1c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-9c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293h9c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707v1c0 0.552 0.448 1 1 1s1-0.448 1-1v-1c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879h-9c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v9c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h1c0.552 0 1-0.448 1-1s-0.448-1-1-1z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-more-vertical" viewBox="0 0 24 24">
|
||||
<path d="M14 12c0-0.552-0.225-1.053-0.586-1.414s-0.862-0.586-1.414-0.586-1.053 0.225-1.414 0.586-0.586 0.862-0.586 1.414 0.225 1.053 0.586 1.414 0.862 0.586 1.414 0.586 1.053-0.225 1.414-0.586 0.586-0.862 0.586-1.414zM14 5c0-0.552-0.225-1.053-0.586-1.414s-0.862-0.586-1.414-0.586-1.053 0.225-1.414 0.586-0.586 0.862-0.586 1.414 0.225 1.053 0.586 1.414 0.862 0.586 1.414 0.586 1.053-0.225 1.414-0.586 0.586-0.862 0.586-1.414zM14 19c0-0.552-0.225-1.053-0.586-1.414s-0.862-0.586-1.414-0.586-1.053 0.225-1.414 0.586-0.586 0.862-0.586 1.414 0.225 1.053 0.586 1.414 0.862 0.586 1.414 0.586 1.053-0.225 1.414-0.586 0.586-0.862 0.586-1.414z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-bell" viewBox="0 0 24 24">
|
||||
<path d="M17 8c0 4.011 0.947 6.52 1.851 8h-13.702c0.904-1.48 1.851-3.989 1.851-8 0-1.381 0.559-2.63 1.464-3.536s2.155-1.464 3.536-1.464 2.63 0.559 3.536 1.464 1.464 2.155 1.464 3.536zM19 8c0-1.933-0.785-3.684-2.050-4.95s-3.017-2.050-4.95-2.050-3.684 0.785-4.95 2.050-2.050 3.017-2.050 4.95c0 6.127-2.393 8.047-2.563 8.174-0.453 0.308-0.573 0.924-0.269 1.381 0.192 0.287 0.506 0.443 0.832 0.445h18c0.552 0 1-0.448 1-1 0-0.339-0.168-0.638-0.429-0.821-0.176-0.13-2.571-2.050-2.571-8.179zM12.865 20.498c-0.139 0.239-0.359 0.399-0.608 0.465s-0.52 0.037-0.759-0.101c-0.162-0.094-0.283-0.222-0.359-0.357-0.274-0.48-0.884-0.647-1.364-0.373s-0.647 0.884-0.373 1.364c0.25 0.439 0.623 0.823 1.093 1.096 0.716 0.416 1.535 0.501 2.276 0.304s1.409-0.678 1.824-1.394c0.277-0.478 0.114-1.090-0.363-1.367s-1.090-0.114-1.367 0.363z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-file-text" viewBox="0 0 24 24">
|
||||
<path d="M17.586 7h-2.586v-2.586zM20.707 7.293l-6-6c-0.092-0.092-0.202-0.166-0.324-0.217s-0.253-0.076-0.383-0.076h-8c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v16c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h12c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-12c0-0.276-0.112-0.526-0.293-0.707zM13 3v5c0 0.552 0.448 1 1 1h5v11c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293h-12c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-16c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293zM16 12h-8c-0.552 0-1 0.448-1 1s0.448 1 1 1h8c0.552 0 1-0.448 1-1s-0.448-1-1-1zM16 16h-8c-0.552 0-1 0.448-1 1s0.448 1 1 1h8c0.552 0 1-0.448 1-1s-0.448-1-1-1zM10 8h-2c-0.552 0-1 0.448-1 1s0.448 1 1 1h2c0.552 0 1-0.448 1-1s-0.448-1-1-1z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-eye" viewBox="0 0 24 24">
|
||||
<path d="M0.106 11.553c-0.136 0.274-0.146 0.603 0 0.894 0 0 0.396 0.789 1.12 1.843 0.451 0.656 1.038 1.432 1.757 2.218 0.894 0.979 2.004 1.987 3.319 2.8 1.595 0.986 3.506 1.692 5.698 1.692s4.103-0.706 5.698-1.692c1.315-0.813 2.425-1.821 3.319-2.8 0.718-0.786 1.306-1.562 1.757-2.218 0.724-1.054 1.12-1.843 1.12-1.843 0.136-0.274 0.146-0.603 0-0.894 0 0-0.396-0.789-1.12-1.843-0.451-0.656-1.038-1.432-1.757-2.218-0.894-0.979-2.004-1.987-3.319-2.8-1.595-0.986-3.506-1.692-5.698-1.692s-4.103 0.706-5.698 1.692c-1.315 0.813-2.425 1.821-3.319 2.8-0.719 0.786-1.306 1.561-1.757 2.218-0.724 1.054-1.12 1.843-1.12 1.843zM2.14 12c0.163-0.281 0.407-0.681 0.734-1.158 0.41-0.596 0.94-1.296 1.585-2.001 0.805-0.881 1.775-1.756 2.894-2.448 1.35-0.834 2.901-1.393 4.647-1.393s3.297 0.559 4.646 1.393c1.119 0.692 2.089 1.567 2.894 2.448 0.644 0.705 1.175 1.405 1.585 2.001 0.328 0.477 0.572 0.876 0.734 1.158-0.163 0.281-0.407 0.681-0.734 1.158-0.41 0.596-0.94 1.296-1.585 2.001-0.805 0.881-1.775 1.756-2.894 2.448-1.349 0.834-2.9 1.393-4.646 1.393s-3.297-0.559-4.646-1.393c-1.119-0.692-2.089-1.567-2.894-2.448-0.644-0.705-1.175-1.405-1.585-2.001-0.328-0.477-0.572-0.877-0.735-1.158zM16 12c0-1.104-0.449-2.106-1.172-2.828s-1.724-1.172-2.828-1.172-2.106 0.449-2.828 1.172-1.172 1.724-1.172 2.828 0.449 2.106 1.172 2.828 1.724 1.172 2.828 1.172 2.106-0.449 2.828-1.172 1.172-1.724 1.172-2.828zM14 12c0 0.553-0.223 1.051-0.586 1.414s-0.861 0.586-1.414 0.586-1.051-0.223-1.414-0.586-0.586-0.861-0.586-1.414 0.223-1.051 0.586-1.414 0.861-0.586 1.414-0.586 1.051 0.223 1.414 0.586 0.586 0.861 0.586 1.414z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-edit" viewBox="0 0 24 24">
|
||||
<path d="M11 3h-7c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v14c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h14c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-7c0-0.552-0.448-1-1-1s-1 0.448-1 1v7c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293h-14c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-14c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293h7c0.552 0 1-0.448 1-1s-0.448-1-1-1zM17.793 1.793l-9.5 9.5c-0.122 0.121-0.217 0.28-0.263 0.465l-1 4c-0.039 0.15-0.042 0.318 0 0.485 0.134 0.536 0.677 0.862 1.213 0.728l4-1c0.167-0.041 0.33-0.129 0.465-0.263l9.5-9.5c0.609-0.609 0.914-1.41 0.914-2.207s-0.305-1.598-0.914-2.207-1.411-0.915-2.208-0.915-1.598 0.305-2.207 0.914zM19.207 3.207c0.219-0.219 0.504-0.328 0.793-0.328s0.574 0.109 0.793 0.328 0.328 0.504 0.328 0.793-0.109 0.574-0.328 0.793l-9.304 9.304-2.114 0.529 0.529-2.114z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-edit-2" viewBox="0 0 24 24">
|
||||
<path d="M16.293 2.293l-13.5 13.5c-0.117 0.116-0.21 0.268-0.258 0.444l-1.5 5.5c-0.046 0.163-0.049 0.346 0 0.526 0.145 0.533 0.695 0.847 1.228 0.702l5.5-1.5c0.159-0.042 0.315-0.129 0.444-0.258l13.5-13.5c0.747-0.747 1.121-1.729 1.121-2.707s-0.374-1.96-1.121-2.707-1.729-1.121-2.707-1.121-1.96 0.374-2.707 1.121zM17.707 3.707c0.357-0.357 0.824-0.535 1.293-0.535s0.936 0.178 1.293 0.536 0.535 0.823 0.535 1.292-0.178 0.936-0.535 1.293l-13.312 13.312-3.556 0.97 0.97-3.555z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-trash" viewBox="0 0 24 24">
|
||||
<path d="M18 7v13c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293h-10c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-13zM17 5v-1c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879h-4c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v1h-4c-0.552 0-1 0.448-1 1s0.448 1 1 1h1v13c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h10c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-13h1c0.552 0 1-0.448 1-1s-0.448-1-1-1zM9 5v-1c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293h4c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707v1z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-reply1" viewBox="0 0 20 20">
|
||||
<path d="M19 16.685c0 0-2.225-9.732-11-9.732v-3.984l-7 6.573 7 6.69v-4.357c4.763-0.001 8.516 0.421 11 4.81z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-star" viewBox="0 0 24 24">
|
||||
<path d="M12.897 1.557c-0.092-0.189-0.248-0.352-0.454-0.454-0.495-0.244-1.095-0.041-1.339 0.454l-2.858 5.789-6.391 0.935c-0.208 0.029-0.411 0.127-0.571 0.291-0.386 0.396-0.377 1.029 0.018 1.414l4.623 4.503-1.091 6.362c-0.036 0.207-0.006 0.431 0.101 0.634 0.257 0.489 0.862 0.677 1.351 0.42l5.714-3.005 5.715 3.005c0.186 0.099 0.408 0.139 0.634 0.101 0.544-0.093 0.91-0.61 0.817-1.155l-1.091-6.362 4.623-4.503c0.151-0.146 0.259-0.344 0.292-0.572 0.080-0.546-0.298-1.054-0.845-1.134l-6.39-0.934zM12 4.259l2.193 4.444c0.151 0.305 0.436 0.499 0.752 0.547l4.906 0.717-3.549 3.457c-0.244 0.238-0.341 0.569-0.288 0.885l0.837 4.883-4.386-2.307c-0.301-0.158-0.647-0.148-0.931 0l-4.386 2.307 0.837-4.883c0.058-0.336-0.059-0.661-0.288-0.885l-3.549-3.457 4.907-0.718c0.336-0.049 0.609-0.26 0.752-0.546z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-message-square" viewBox="0 0 24 24">
|
||||
<path d="M22 15v-10c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879h-14c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v16c0 0.256 0.098 0.512 0.293 0.707 0.391 0.391 1.024 0.391 1.414 0l3.707-3.707h11.586c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121zM20 15c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293h-12c-0.276 0-0.526 0.112-0.707 0.293l-2.293 2.293v-13.586c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293h14c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-image" viewBox="0 0 24 24">
|
||||
<path d="M5 2c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v14c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h14c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-14c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879zM11 8.5c0-0.69-0.281-1.316-0.732-1.768s-1.078-0.732-1.768-0.732-1.316 0.281-1.768 0.732-0.732 1.078-0.732 1.768 0.281 1.316 0.732 1.768 1.078 0.732 1.768 0.732 1.316-0.281 1.768-0.732 0.732-1.078 0.732-1.768zM9 8.5c0 0.138-0.055 0.262-0.146 0.354s-0.216 0.146-0.354 0.146-0.262-0.055-0.354-0.146-0.146-0.216-0.146-0.354 0.055-0.262 0.146-0.354 0.216-0.146 0.354-0.146 0.262 0.055 0.354 0.146 0.146 0.216 0.146 0.354zM7.414 20l8.586-8.586 4 4v3.586c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293zM20 12.586l-3.293-3.293c-0.391-0.391-1.024-0.391-1.414 0l-10.644 10.644c-0.135-0.050-0.255-0.129-0.356-0.23-0.182-0.182-0.293-0.431-0.293-0.707v-14c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293h14c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-external-link" viewBox="0 0 24 24">
|
||||
<path d="M17 13v6c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293h-11c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-11c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293h6c0.552 0 1-0.448 1-1s-0.448-1-1-1h-6c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v11c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h11c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-6c0-0.552-0.448-1-1-1s-1 0.448-1 1zM10.707 14.707l9.293-9.293v3.586c0 0.552 0.448 1 1 1s1-0.448 1-1v-6c0-0.136-0.027-0.265-0.076-0.383s-0.121-0.228-0.216-0.323c-0.001-0.001-0.001-0.001-0.002-0.002-0.092-0.092-0.202-0.166-0.323-0.216-0.118-0.049-0.247-0.076-0.383-0.076h-6c-0.552 0-1 0.448-1 1s0.448 1 1 1h3.586l-9.293 9.293c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-coffee" viewBox="0 0 24 24">
|
||||
<title>coffee1</title>
|
||||
<path d="M17 19h-12c-0.553 0-1-0.447-1-1s0.447-1 1-1h12c0.553 0 1 0.447 1 1s-0.447 1-1 1z"></path>
|
||||
<path d="M17.5 5h-12.5v9c0 1.1 0.9 2 2 2h8c1.1 0 2-0.9 2-2v-2h0.5c1.93 0 3.5-1.57 3.5-3.5s-1.57-3.5-3.5-3.5zM15 14h-8v-7h8v7zM17.5 10h-1.5v-3h1.5c0.827 0 1.5 0.673 1.5 1.5s-0.673 1.5-1.5 1.5z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-rss" viewBox="0 0 32 32">
|
||||
<title>rss</title>
|
||||
<path d="M4.259 23.467c-2.35 0-4.259 1.917-4.259 4.252 0 2.349 1.909 4.244 4.259 4.244 2.358 0 4.265-1.895 4.265-4.244-0-2.336-1.907-4.252-4.265-4.252zM0.005 10.873v6.133c3.993 0 7.749 1.562 10.577 4.391 2.825 2.822 4.384 6.595 4.384 10.603h6.16c-0-11.651-9.478-21.127-21.121-21.127zM0.012 0v6.136c14.243 0 25.836 11.604 25.836 25.864h6.152c0-17.64-14.352-32-31.988-32z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-arrow-down" viewBox="0 0 26 28">
|
||||
<title>arrow-down</title>
|
||||
<path d="M25.172 13c0 0.531-0.219 1.047-0.578 1.406l-10.172 10.187c-0.375 0.359-0.891 0.578-1.422 0.578s-1.047-0.219-1.406-0.578l-10.172-10.187c-0.375-0.359-0.594-0.875-0.594-1.406s0.219-1.047 0.594-1.422l1.156-1.172c0.375-0.359 0.891-0.578 1.422-0.578s1.047 0.219 1.406 0.578l4.594 4.594v-11c0-1.094 0.906-2 2-2h2c1.094 0 2 0.906 2 2v11l4.594-4.594c0.359-0.359 0.875-0.578 1.406-0.578s1.047 0.219 1.422 0.578l1.172 1.172c0.359 0.375 0.578 0.891 0.578 1.422z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-arrow-up" viewBox="0 0 26 28">
|
||||
<title>arrow-up</title>
|
||||
<path d="M25.172 15.172c0 0.531-0.219 1.031-0.578 1.406l-1.172 1.172c-0.375 0.375-0.891 0.594-1.422 0.594s-1.047-0.219-1.406-0.594l-4.594-4.578v11c0 1.125-0.938 1.828-2 1.828h-2c-1.062 0-2-0.703-2-1.828v-11l-4.594 4.578c-0.359 0.375-0.875 0.594-1.406 0.594s-1.047-0.219-1.406-0.594l-1.172-1.172c-0.375-0.375-0.594-0.875-0.594-1.406s0.219-1.047 0.594-1.422l10.172-10.172c0.359-0.375 0.875-0.578 1.406-0.578s1.047 0.203 1.422 0.578l10.172 10.172c0.359 0.375 0.578 0.891 0.578 1.422z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-mail" viewBox="0 0 32 32">
|
||||
<title>mail</title>
|
||||
<path d="M28 5h-24c-2.209 0-4 1.792-4 4v13c0 2.209 1.791 4 4 4h24c2.209 0 4-1.791 4-4v-13c0-2.208-1.791-4-4-4zM2 10.25l6.999 5.25-6.999 5.25v-10.5zM30 22c0 1.104-0.898 2-2 2h-24c-1.103 0-2-0.896-2-2l7.832-5.875 4.368 3.277c0.533 0.398 1.166 0.6 1.8 0.6 0.633 0 1.266-0.201 1.799-0.6l4.369-3.277 7.832 5.875zM30 20.75l-7-5.25 7-5.25v10.5zM17.199 18.602c-0.349 0.262-0.763 0.4-1.199 0.4s-0.851-0.139-1.2-0.4l-12.8-9.602c0-1.103 0.897-2 2-2h24c1.102 0 2 0.897 2 2l-12.801 9.602z"></path>
|
||||
<symbol id="icon-mail" viewBox="0 0 24 24">
|
||||
<path d="M3 7.921l8.427 5.899c0.34 0.235 0.795 0.246 1.147 0l8.426-5.899v10.079c0 0.272-0.11 0.521-0.295 0.705s-0.433 0.295-0.705 0.295h-16c-0.272 0-0.521-0.11-0.705-0.295s-0.295-0.433-0.295-0.705zM1 5.983c0 0.010 0 0.020 0 0.030v11.987c0 0.828 0.34 1.579 0.88 2.12s1.292 0.88 2.12 0.88h16c0.828 0 1.579-0.34 2.12-0.88s0.88-1.292 0.88-2.12v-11.988c0-0.010 0-0.020 0-0.030-0.005-0.821-0.343-1.565-0.88-2.102-0.541-0.54-1.292-0.88-2.12-0.88h-16c-0.828 0-1.579 0.34-2.12 0.88-0.537 0.537-0.875 1.281-0.88 2.103zM20.894 5.554l-8.894 6.225-8.894-6.225c0.048-0.096 0.112-0.183 0.188-0.259 0.185-0.185 0.434-0.295 0.706-0.295h16c0.272 0 0.521 0.11 0.705 0.295 0.076 0.076 0.14 0.164 0.188 0.259z"></path>
|
||||
</symbol>
|
||||
<symbol
|
||||
id="icon-mouse"
|
||||
|
@ -81,15 +145,12 @@ export class Symbols extends Component<any, any> {
|
|||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-search" viewBox="0 0 32 32">
|
||||
<title>search</title>
|
||||
<path d="M31.008 27.231l-7.58-6.447c-0.784-0.705-1.622-1.029-2.299-0.998 1.789-2.096 2.87-4.815 2.87-7.787 0-6.627-5.373-12-12-12s-12 5.373-12 12 5.373 12 12 12c2.972 0 5.691-1.081 7.787-2.87-0.031 0.677 0.293 1.515 0.998 2.299l6.447 7.58c1.104 1.226 2.907 1.33 4.007 0.23s0.997-2.903-0.23-4.007zM12 20c-4.418 0-8-3.582-8-8s3.582-8 8-8 8 3.582 8 8-3.582 8-8 8z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-github" viewBox="0 0 32 32">
|
||||
<title>github</title>
|
||||
<path d="M16 0.395c-8.836 0-16 7.163-16 16 0 7.069 4.585 13.067 10.942 15.182 0.8 0.148 1.094-0.347 1.094-0.77 0-0.381-0.015-1.642-0.022-2.979-4.452 0.968-5.391-1.888-5.391-1.888-0.728-1.849-1.776-2.341-1.776-2.341-1.452-0.993 0.11-0.973 0.11-0.973 1.606 0.113 2.452 1.649 2.452 1.649 1.427 2.446 3.743 1.739 4.656 1.33 0.143-1.034 0.558-1.74 1.016-2.14-3.554-0.404-7.29-1.777-7.29-7.907 0-1.747 0.625-3.174 1.649-4.295-0.166-0.403-0.714-2.030 0.155-4.234 0 0 1.344-0.43 4.401 1.64 1.276-0.355 2.645-0.532 4.005-0.539 1.359 0.006 2.729 0.184 4.008 0.539 3.054-2.070 4.395-1.64 4.395-1.64 0.871 2.204 0.323 3.831 0.157 4.234 1.026 1.12 1.647 2.548 1.647 4.295 0 6.145-3.743 7.498-7.306 7.895 0.574 0.497 1.085 1.47 1.085 2.963 0 2.141-0.019 3.864-0.019 4.391 0 0.426 0.288 0.925 1.099 0.768 6.354-2.118 10.933-8.113 10.933-15.18 0-8.837-7.164-16-16-16z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-spinner" viewBox="0 0 32 32">
|
||||
<title>spinner</title>
|
||||
<path d="M16 32c-4.274 0-8.292-1.664-11.314-4.686s-4.686-7.040-4.686-11.314c0-3.026 0.849-5.973 2.456-8.522 1.563-2.478 3.771-4.48 6.386-5.791l1.344 2.682c-2.126 1.065-3.922 2.693-5.192 4.708-1.305 2.069-1.994 4.462-1.994 6.922 0 7.168 5.832 13 13 13s13-5.832 13-13c0-2.459-0.69-4.853-1.994-6.922-1.271-2.015-3.066-3.643-5.192-4.708l1.344-2.682c2.615 1.31 4.824 3.313 6.386 5.791 1.607 2.549 2.456 5.495 2.456 8.522 0 4.274-1.664 8.292-4.686 11.314s-7.040 4.686-11.314 4.686z"></path>
|
||||
</symbol>
|
||||
</defs>
|
||||
|
|
11
ui/src/components/user.tsx
vendored
11
ui/src/components/user.tsx
vendored
|
@ -37,6 +37,7 @@ import {
|
|||
createCommentLikeRes,
|
||||
createPostLikeFindRes,
|
||||
commentsToFlatNodes,
|
||||
setupTippy,
|
||||
} from '../utils';
|
||||
import { PostListing } from './post-listing';
|
||||
import { SortSelect } from './sort-select';
|
||||
|
@ -267,6 +268,7 @@ export class User extends Component<any, UserState> {
|
|||
SortType[this.state.sort]
|
||||
}`}
|
||||
target="_blank"
|
||||
title="RSS"
|
||||
>
|
||||
<svg class="icon mx-2 text-muted small">
|
||||
<use xlinkHref="#icon-rss">#</use>
|
||||
|
@ -357,10 +359,11 @@ export class User extends Component<any, UserState> {
|
|||
</ul>
|
||||
</h5>
|
||||
<div>
|
||||
{i18n.t('joined')} <MomentTime data={user} />
|
||||
{i18n.t('joined')} <MomentTime data={user} showAgo />
|
||||
</div>
|
||||
<div class="table-responsive mt-1">
|
||||
<table class="table table-bordered table-sm mt-2 mb-0">
|
||||
{/*
|
||||
<tr>
|
||||
<td class="text-center" colSpan={2}>
|
||||
{i18n.t('number_of_points', {
|
||||
|
@ -368,18 +371,23 @@ export class User extends Component<any, UserState> {
|
|||
})}
|
||||
</td>
|
||||
</tr>
|
||||
*/}
|
||||
<tr>
|
||||
{/*
|
||||
<td>
|
||||
{i18n.t('number_of_points', { count: user.post_score })}
|
||||
</td>
|
||||
*/}
|
||||
<td>
|
||||
{i18n.t('number_of_posts', { count: user.number_of_posts })}
|
||||
</td>
|
||||
{/*
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{i18n.t('number_of_points', { count: user.comment_score })}
|
||||
</td>
|
||||
*/}
|
||||
<td>
|
||||
{i18n.t('number_of_comments', {
|
||||
count: user.number_of_comments,
|
||||
|
@ -1033,6 +1041,7 @@ export class User extends Component<any, UserState> {
|
|||
document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`;
|
||||
window.scrollTo(0, 0);
|
||||
this.setState(this.state);
|
||||
setupTippy();
|
||||
} else if (res.op == UserOperation.EditComment) {
|
||||
let data = res.data as CommentResponse;
|
||||
editCommentRes(data, this.state.comments);
|
||||
|
|
4
ui/src/i18next.ts
vendored
4
ui/src/i18next.ts
vendored
|
@ -13,7 +13,8 @@ import { it } from './translations/it';
|
|||
import { fi } from './translations/fi';
|
||||
import { ca } from './translations/ca';
|
||||
import { fa } from './translations/fa';
|
||||
import { pt_BR } from './translations/pt_br';
|
||||
import { pt_BR } from './translations/pt_BR';
|
||||
import { ja } from './translations/ja';
|
||||
|
||||
// https://github.com/nimbusec-oss/inferno-i18next/blob/master/tests/T.test.js#L66
|
||||
const resources = {
|
||||
|
@ -31,6 +32,7 @@ const resources = {
|
|||
ca,
|
||||
fa,
|
||||
pt_BR,
|
||||
ja,
|
||||
};
|
||||
|
||||
function format(value: any, format: any, lng: any): any {
|
||||
|
|
2
ui/src/index.html
vendored
2
ui/src/index.html
vendored
|
@ -13,6 +13,8 @@
|
|||
<!-- Styles -->
|
||||
<link rel="stylesheet" type="text/css" href="/static/assets/css/tribute.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/static/assets/css/toastify.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/static/assets/css/selectr.min.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/static/assets/css/tippy.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/static/assets/css/themes/darkly.min.css" id="darkly" />
|
||||
<link rel="stylesheet" type="text/css" href="/static/assets/css/main.css" />
|
||||
|
||||
|
|
6
ui/src/interfaces.ts
vendored
6
ui/src/interfaces.ts
vendored
|
@ -93,6 +93,7 @@ export interface User {
|
|||
lang: string;
|
||||
avatar?: string;
|
||||
show_avatars: boolean;
|
||||
unreadCount?: number;
|
||||
}
|
||||
|
||||
export interface UserView {
|
||||
|
@ -155,6 +156,10 @@ export interface Post {
|
|||
deleted: boolean;
|
||||
locked: boolean;
|
||||
stickied: boolean;
|
||||
embed_title?: string;
|
||||
embed_description?: string;
|
||||
embed_html?: string;
|
||||
thumbnail_url?: string;
|
||||
nsfw: boolean;
|
||||
banned: boolean;
|
||||
banned_from_community: boolean;
|
||||
|
@ -207,6 +212,7 @@ export interface Comment {
|
|||
saved?: boolean;
|
||||
user_mention_id?: number; // For mention type
|
||||
recipient_id?: number;
|
||||
depth?: number;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
|
|
7
ui/src/services/UserService.ts
vendored
7
ui/src/services/UserService.ts
vendored
|
@ -7,9 +7,8 @@ import { Subject } from 'rxjs';
|
|||
export class UserService {
|
||||
private static _instance: UserService;
|
||||
public user: User;
|
||||
public sub: Subject<{ user: User; unreadCount: number }> = new Subject<{
|
||||
public sub: Subject<{ user: User }> = new Subject<{
|
||||
user: User;
|
||||
unreadCount: number;
|
||||
}>();
|
||||
|
||||
private constructor() {
|
||||
|
@ -32,7 +31,7 @@ export class UserService {
|
|||
this.user = undefined;
|
||||
Cookies.remove('jwt');
|
||||
setTheme();
|
||||
this.sub.next({ user: undefined, unreadCount: 0 });
|
||||
this.sub.next({ user: undefined });
|
||||
console.log('Logged out.');
|
||||
}
|
||||
|
||||
|
@ -45,7 +44,7 @@ export class UserService {
|
|||
if (this.user.theme != 'darkly') {
|
||||
setTheme(this.user.theme);
|
||||
}
|
||||
this.sub.next({ user: this.user, unreadCount: 0 });
|
||||
this.sub.next({ user: this.user });
|
||||
console.log(this.user);
|
||||
}
|
||||
|
||||
|
|
7
ui/src/services/WebSocketService.ts
vendored
7
ui/src/services/WebSocketService.ts
vendored
|
@ -61,6 +61,7 @@ export class WebSocketService {
|
|||
|
||||
private constructor() {
|
||||
this.ws = new ReconnectingWebSocket(wsUri);
|
||||
let firstConnect = true;
|
||||
|
||||
this.subject = Observable.create((obs: any) => {
|
||||
this.ws.onmessage = e => {
|
||||
|
@ -68,13 +69,19 @@ export class WebSocketService {
|
|||
};
|
||||
this.ws.onopen = () => {
|
||||
console.log(`Connected to ${wsUri}`);
|
||||
|
||||
if (UserService.Instance.user) {
|
||||
this.userJoin();
|
||||
}
|
||||
|
||||
if (!firstConnect) {
|
||||
let res: WebSocketJsonResponse = {
|
||||
reconnect: true,
|
||||
};
|
||||
obs.next(res);
|
||||
}
|
||||
|
||||
firstConnect = false;
|
||||
};
|
||||
}).pipe(share());
|
||||
}
|
||||
|
|
239
ui/src/translations/ca.ts
vendored
239
ui/src/translations/ca.ts
vendored
|
@ -1,239 +0,0 @@
|
|||
export const ca = {
|
||||
translation: {
|
||||
post: 'Publicar',
|
||||
remove_post: 'Eliminar publicació',
|
||||
no_posts: 'Sense publicacions.',
|
||||
create_a_post: 'Crear una publicació',
|
||||
create_post: 'Crear Publicació',
|
||||
number_of_posts: '{{count}} Publicacions',
|
||||
posts: 'Publicacions',
|
||||
related_posts: 'Aquestes publicacions podrien estar relacionades',
|
||||
cross_posts: 'Aquest link també ha sigut publicat en:',
|
||||
cross_post: 'cross-post',
|
||||
comments: 'Comentaris',
|
||||
number_of_comments: '{{count}} Comentaris',
|
||||
remove_comment: 'Eliminar Comentaris',
|
||||
communities: 'Comunitats',
|
||||
users: 'Usuaris',
|
||||
create_a_community: 'Crear una comunitat',
|
||||
create_community: 'Crear Comunitat',
|
||||
remove_community: 'Eliminar Comunitat',
|
||||
subscribed_to_communities: 'Subscrit a <1>comunitats</1>',
|
||||
trending_communities: '<1>Comunitats</1> en tendència',
|
||||
list_of_communities: 'Llista de comunitats',
|
||||
number_of_communities: '{{count}} Comunitats',
|
||||
community_reqs: 'minúscules, guió baix, i sense espais.',
|
||||
create_private_message: 'Crear Missatge Privat',
|
||||
send_secure_message: 'Enviar Missatge Segur',
|
||||
send_message: 'Enviar Missatge',
|
||||
message: 'Missatge',
|
||||
edit: 'editar',
|
||||
reply: 'respondre',
|
||||
cancel: 'Cancelar',
|
||||
preview: 'Previsualitzar',
|
||||
upload_image: 'pujar imatge',
|
||||
avatar: 'Avatar',
|
||||
upload_avatar: 'Pujar Avatar',
|
||||
show_avatars: 'Veure Avatares',
|
||||
formatting_help: 'Ajuda de format',
|
||||
view_source: 'veure font',
|
||||
unlock: 'desbloquejar',
|
||||
lock: 'bloquejar',
|
||||
sticky: 'fijat',
|
||||
unsticky: 'no fijat',
|
||||
link: 'link',
|
||||
archive_link: 'arxivar link',
|
||||
mod: 'moderador',
|
||||
mods: 'moderadores',
|
||||
moderates: 'Modera',
|
||||
settings: 'Configuració',
|
||||
remove_as_mod: 'eliminar com moderador',
|
||||
appoint_as_mod: 'designar com moderador',
|
||||
modlog: 'Historial de moderació',
|
||||
admin: 'administrador',
|
||||
admins: 'administradors',
|
||||
remove_as_admin: 'eliminar com administrador',
|
||||
appoint_as_admin: 'designar com administrador',
|
||||
remove: 'eliminar',
|
||||
removed: 'eliminat',
|
||||
locked: 'bloquejat',
|
||||
stickied: 'fijat',
|
||||
reason: 'Raó',
|
||||
mark_as_read: 'marcar com llegit',
|
||||
mark_as_unread: 'marcar com no llegit',
|
||||
delete: 'eliminar',
|
||||
deleted: 'eliminat',
|
||||
delete_account: 'Eliminar Compte',
|
||||
delete_account_confirm:
|
||||
'Avís: aquesta acció eliminarà permanentment la teva informació. Introdueix la teva contrasenya per a continuar',
|
||||
restore: 'restaurar',
|
||||
ban: 'expulsar',
|
||||
ban_from_site: 'expulsar del lloc',
|
||||
unban: 'admetre',
|
||||
unban_from_site: 'admetre al lloc',
|
||||
banned: 'expulsat',
|
||||
save: 'guardar',
|
||||
unsave: 'descartar',
|
||||
create: 'crear',
|
||||
creator: 'creador',
|
||||
username: "Nom d'Usuari",
|
||||
email_or_username: 'Correu o Usuari',
|
||||
number_of_users: '{{count}} Usuaris',
|
||||
number_of_subscribers: '{{count}} Subscriptors',
|
||||
number_of_points: '{{count}} Punts',
|
||||
number_online: '{{count}} Usauris En Línia',
|
||||
name: 'Nom',
|
||||
title: 'Titol',
|
||||
category: 'Categoria',
|
||||
subscribers: 'Suscriptors',
|
||||
both: 'Ambdos',
|
||||
saved: 'Guardat',
|
||||
unsubscribe: "Desubscriure's",
|
||||
subscribe: "Subscriure's",
|
||||
subscribed: 'Subscrit',
|
||||
prev: 'Anterior',
|
||||
next: 'Següent',
|
||||
sidebar: 'Descripció de la comunitat',
|
||||
sort_type: "Tipus d'orden",
|
||||
hot: 'Popular',
|
||||
new: 'Nou',
|
||||
top_day: 'El millor del dia',
|
||||
week: 'Setmana',
|
||||
month: 'Mes',
|
||||
year: 'Any',
|
||||
all: 'Tot',
|
||||
top: 'Millor',
|
||||
api: 'API',
|
||||
docs: 'Docs',
|
||||
inbox: "Bústia d'entrada",
|
||||
inbox_for: "Bústia d'entrada per a <1>{{user}}</1>",
|
||||
mark_all_as_read: 'marcar tot com llegit',
|
||||
type: 'Tipus',
|
||||
unread: 'No llegit',
|
||||
replies: 'Respostes',
|
||||
mentions: 'Menciones',
|
||||
reply_sent: 'Resposta enviada',
|
||||
message_sent: 'Missatge enviado',
|
||||
search: 'Buscar',
|
||||
overview: 'Resum',
|
||||
view: 'Vista',
|
||||
logout: 'Tancar sessió',
|
||||
login_sign_up: 'Iniciar sessió / Crear compte',
|
||||
login: 'Iniciar sessió',
|
||||
sign_up: 'Crear compte',
|
||||
notifications_error:
|
||||
"Notificacions d'escriptori no disponibles al teu navegador. Prova amb Firefox o Chrome.",
|
||||
unread_messages: 'Missatges no llegits',
|
||||
messages: 'Missatges',
|
||||
password: 'Contrasenya',
|
||||
verify_password: 'Verificar Contrasenya',
|
||||
old_password: 'Antiga Contrasenya',
|
||||
forgot_password: 'oblidí la meva contrasenya',
|
||||
reset_password_mail_sent: 'Enviar correu per a restablir la contrasenya.',
|
||||
password_change: 'Canvi de Contrasenya',
|
||||
new_password: 'Nueva Contrasenya',
|
||||
no_email_setup: 'Aquest servidor no ha activat correctament el correu.',
|
||||
email: 'Correu electrònic',
|
||||
matrix_user_id: 'Usuari Matricial',
|
||||
private_message_disclaimer:
|
||||
'Avís: Els missatges privats en Lemmy no són segurs. Sisplau creu un compte en <1>Riot.im</1> per a mensajeria segura.',
|
||||
send_notifications_to_email: 'Enviar notificacions al correu',
|
||||
optional: 'Opcional',
|
||||
expires: 'Expira',
|
||||
language: 'Llenguatge',
|
||||
browser_default: 'Per defecte del navegador',
|
||||
downvotes_disabled: 'Vots negatius deshabilitats',
|
||||
enable_downvotes: 'Habilitar vots negatius',
|
||||
open_registration: 'Obrir registre',
|
||||
registration_closed: 'Registre tancat',
|
||||
enable_nsfw: 'Habilitar NSFW',
|
||||
url: 'URL',
|
||||
body: 'Descripció',
|
||||
copy_suggested_title: 'Copiar el títol sugerido: {{title}}',
|
||||
community: 'Comunitat',
|
||||
expand_here: 'Expandir ací',
|
||||
subscribe_to_communities: "Subscriure's a algunes <1>comunitats</1>.",
|
||||
chat: 'Chat',
|
||||
recent_comments: 'Comentaris recients',
|
||||
no_results: 'Sense resultats.',
|
||||
setup: 'Configurar',
|
||||
lemmy_instance_setup: "Configuració d'instancia de Lemmy",
|
||||
setup_admin: 'Configurar administrador del Lloc',
|
||||
your_site: 'el teu lloc',
|
||||
modified: 'modificat',
|
||||
nsfw: 'NSFW',
|
||||
show_nsfw: 'Mostrar contingut NSFW',
|
||||
theme: 'Tema',
|
||||
sponsors: 'Patrocinadors',
|
||||
sponsors_of_lemmy: 'Patrocinadors de Lemmy',
|
||||
sponsor_message:
|
||||
'Lemmy és programari lliure i de <1>codi obert</1>, la qual cosa significa que no tindrà publicitats, monetització, ni capitals emprenedors, mai. Les teves donacions secunden directament el desenvolupament a temps complet del projecte. Moltes gràcies a les següents persones:',
|
||||
support_on_patreon: 'Suport a Patreon',
|
||||
donate_to_lemmy: 'Donar a Lemmy',
|
||||
donate: 'Donar',
|
||||
general_sponsors:
|
||||
'Los Patrocinadores Generales son aquellos que señaron entre $10 y $39 a Lemmy.',
|
||||
crypto: 'Crypto',
|
||||
bitcoin: 'Bitcoin',
|
||||
ethereum: 'Ethereum',
|
||||
monero: 'Monero',
|
||||
code: 'Codi',
|
||||
joined: 'Es va unir',
|
||||
by: 'per',
|
||||
to: 'a',
|
||||
from: 'des de',
|
||||
transfer_community: 'transferir comunitat',
|
||||
transfer_site: 'transferir lloc',
|
||||
are_you_sure: 'Ets segur?',
|
||||
yes: 'sí',
|
||||
no: 'no',
|
||||
powered_by: 'Impulsat per',
|
||||
landing_0:
|
||||
'Lemmy és un <1>agregador de links</1> / alternativa a reddit, amb la intenció de funcionar al <2>fedivers</2>.<3></3>És allotjable per un mateix (sense necessitat de grans companyies), té actualització en directe de cadenes de comentaris, i és petit (<4>~80kB</4>). Federar amb el sistema de xarxes ActivityPub forma part dels objectius del projecte. <5></5>Aquesta és una <6>versió beta molt prematura</6>, i actualment moltes de les característiques són trencades o falten. <7></7>Suggereix noves característiques o reporta errors <8>aquí</8>.<9></9>Fet amb <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
||||
not_logged_in: 'No has iniciat sessió.',
|
||||
logged_in: 'Has iniciat sessió.',
|
||||
community_ban: "Has sigut expulsat d'aquesta comunitat.",
|
||||
site_ban: "Has sigut expulsat d'aquest lloc.",
|
||||
couldnt_create_comment: "No s'ha pogut crear el comentari.",
|
||||
couldnt_like_comment: "No s'ha pogut donar m'agrada al comentari.",
|
||||
couldnt_update_comment: "No s'ha pogut actualitzar el comentari.",
|
||||
couldnt_save_comment: "No s'ha pogut guardar el comentari.",
|
||||
no_comment_edit_allowed: 'No tens permisos per a editar el comentari.',
|
||||
no_post_edit_allowed: 'No tens permisos per a editar la publicació.',
|
||||
no_community_edit_allowed: 'No tens permisos per a editar la comunitat.',
|
||||
couldnt_find_community: "No s'ha pogut trobar la comunitat.",
|
||||
couldnt_update_community: "No s'ha pogut actualitzar la comunitat.",
|
||||
community_already_exists: 'Aquesta comunitat ja existeix.',
|
||||
community_moderator_already_exists:
|
||||
'Aquest moderador de la comunitat ja existeix.',
|
||||
community_follower_already_exists:
|
||||
'Aquest seguidor de la comunitat ja existeix.',
|
||||
community_user_already_banned:
|
||||
'Aquest usuari de la comunitat ja fou expulsat.',
|
||||
couldnt_create_post: "No s'ha pogut crear la publicació.",
|
||||
couldnt_like_post: "No s'ha pogut donar m'agrada a la publicació.",
|
||||
couldnt_find_post: "No s'ha pogut trobar la publicació.",
|
||||
couldnt_get_posts: "No s'han pogut obtindre les publicacions.",
|
||||
couldnt_update_post: "No s'ha pogut actualitzar la publicació.",
|
||||
couldnt_save_post: "No s'ha pogut guardar la publicació.",
|
||||
no_slurs: 'Prohibit insultar.',
|
||||
not_an_admin: 'No és un administrador.',
|
||||
site_already_exists: 'El lloc ja existeix.',
|
||||
couldnt_update_site: "No s'ha pogut actualitzar el lloc.",
|
||||
couldnt_find_that_username_or_email:
|
||||
"No s'ha pogut trobar aquest nom de usuari o correu electrònic.",
|
||||
password_incorrect: 'Contrasenya incorrecta.',
|
||||
passwords_dont_match: 'Les contrasenyes no coincideixen.',
|
||||
admin_already_created: 'Ho sentim, ja hi ha un adminisitrador.',
|
||||
user_already_exists: "L'usuari ja existeix.",
|
||||
email_already_exists: 'El correu ja és en ús.',
|
||||
couldnt_update_user: "No s'ha pogut actualitzar l'usuari.",
|
||||
system_err_login:
|
||||
'Error del sistema. Intenti tancar sessió i ingressar de nou.',
|
||||
couldnt_create_private_message: "No s'ha pogut crear el missatge privat.",
|
||||
no_private_message_edit_allowed:
|
||||
'Sense permisos per a editar el missatge privat.',
|
||||
couldnt_update_private_message:
|
||||
"No s'ha pogut actualitzar el missatge privat.",
|
||||
},
|
||||
};
|
210
ui/src/translations/de.ts
vendored
210
ui/src/translations/de.ts
vendored
|
@ -1,210 +0,0 @@
|
|||
export const de = {
|
||||
translation: {
|
||||
post: 'post',
|
||||
remove_post: 'Beitrag löschen',
|
||||
no_posts: 'Keine Beiträge.',
|
||||
create_a_post: 'Einen Beitrag anlegen',
|
||||
create_post: 'Beitrag anlegen',
|
||||
number_of_posts: '{{count}} Beiträge',
|
||||
posts: 'Beiträge',
|
||||
related_posts: 'Diese Beiträge könnten verwandt sein',
|
||||
cross_posts: 'Dieser Link wurde auch veröffentlicht unter:',
|
||||
cross_post: 'Crosspost',
|
||||
comments: 'Kommentare',
|
||||
number_of_comments: '{{count}} Kommentare',
|
||||
remove_comment: 'Kommentar löschen',
|
||||
communities: 'Communities',
|
||||
users: 'Benutzer',
|
||||
create_a_community: 'Eine Gemeinschaft anlegen',
|
||||
create_community: 'Gemeinschaft anlegen',
|
||||
remove_community: 'Gemeinschaft entfernen',
|
||||
subscribed_to_communities: 'Abonnierte <1>communities</1>',
|
||||
trending_communities: 'Trending <1>communities</1>',
|
||||
list_of_communities: 'Liste von communities',
|
||||
number_of_communities: '{{count}} Communities',
|
||||
community_reqs: 'Kleinbuchstaben, Großbuchstaben und keine Leerzeichen.',
|
||||
edit: 'editieren',
|
||||
reply: 'antworten',
|
||||
cancel: 'Abbrechen',
|
||||
preview: 'Vorschau',
|
||||
upload_image: 'Bild hochladen',
|
||||
formatting_help: 'Formatierungshilfe',
|
||||
view_source: 'Quelle anzeigen',
|
||||
unlock: 'entsperren',
|
||||
lock: 'sperren',
|
||||
sticky: 'haftend',
|
||||
unsticky: 'nicht haftend',
|
||||
link: 'link',
|
||||
archive_link: 'Archiv-Link',
|
||||
mod: 'Moderator',
|
||||
mods: 'Moderatoren',
|
||||
moderates: 'Moderiert',
|
||||
settings: 'Einstellungen',
|
||||
remove_as_mod: 'Als Moderator entfernen',
|
||||
appoint_as_mod: 'Zum Moderator ernennen',
|
||||
modlog: 'Modlog',
|
||||
admin: 'Administrator',
|
||||
admins: 'Administratoren',
|
||||
remove_as_admin: 'Als Administrator entfernen',
|
||||
appoint_as_admin: 'Zum Administrator ernennen',
|
||||
remove: 'entfernen',
|
||||
removed: 'entfernt',
|
||||
locked: 'gesperrt',
|
||||
stickied: 'angeheftet',
|
||||
reason: 'Grund',
|
||||
mark_as_read: 'als gelesen markieren',
|
||||
mark_as_unread: 'als ungelesen markieren',
|
||||
delete: 'löschen',
|
||||
deleted: 'gelöscht',
|
||||
delete_account: 'Konto löschen',
|
||||
delete_account_confirm:
|
||||
'Achtung: Dadurch werden alle Ihre Daten dauerhaft gelöscht. Geben Sie zur Bestätigung Ihr Passwort ein.',
|
||||
restore: 'wiederherstellen',
|
||||
ban: 'bannen',
|
||||
ban_from_site: 'Von der Seite bannen',
|
||||
unban: 'entbannen',
|
||||
unban_from_site: 'Von der Seite entbannen',
|
||||
banned: 'gesperrt',
|
||||
save: 'speichern',
|
||||
unsave: 'nicht speichern',
|
||||
create: 'anlegen',
|
||||
creator: 'Ersteller',
|
||||
username: 'Benutzername',
|
||||
email_or_username: 'E-mail oder Username',
|
||||
number_of_users: '{{count}} Benutzer',
|
||||
number_of_subscribers: '{{count}} Abonnenten',
|
||||
number_of_points: '{{count}} Punkte',
|
||||
number_online: '{{count}} Benutzer online',
|
||||
name: 'Name',
|
||||
title: 'Titel',
|
||||
category: 'Kategorie',
|
||||
subscribers: 'Abonnenten',
|
||||
both: 'Beide',
|
||||
saved: 'Gespeichert',
|
||||
unsubscribe: 'Abbestellen',
|
||||
subscribe: 'Abonnieren',
|
||||
subscribed: 'Abonniert',
|
||||
prev: 'Zurück',
|
||||
next: 'Weiter',
|
||||
sidebar: 'Seitenleiste',
|
||||
sort_type: 'Sortieren nach',
|
||||
hot: 'Hot',
|
||||
new: 'Neu',
|
||||
top_day: 'Top täglich',
|
||||
week: 'Woche',
|
||||
month: 'Monat',
|
||||
year: 'Jahr',
|
||||
all: 'Alle',
|
||||
top: 'Top',
|
||||
api: 'API',
|
||||
inbox: 'Posteingang',
|
||||
inbox_for: 'Posteingang für <1>{{user}}</1>',
|
||||
mark_all_as_read: 'Alle als gelesen markieren',
|
||||
type: 'Typ',
|
||||
unread: 'Ungelesen',
|
||||
replies: 'Antworten',
|
||||
mentions: 'Erwähnung',
|
||||
reply_sent: 'Antwort gesendet',
|
||||
search: 'Suchen',
|
||||
overview: 'Übersicht',
|
||||
view: 'Ansicht',
|
||||
logout: 'Ausloggen',
|
||||
login_sign_up: 'Einloggen / Registrieren',
|
||||
notifications_error:
|
||||
'Desktop-Benachrichtigungen sind in deinem browser nicht verfügbar. Versuche Firefox oder Chrome.',
|
||||
unread_messages: 'Ungelesene Nachrichten',
|
||||
password: 'Passwort',
|
||||
verify_password: 'Passwort überprüfen',
|
||||
forgot_password: 'Passwort vergessen',
|
||||
reset_password_mail_sent:
|
||||
'Eine E-Mail wurde geschickt, um dein Passwort zurückzusetzen.',
|
||||
password_change: 'Passwort geändert',
|
||||
new_password: 'neues Passwort',
|
||||
no_email_setup: 'Dieser Server hat E-Mails nicht korrekt eingerichtet.',
|
||||
login: 'Einloggen',
|
||||
sign_up: 'Registrieren',
|
||||
email: 'E-Mail',
|
||||
optional: 'optional',
|
||||
expires: 'Ablaufdatum',
|
||||
language: 'Sprache',
|
||||
browser_default: 'Standard-Browser',
|
||||
url: 'URL',
|
||||
body: 'Text',
|
||||
copy_suggested_title: 'Vorgeschlagenen Titel übernehmen: {{title}}',
|
||||
community: 'Gemeinschaft',
|
||||
expand_here: 'hier erweitern',
|
||||
subscribe_to_communities: 'Abonniere ein paar <1>communities</1>.',
|
||||
chat: 'Chat',
|
||||
recent_comments: 'Neueste Kommentare',
|
||||
no_results: 'Keine Ergebnisse.',
|
||||
setup: 'Einrichten',
|
||||
lemmy_instance_setup: 'Lemmy Instanz Einrichten',
|
||||
setup_admin: 'Seiten Administrator konfigurieren',
|
||||
your_site: 'deine Seite',
|
||||
modified: 'verändert',
|
||||
nsfw: 'NSFW',
|
||||
show_nsfw: 'NSFW-Inhalte anzeigen',
|
||||
theme: 'Aussehen',
|
||||
sponsors: 'Sponsoren',
|
||||
sponsors_of_lemmy: 'Sponsoren von Lemmy',
|
||||
sponsor_message:
|
||||
'Lemmy ist freie <1>Open-Source</1> Software, also ohne Werbung, Monetarisierung oder Venturekapital, Punkt. Deine Spenden gehen direkt an die Vollzeit Entwicklung des Projekts. Vielen Dank an die folgenden Personen:',
|
||||
support_on_patreon: 'Auf Patreon unterstützen',
|
||||
support_on_liberapay: 'Auf Liberapay unterstützen',
|
||||
general_sponsors:
|
||||
'Allgemeine Sponsoren sind die, die zwischen $10 und $39 zu Lemmy beitragen.',
|
||||
crypto: 'Kryptowährung',
|
||||
bitcoin: 'Bitcoin',
|
||||
ethereum: 'Ethereum',
|
||||
monero: 'Monero',
|
||||
code: 'Code',
|
||||
joined: 'beigetreten',
|
||||
by: 'von',
|
||||
to: 'bis',
|
||||
transfer_community: 'Gemeinschaft übertragen',
|
||||
transfer_site: 'Transferseite',
|
||||
are_you_sure: 'Bist du sicher?',
|
||||
yes: 'Ja',
|
||||
no: 'Nein',
|
||||
powered_by: 'Bereitgestellt durch',
|
||||
landing_0:
|
||||
'Lemmy ist ein <1>Link-Aggregator</1> / Reddit Alternative im <2>Fediverse</2>.<3></3>Es ist selbst-hostbar, hat live-updates von Kommentar-threads und ist winzig (<4>~80kB</4>). Federation in das ActivityPub Netzwerk ist geplant. <5></5>Dies ist eine <6>sehr frühe Beta Version</6>, und viele Features funktionieren zurzeit nicht richtig oder fehlen. <7></7>Schlage neue Features vor oder melde Bugs <8>hier.</8><9></9>Gebaut mit <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
||||
not_logged_in: 'Nicht eingeloggt.',
|
||||
community_ban: 'Du wurdest von dieser Gemeinschaft gebannt.',
|
||||
site_ban: 'Du wurdest von dieser Seite gebannt',
|
||||
couldnt_create_comment: 'Konnte Kommentar nicht anlegen.',
|
||||
couldnt_like_comment: 'Konnte nicht liken.',
|
||||
couldnt_update_comment: 'Konnte Kommentar nicht aktualisieren.',
|
||||
couldnt_save_comment: 'Konnte Kommentar nicht speichern.',
|
||||
no_comment_edit_allowed: 'Keine Erlaubnis Kommentar zu editieren.',
|
||||
no_post_edit_allowed: 'Keine Erlaubnis Beitrag zu editieren.',
|
||||
no_community_edit_allowed: 'Keine Erlaubnis Gemeinschaft zu editieren.',
|
||||
couldnt_find_community: 'Konnte Gemeinschaft nicht finden.',
|
||||
couldnt_update_community: 'Konnte Gemeinschaft nicht aktualisieren.',
|
||||
community_already_exists: 'Gemeinschaft existiert bereits.',
|
||||
community_moderator_already_exists:
|
||||
'Gemeinschaft Moderator existiert bereits.',
|
||||
community_follower_already_exists:
|
||||
'Gemeinschaft Follower existiert bereits.',
|
||||
community_user_already_banned: 'Gemeinschaft Nutzer schon gebannt.',
|
||||
couldnt_create_post: 'Konnte Beitrag nicht anlegen.',
|
||||
couldnt_like_post: 'Konnte Beitrag nicht liken.',
|
||||
couldnt_find_post: 'Konnte Beitrag nicht finden.',
|
||||
couldnt_get_posts: 'Konnte Beiträge nicht holen.',
|
||||
couldnt_update_post: 'Konnte Beitrag nicht aktualisieren.',
|
||||
couldnt_save_post: 'Konnte Beitrag nicht speichern.',
|
||||
no_slurs: 'Keine Beleidigungen.',
|
||||
not_an_admin: 'Kein Administrator.',
|
||||
site_already_exists: 'Seite existiert bereits.',
|
||||
couldnt_update_site: 'Konnte Seite nicht aktualisieren.',
|
||||
couldnt_find_that_username_or_email:
|
||||
'Konnte Username oder E-Mail nicht finden.',
|
||||
password_incorrect: 'Passwort falsch.',
|
||||
passwords_dont_match: 'Passwörter stimmen nicht überein.',
|
||||
admin_already_created: 'Entschuldigung, es gibt schon einen Administrator.',
|
||||
user_already_exists: 'Nutzer existiert bereits.',
|
||||
couldnt_update_user: 'Konnte Nutzer nicht aktualisieren',
|
||||
system_err_login:
|
||||
'Systemfehler. Versuche dich aus- und wieder einzuloggen.',
|
||||
},
|
||||
};
|
240
ui/src/translations/en.ts
vendored
240
ui/src/translations/en.ts
vendored
|
@ -1,240 +0,0 @@
|
|||
export const en = {
|
||||
translation: {
|
||||
post: 'post',
|
||||
remove_post: 'Remove Post',
|
||||
no_posts: 'No Posts.',
|
||||
create_a_post: 'Create a post',
|
||||
create_post: 'Create Post',
|
||||
number_of_posts: '{{count}} Posts',
|
||||
posts: 'Posts',
|
||||
related_posts: 'These posts might be related',
|
||||
cross_posts: 'This link has also been posted to:',
|
||||
cross_post: 'cross-post',
|
||||
cross_posted_to: 'cross-posted to: ',
|
||||
comments: 'Comments',
|
||||
number_of_comments: '{{count}} Comments',
|
||||
remove_comment: 'Remove Comment',
|
||||
communities: 'Communities',
|
||||
users: 'Users',
|
||||
create_a_community: 'Create a community',
|
||||
create_community: 'Create Community',
|
||||
remove_community: 'Remove Community',
|
||||
subscribed_to_communities: 'Subscribed to <1>communities</1>',
|
||||
trending_communities: 'Trending <1>communities</1>',
|
||||
list_of_communities: 'List of communities',
|
||||
number_of_communities: '{{count}} Communities',
|
||||
community_reqs: 'lowercase, underscores, and no spaces.',
|
||||
create_private_message: 'Create Private Message',
|
||||
send_secure_message: 'Send Secure Message',
|
||||
send_message: 'Send Message',
|
||||
message: 'Message',
|
||||
edit: 'edit',
|
||||
reply: 'reply',
|
||||
cancel: 'Cancel',
|
||||
preview: 'Preview',
|
||||
upload_image: 'upload image',
|
||||
avatar: 'Avatar',
|
||||
upload_avatar: 'Upload Avatar',
|
||||
show_avatars: 'Show Avatars',
|
||||
formatting_help: 'formatting help',
|
||||
view_source: 'view source',
|
||||
unlock: 'unlock',
|
||||
lock: 'lock',
|
||||
sticky: 'sticky',
|
||||
unsticky: 'unsticky',
|
||||
link: 'link',
|
||||
archive_link: 'archive link',
|
||||
mod: 'mod',
|
||||
mods: 'mods',
|
||||
moderates: 'Moderates',
|
||||
settings: 'Settings',
|
||||
remove_as_mod: 'remove as mod',
|
||||
appoint_as_mod: 'appoint as mod',
|
||||
modlog: 'Modlog',
|
||||
admin: 'admin',
|
||||
admins: 'admins',
|
||||
remove_as_admin: 'remove as admin',
|
||||
appoint_as_admin: 'appoint as admin',
|
||||
remove: 'remove',
|
||||
removed: 'removed',
|
||||
locked: 'locked',
|
||||
stickied: 'stickied',
|
||||
reason: 'Reason',
|
||||
mark_as_read: 'mark as read',
|
||||
mark_as_unread: 'mark as unread',
|
||||
delete: 'delete',
|
||||
deleted: 'deleted',
|
||||
delete_account: 'Delete Account',
|
||||
delete_account_confirm:
|
||||
'Warning: this will permanently delete all your data. Enter your password to confirm.',
|
||||
restore: 'restore',
|
||||
ban: 'ban',
|
||||
ban_from_site: 'ban from site',
|
||||
unban: 'unban',
|
||||
unban_from_site: 'unban from site',
|
||||
banned: 'banned',
|
||||
save: 'save',
|
||||
unsave: 'unsave',
|
||||
create: 'create',
|
||||
creator: 'creator',
|
||||
username: 'Username',
|
||||
email_or_username: 'Email or Username',
|
||||
number_of_users: '{{count}} Users',
|
||||
number_of_subscribers: '{{count}} Subscribers',
|
||||
number_of_points: '{{count}} Points',
|
||||
number_online: '{{count}} Users Online',
|
||||
name: 'Name',
|
||||
title: 'Title',
|
||||
category: 'Category',
|
||||
subscribers: 'Subscribers',
|
||||
both: 'Both',
|
||||
saved: 'Saved',
|
||||
unsubscribe: 'Unsubscribe',
|
||||
subscribe: 'Subscribe',
|
||||
subscribed: 'Subscribed',
|
||||
prev: 'Prev',
|
||||
next: 'Next',
|
||||
sidebar: 'Sidebar',
|
||||
sort_type: 'Sort type',
|
||||
hot: 'Hot',
|
||||
new: 'New',
|
||||
old: 'Old',
|
||||
top_day: 'Top day',
|
||||
week: 'Week',
|
||||
month: 'Month',
|
||||
year: 'Year',
|
||||
all: 'All',
|
||||
top: 'Top',
|
||||
api: 'API',
|
||||
docs: 'Docs',
|
||||
inbox: 'Inbox',
|
||||
inbox_for: 'Inbox for <1>{{user}}</1>',
|
||||
mark_all_as_read: 'mark all as read',
|
||||
type: 'Type',
|
||||
unread: 'Unread',
|
||||
replies: 'Replies',
|
||||
mentions: 'Mentions',
|
||||
reply_sent: 'Reply sent',
|
||||
message_sent: 'Message sent',
|
||||
search: 'Search',
|
||||
overview: 'Overview',
|
||||
view: 'View',
|
||||
logout: 'Logout',
|
||||
login_sign_up: 'Login / Sign up',
|
||||
login: 'Login',
|
||||
sign_up: 'Sign Up',
|
||||
notifications_error:
|
||||
'Desktop notifications not available in your browser. Try Firefox or Chrome.',
|
||||
unread_messages: 'Unread Messages',
|
||||
messages: 'Messages',
|
||||
password: 'Password',
|
||||
verify_password: 'Verify Password',
|
||||
old_password: 'Old Password',
|
||||
forgot_password: 'forgot password',
|
||||
reset_password_mail_sent: 'Sent an Email to reset your password.',
|
||||
password_change: 'Password Change',
|
||||
new_password: 'New Password',
|
||||
no_email_setup: "This server hasn't correctly set up email.",
|
||||
email: 'Email',
|
||||
matrix_user_id: 'Matrix User',
|
||||
private_message_disclaimer:
|
||||
'Warning: Private messages in Lemmy are not secure. Please create an account on <1>Riot.im</1> for secure messaging.',
|
||||
send_notifications_to_email: 'Send notifications to Email',
|
||||
optional: 'Optional',
|
||||
expires: 'Expires',
|
||||
language: 'Language',
|
||||
browser_default: 'Browser Default',
|
||||
downvotes_disabled: 'Downvotes disabled',
|
||||
enable_downvotes: 'Enable Downvotes',
|
||||
open_registration: 'Open Registration',
|
||||
registration_closed: 'Registration closed',
|
||||
enable_nsfw: 'Enable NSFW',
|
||||
url: 'URL',
|
||||
body: 'Body',
|
||||
copy_suggested_title: 'copy suggested title: {{title}}',
|
||||
community: 'Community',
|
||||
expand_here: 'Expand here',
|
||||
subscribe_to_communities: 'Subscribe to some <1>communities</1>.',
|
||||
chat: 'Chat',
|
||||
recent_comments: 'Recent Comments',
|
||||
no_results: 'No results.',
|
||||
setup: 'Setup',
|
||||
lemmy_instance_setup: 'Lemmy Instance Setup',
|
||||
setup_admin: 'Set Up Site Administrator',
|
||||
your_site: 'your site',
|
||||
modified: 'modified',
|
||||
nsfw: 'NSFW',
|
||||
show_nsfw: 'Show NSFW content',
|
||||
theme: 'Theme',
|
||||
sponsors: 'Sponsors',
|
||||
sponsors_of_lemmy: 'Sponsors of Lemmy',
|
||||
sponsor_message:
|
||||
'Lemmy is free, <1>open-source</1> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people:',
|
||||
support_on_patreon: 'Support on Patreon',
|
||||
support_on_liberapay: 'Support on Liberapay',
|
||||
donate_to_lemmy: 'Donate to Lemmy',
|
||||
donate: 'Donate',
|
||||
general_sponsors:
|
||||
'General Sponsors are those that pledged $10 to $39 to Lemmy.',
|
||||
crypto: 'Crypto',
|
||||
bitcoin: 'Bitcoin',
|
||||
ethereum: 'Ethereum',
|
||||
monero: 'Monero',
|
||||
code: 'Code',
|
||||
joined: 'Joined',
|
||||
by: 'by',
|
||||
to: 'to',
|
||||
from: 'from',
|
||||
transfer_community: 'transfer community',
|
||||
transfer_site: 'transfer site',
|
||||
are_you_sure: 'are you sure?',
|
||||
yes: 'yes',
|
||||
no: 'no',
|
||||
powered_by: 'Powered by',
|
||||
landing_0:
|
||||
"Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It's self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
|
||||
not_logged_in: 'Not logged in.',
|
||||
logged_in: 'Logged in.',
|
||||
community_ban: 'You have been banned from this community.',
|
||||
site_ban: 'You have been banned from the site',
|
||||
couldnt_create_comment: "Couldn't create comment.",
|
||||
couldnt_like_comment: "Couldn't like comment.",
|
||||
couldnt_update_comment: "Couldn't update comment.",
|
||||
couldnt_save_comment: "Couldn't save comment.",
|
||||
couldnt_get_comments: "Couldn't get comments.",
|
||||
no_comment_edit_allowed: 'Not allowed to edit comment.',
|
||||
no_post_edit_allowed: 'Not allowed to edit post.',
|
||||
no_community_edit_allowed: 'Not allowed to edit community.',
|
||||
couldnt_find_community: "Couldn't find community.",
|
||||
couldnt_update_community: "Couldn't update Community.",
|
||||
community_already_exists: 'Community already exists.',
|
||||
community_moderator_already_exists: 'Community moderator already exists.',
|
||||
community_follower_already_exists: 'Community follower already exists.',
|
||||
community_user_already_banned: 'Community user already banned.',
|
||||
couldnt_create_post: "Couldn't create post.",
|
||||
post_title_too_long: 'Post title too long.',
|
||||
couldnt_like_post: "Couldn't like post.",
|
||||
couldnt_find_post: "Couldn't find post.",
|
||||
couldnt_get_posts: "Couldn't get posts",
|
||||
couldnt_update_post: "Couldn't update post",
|
||||
couldnt_save_post: "Couldn't save post.",
|
||||
no_slurs: 'No slurs.',
|
||||
not_an_admin: 'Not an admin.',
|
||||
site_already_exists: 'Site already exists.',
|
||||
couldnt_update_site: "Couldn't update site.",
|
||||
couldnt_find_that_username_or_email:
|
||||
"Couldn't find that username or email.",
|
||||
password_incorrect: 'Password incorrect.',
|
||||
passwords_dont_match: 'Passwords do not match.',
|
||||
admin_already_created: "Sorry, there's already an admin.",
|
||||
user_already_exists: 'User already exists.',
|
||||
email_already_exists: 'Email already exists.',
|
||||
couldnt_update_user: "Couldn't update user.",
|
||||
system_err_login: 'System error. Try logging out and back in.',
|
||||
couldnt_create_private_message: "Couldn't create private message.",
|
||||
no_private_message_edit_allowed: 'Not allowed to edit private message.',
|
||||
couldnt_update_private_message: "Couldn't update private message.",
|
||||
time: 'Time',
|
||||
action: 'Action',
|
||||
},
|
||||
};
|
177
ui/src/translations/eo.ts
vendored
177
ui/src/translations/eo.ts
vendored
|
@ -1,177 +0,0 @@
|
|||
export const eo = {
|
||||
translation: {
|
||||
post: 'Poŝti',
|
||||
remove_post: 'Fortiri Poŝton',
|
||||
no_posts: 'Ne Poŝtoj.',
|
||||
create_a_post: 'Verki Poŝton',
|
||||
create_post: 'Verki Poŝton',
|
||||
number_of_posts: '{{count}} Poŝtoj',
|
||||
posts: 'Poŝtoj',
|
||||
related_posts: 'Tiuj poŝtoj eble rilatas',
|
||||
cross_posts: 'Tiuj ligilo ankaŭ estas poŝtinta al:',
|
||||
cross_post: 'laŭapoŝto',
|
||||
comments: 'Komentoj',
|
||||
number_of_comments: '{{count}} Komentoj',
|
||||
remove_comment: 'Fortiri Komentojn',
|
||||
communities: 'Komunumoj',
|
||||
users: 'Uzantoj',
|
||||
create_a_community: 'Krei komunumon',
|
||||
create_community: 'Krei Komunumon',
|
||||
remove_community: 'Forigi Komunumon',
|
||||
subscribed_to_communities: 'Abonita al <1>komunumoj</1>',
|
||||
trending_communities: 'Furora <1>komunumoj</1>',
|
||||
list_of_communities: 'Listo de komunumoj',
|
||||
community_reqs: 'minusklaj leteroj, substrekoj, kaj ne spacetoj.',
|
||||
edit: 'redakti',
|
||||
reply: 'repliki',
|
||||
cancel: 'nuligi',
|
||||
unlock: 'malŝlosi',
|
||||
lock: 'ŝlosi',
|
||||
link: 'ligi',
|
||||
mod: 'moderanto',
|
||||
mods: 'moderantoj',
|
||||
moderates: 'Moderigas',
|
||||
settings: 'Agordoj',
|
||||
remove_as_mod: 'forigi per moderanto',
|
||||
appoint_as_mod: 'nomumi per moderanto',
|
||||
modlog: 'Moderlogo',
|
||||
admin: 'administranto',
|
||||
admins: 'administrantoj',
|
||||
remove_as_admin: 'forigi per administranto',
|
||||
appoint_as_admin: 'nomumi per administranto',
|
||||
remove: 'fortiri',
|
||||
removed: 'fortirita',
|
||||
locked: 'ŝlosita',
|
||||
reason: 'Kialo',
|
||||
mark_as_read: 'marki kiel legita',
|
||||
mark_as_unread: 'marki kiel nelegita',
|
||||
delete: 'forigi',
|
||||
deleted: 'forigita',
|
||||
restore: 'restaŭri',
|
||||
ban: 'forbari',
|
||||
ban_from_site: 'forbari de retejo',
|
||||
unban: 'malforbari',
|
||||
unban_from_site: 'malforbari de retejo',
|
||||
save: 'konservi',
|
||||
unsave: 'malkonservi',
|
||||
create: 'krei',
|
||||
username: 'Uzantnomo',
|
||||
email_or_username: 'Retadreso aŭ Uzantnomo',
|
||||
number_of_users: '{{count}} Uzantoj',
|
||||
number_of_subscribers: '{{count}} Abonantoj',
|
||||
number_of_points: '{{count}} Voĉdonoj',
|
||||
name: 'Nomo',
|
||||
title: 'Titolo',
|
||||
category: 'Kategorio',
|
||||
subscribers: 'Abonantoj',
|
||||
both: 'Ambaŭ',
|
||||
saved: 'Konservita',
|
||||
unsubscribe: 'Malaboni',
|
||||
subscribe: 'Aboni',
|
||||
subscribed: 'Abonita',
|
||||
prev: 'Antaŭe',
|
||||
next: 'Poste',
|
||||
sidebar: 'Flankstango',
|
||||
sort_type: 'Klasi per kia',
|
||||
hot: 'Varmaj',
|
||||
new: 'Novaj',
|
||||
top_day: 'Supraj tagaj',
|
||||
week: 'Semajno',
|
||||
month: 'Monato',
|
||||
year: 'Jaro',
|
||||
all: 'Ĉiam',
|
||||
top: 'Supraj',
|
||||
api: 'API',
|
||||
inbox: 'Ricevujo',
|
||||
inbox_for: 'Ricevujo de <1>{{user}}</1>',
|
||||
mark_all_as_read: 'marki ĉiujn kiel legitaj',
|
||||
type: 'Tipo',
|
||||
unread: 'Nelegitaj',
|
||||
reply_sent: 'Repliko sendis',
|
||||
search: 'Serĉi',
|
||||
overview: 'Resumo',
|
||||
view: 'Rigardi',
|
||||
logout: 'Elsaluti',
|
||||
login_sign_up: 'Ensaluti / Registriĝi',
|
||||
login: 'Ensaluti',
|
||||
sign_up: 'Registriĝi',
|
||||
notifications_error:
|
||||
'Labortablaj avizoj estas nehavebla en via retumilo. Provu Firefox-on aŭ Chrome-on.',
|
||||
unread_messages: 'Nelegitaj Mesaĝoj',
|
||||
password: 'Pasvorto',
|
||||
verify_password: 'Konfirmu Vian Pasvorton',
|
||||
email: 'Retadreso',
|
||||
optional: 'Fakultativa',
|
||||
expires: 'Finiĝos',
|
||||
url: 'URL',
|
||||
body: 'Ĉefparto',
|
||||
copy_suggested_title: 'kopii la sugestiitan titolon: {{title}}',
|
||||
community: 'Komunumo',
|
||||
expand_here: 'Ekspansii ĉi tie',
|
||||
subscribe_to_communities: 'Aboni al iuj <1>komunumoj</1>.',
|
||||
chat: 'Babilo',
|
||||
recent_comments: 'Freŝaj Komentoj',
|
||||
no_results: 'Ne rezultoj.',
|
||||
setup: 'Agordi',
|
||||
lemmy_instance_setup: 'Agordi Instancon de Lemmy',
|
||||
setup_admin: 'Agordi Retejan Administranton',
|
||||
your_site: 'via retejo',
|
||||
modified: 'modifita',
|
||||
nsfw: 'NSFW',
|
||||
show_nsfw: 'Vidigi NSFW-an enhavon',
|
||||
sponsors: 'Subtenantoj',
|
||||
sponsors_of_lemmy: 'Subtenantoj de Lemmy',
|
||||
sponsor_message:
|
||||
'Lemmy estas senpaga, <1>liberkoda</1> programaro. Tio signifas ne reklami, pagigi, aŭ riska kapitalo, ĉiam. Viaj donacoj rekte subtenas plentempan evoluon de la projekto. Dankon al tiuj homoj:',
|
||||
support_on_patreon: 'Subteni per Patreon',
|
||||
general_sponsors:
|
||||
'Ĝeneralaj Subtenantoj estas tiuj ke donacis inter $10 kaj $39 al Lemmy.',
|
||||
crypto: 'Crypto',
|
||||
bitcoin: 'Bitcoin',
|
||||
ethereum: 'Ethereum',
|
||||
monero: 'Monero',
|
||||
code: 'Kodo',
|
||||
joined: 'Unuiĝis',
|
||||
by: 'de',
|
||||
to: 'al',
|
||||
transfer_community: 'transdoni la komunumon',
|
||||
transfer_site: 'transdoni la retejon',
|
||||
powered_by: 'Konstruis per',
|
||||
landing_0:
|
||||
'Lemmy estas <1>ligila agregatilo</1> / Reddit anstataŭo ke intenciĝas funkci en la <2>federacio-universo</2>.<3></3>ĝi estas mem-gastigebla, havas nuna-ĝisdatigajn komentarojn, kaj estas malgrandega (<4>~80kB</4>). Federacio en la ActivityPub-an reton estas planizita. <5></5>Estas <6>fruega beta versio</6>, kaj multaj trajtoj estas nune difektaj aŭ mankaj. <7></7>Sugestias novajn trajtojn aŭ raportas cimojn <8>ĉi tie.</8><9></9>Faris per <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
||||
not_logged_in: 'Ne estas ensalutinta.',
|
||||
community_ban: 'Vi estas forbarita de la komunumo.',
|
||||
site_ban: 'Vi estas forbarita de la retejo',
|
||||
couldnt_create_comment: 'Ne povis krei la komenton.',
|
||||
couldnt_like_comment: 'Ne povis ŝati la komenton.',
|
||||
couldnt_update_comment: 'Ne povis ĝisdatigi komenton.',
|
||||
couldnt_save_comment: 'Ne povis konservi komenton.',
|
||||
no_comment_edit_allowed: 'Ne rajtas redakti la komenton.',
|
||||
no_post_edit_allowed: 'Ne rajtas redakti la poŝton.',
|
||||
no_community_edit_allowed: 'Ne rajtas redakti la komunumon.',
|
||||
couldnt_find_community: 'Ne povis trovi la komunumon.',
|
||||
couldnt_update_community: 'Ne povis ĝisdatigi la komunumon.',
|
||||
community_already_exists: 'Komunumo jam ekzistas.',
|
||||
community_moderator_already_exists: 'Komunuma moderanto jam ekzistas.',
|
||||
community_follower_already_exists: 'Komunuma sekvanto.',
|
||||
community_user_already_banned: 'Komunuma uzanto jam estas forbarita.',
|
||||
couldnt_create_post: 'Ne povis krei la poŝton.',
|
||||
couldnt_like_post: 'Ne povis ŝati la poŝton.',
|
||||
couldnt_find_post: 'Ne povis trovi la poŝton.',
|
||||
couldnt_get_posts: 'Ne povis irpreni poŝtojn',
|
||||
couldnt_update_post: 'Ne povis ĝisdatigi la poŝton',
|
||||
couldnt_save_post: 'Ne povis konservi la poŝton.',
|
||||
no_slurs: 'Ne bigotaj vortoj.',
|
||||
not_an_admin: 'Ne estas administranto.',
|
||||
site_already_exists: 'Retejo jam ekzistas.',
|
||||
couldnt_update_site: 'Ne povis ĝisdatigi la retejon.',
|
||||
couldnt_find_that_username_or_email:
|
||||
'Ne povis trovi tiun uzantnomon aŭ retadreson.',
|
||||
password_incorrect: 'Pasvorto malĝustas.',
|
||||
passwords_dont_match: 'Pasvortoj ne samas.',
|
||||
admin_already_created: 'Pardonu, jam estas administranto.',
|
||||
user_already_exists: 'Uzanto jam ekzistas.',
|
||||
couldnt_update_user: 'Ne povis ĝisdatigi la uzanton.',
|
||||
system_err_login: 'Sistema eraro. Provu elsaluti kaj ensaluti.',
|
||||
},
|
||||
};
|
242
ui/src/translations/es.ts
vendored
242
ui/src/translations/es.ts
vendored
|
@ -1,242 +0,0 @@
|
|||
export const es = {
|
||||
translation: {
|
||||
post: 'Publicar',
|
||||
remove_post: 'Eliminar publicación',
|
||||
no_posts: 'Sin publicaciones.',
|
||||
create_a_post: 'Crear una publicación',
|
||||
create_post: 'Crear Publicación',
|
||||
number_of_posts: '{{count}} Publicaciones',
|
||||
posts: 'Publicaciones',
|
||||
related_posts: 'Estas publicaciones podrían estar relacionadas',
|
||||
cross_posts: 'Este link también ha sido publicado en:',
|
||||
cross_post: 'cross-post',
|
||||
comments: 'Comentarios',
|
||||
number_of_comments: '{{count}} Comentarios',
|
||||
remove_comment: 'Eliminar Comentarios',
|
||||
communities: 'Comunidades',
|
||||
users: 'Usuarios',
|
||||
create_a_community: 'Crear una comunidad',
|
||||
create_community: 'Crear Comunidad',
|
||||
remove_community: 'Eliminar Comunidad',
|
||||
subscribed_to_communities: 'Suscrito a <1>comunidades</1>',
|
||||
trending_communities: '<1>Comunidades</1> en tendencia',
|
||||
list_of_communities: 'Lista de comunidades',
|
||||
number_of_communities: '{{count}} Comunidades',
|
||||
community_reqs: 'minúsculas, guión bajo, y sin espacios.',
|
||||
create_private_message: 'Crear Mensaje Privado',
|
||||
send_secure_message: 'Enviar Mensaje Seguro',
|
||||
send_message: 'Enviar Mensaje',
|
||||
message: 'Mensaje',
|
||||
edit: 'editar',
|
||||
reply: 'responder',
|
||||
cancel: 'Cancelar',
|
||||
preview: 'Previsualizar',
|
||||
upload_image: 'subir imagen',
|
||||
avatar: 'Avatar',
|
||||
upload_avatar: 'Subir Avatar',
|
||||
show_avatars: 'Ver Avatares',
|
||||
formatting_help: 'Ayuda de formato',
|
||||
view_source: 'ver fuente',
|
||||
unlock: 'desbloquear',
|
||||
lock: 'bloquear',
|
||||
sticky: 'fijado',
|
||||
unsticky: 'no fijado',
|
||||
link: 'link',
|
||||
archive_link: 'archivar link',
|
||||
mod: 'moderador',
|
||||
mods: 'moderadores',
|
||||
moderates: 'Modera',
|
||||
settings: 'Configuración',
|
||||
remove_as_mod: 'eliminar como moderador',
|
||||
appoint_as_mod: 'designar como moderador',
|
||||
modlog: 'Historial de moderación',
|
||||
admin: 'administrador',
|
||||
admins: 'administradores',
|
||||
remove_as_admin: 'eliminar como administrador',
|
||||
appoint_as_admin: 'designar como administrador',
|
||||
remove: 'eliminar',
|
||||
removed: 'eliminado',
|
||||
locked: 'bloqueado',
|
||||
stickied: 'fijado',
|
||||
reason: 'Razón',
|
||||
mark_as_read: 'marcar como leído',
|
||||
mark_as_unread: 'marcar como no leído',
|
||||
delete: 'eliminar',
|
||||
deleted: 'eliminado',
|
||||
delete_account: 'Eliminar Cuenta',
|
||||
delete_account_confirm:
|
||||
'Aviso: esta acción eliminará permanentemente tu información. Introduce tu contraseña para continuar',
|
||||
restore: 'restaurar',
|
||||
ban: 'expulsar',
|
||||
ban_from_site: 'expulsar del sitio',
|
||||
unban: 'admitir',
|
||||
unban_from_site: 'admitir en el sitio',
|
||||
banned: 'expulsado',
|
||||
save: 'guardar',
|
||||
unsave: 'descartar',
|
||||
create: 'crear',
|
||||
creator: 'creador',
|
||||
username: 'Nombre de Usuario',
|
||||
email_or_username: 'Correo o Usuario',
|
||||
number_of_users: '{{count}} Usuarios',
|
||||
number_of_subscribers: '{{count}} Suscriptores',
|
||||
number_of_points: '{{count}} Puntos',
|
||||
number_online: '{{count}} Usuarios En Línea',
|
||||
name: 'Nombre',
|
||||
title: 'Titulo',
|
||||
category: 'Categoría',
|
||||
subscribers: 'Suscriptores',
|
||||
both: 'Ambos',
|
||||
saved: 'Guardado',
|
||||
unsubscribe: 'Desuscribirse',
|
||||
subscribe: 'Suscribirse',
|
||||
subscribed: 'Suscrito',
|
||||
prev: 'Anterior',
|
||||
next: 'Siguiente',
|
||||
sidebar: 'Descripción de la comunidad',
|
||||
sort_type: 'Tipo de orden',
|
||||
hot: 'Popular',
|
||||
new: 'Nuevo',
|
||||
top_day: 'Lo mejor del día',
|
||||
week: 'Semana',
|
||||
month: 'Mes',
|
||||
year: 'Año',
|
||||
all: 'Todo',
|
||||
top: 'Mejor',
|
||||
api: 'API',
|
||||
docs: 'Docs',
|
||||
inbox: 'Buzón de entrada',
|
||||
inbox_for: 'Buzón de entrada para <1>{{user}}</1>',
|
||||
mark_all_as_read: 'marcar todo como leído',
|
||||
type: 'Tipo',
|
||||
unread: 'No leído',
|
||||
replies: 'Respuestas',
|
||||
mentions: 'Menciones',
|
||||
reply_sent: 'Respuesta enviada',
|
||||
message_sent: 'Mensaje enviado',
|
||||
search: 'Buscar',
|
||||
overview: 'Resumen',
|
||||
view: 'Vista',
|
||||
logout: 'Cerrar sesión',
|
||||
login_sign_up: 'Iniciar sesión / Crear cuenta',
|
||||
login: 'Iniciar sesión',
|
||||
sign_up: 'Crear cuenta',
|
||||
notifications_error:
|
||||
'Notificaciones de escritorio no disponibles en tu navegador. Prueba Firefox o Chrome.',
|
||||
unread_messages: 'Mensajes no leídos',
|
||||
messages: 'Mensajes',
|
||||
password: 'Contraseña',
|
||||
verify_password: 'Verificar contraseña',
|
||||
old_password: 'Antigua Contraseña',
|
||||
forgot_password: 'olvidé mi contraseña',
|
||||
reset_password_mail_sent: 'Enviar correo para reestablecer la contraseña.',
|
||||
password_change: 'Cambio de Contraseña',
|
||||
new_password: 'Nueva Contraseña',
|
||||
no_email_setup: 'Este servidor no ha activado correctamente el correo.',
|
||||
email: 'Correo electrónico',
|
||||
matrix_user_id: 'Usuario Matricial',
|
||||
private_message_disclaimer:
|
||||
'Aviso: Los mensajes privados en Lemmy no son seguros. Por favor cree una cuenta en <1>Riot.im</1> para mensajeria segura.',
|
||||
send_notifications_to_email: 'Enviar notificaciones al correo',
|
||||
optional: 'Opcional',
|
||||
expires: 'Expira',
|
||||
language: 'Idioma',
|
||||
browser_default: 'Por defecto del navegador',
|
||||
downvotes_disabled: 'Votos negativos deshabilitados',
|
||||
enable_downvotes: 'Habilitar votos negativos',
|
||||
open_registration: 'Abrir registro',
|
||||
registration_closed: 'Registro cerrado',
|
||||
enable_nsfw: 'Habilitar NSFW',
|
||||
url: 'URL',
|
||||
body: 'Descripción',
|
||||
copy_suggested_title: 'Copiar el título sugerido: {{title}}',
|
||||
community: 'Comunidad',
|
||||
expand_here: 'Expandir aquí',
|
||||
subscribe_to_communities: 'Suscribirse a algunas <1>comunidades</1>.',
|
||||
chat: 'Chat',
|
||||
recent_comments: 'Comentarios recientes',
|
||||
no_results: 'Sin resultados.',
|
||||
setup: 'Configurar',
|
||||
lemmy_instance_setup: 'Configuración de instancia de Lemmy',
|
||||
setup_admin: 'Configurar administrador del Sitio',
|
||||
your_site: 'tu sitio',
|
||||
modified: 'modificado',
|
||||
nsfw: 'NSFW',
|
||||
show_nsfw: 'Mostrar contenido NSFW',
|
||||
theme: 'Tema',
|
||||
sponsors: 'Patrocinadores',
|
||||
sponsors_of_lemmy: 'Patrocinadores de Lemmy',
|
||||
sponsor_message:
|
||||
'Lemmy es software libre y de <1>código abierto</1>, lo que significa que no tendrá publicidades, monetización, ni capitales emprendedores, nunca. Tus donaciones apoyan directamente el desarrollo a tiempo completo del proyecto. Muchas gracias a las siguientes personas:',
|
||||
support_on_patreon: 'Apoyo en Patreon',
|
||||
support_on_liberapay: 'Apoyo en Liberapay',
|
||||
donate_to_lemmy: 'Donar a Lemmy',
|
||||
donate: 'Donar',
|
||||
general_sponsors:
|
||||
'Los Patrocinadores Generales son aquellos que señaron entre $10 y $39 a Lemmy.',
|
||||
crypto: 'Crypto',
|
||||
bitcoin: 'Bitcoin',
|
||||
ethereum: 'Ethereum',
|
||||
monero: 'Monero',
|
||||
code: 'Código',
|
||||
joined: 'Se unió',
|
||||
by: 'por',
|
||||
to: 'a',
|
||||
from: 'desde',
|
||||
transfer_community: 'transferir comunidad',
|
||||
transfer_site: 'transferir sitio',
|
||||
are_you_sure: '¿Estás seguro?',
|
||||
yes: 'sí',
|
||||
no: 'no',
|
||||
powered_by: 'Impulsado por',
|
||||
landing_0:
|
||||
'Lemmy es un <1>agregador de links</1> / alternativa a reddit, con la intención de funcionar en el <2>fediverso</2>.<3></3>Es alojable por uno mismo (sin necesidad de grandes compañías), tiene actualización en vivo de cadenas de comentarios, y es pequeño (<4>~80kB</4>). Federar con el sistema de redes ActivityPub forma parte de los objetivos del proyecto. <5></5>Esta es una <6>version beta muy prematura</6>, y actualmente muchas de las características están rotas o faltan. <7></7>Sugiere nuevas características o reporta errores <8>aquí</8>.<9></9>Hecho con <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
||||
not_logged_in: 'No has iniciado sesión.',
|
||||
logged_in: 'Has iniciado sesión.',
|
||||
community_ban: 'Has sido expulsado de esta comunidad.',
|
||||
site_ban: 'Has sido expulsado del sitio',
|
||||
couldnt_create_comment: 'No se pudo crear el comentario.',
|
||||
couldnt_like_comment: 'No se pudo dar me gusta al comentario.',
|
||||
couldnt_update_comment: 'No se pudo actualizar el comentario.',
|
||||
couldnt_save_comment: 'No se pudo guardar el comentario.',
|
||||
no_comment_edit_allowed: 'No tiene permisos para editar el comentario.',
|
||||
no_post_edit_allowed: 'No tiene permisos para editar la publicación.',
|
||||
no_community_edit_allowed: 'No tiene permisos para editar la comunidad.',
|
||||
couldnt_find_community: 'No se pudo encontrar la comunidad.',
|
||||
couldnt_update_community: 'No se pudo actualizar la comunidad.',
|
||||
community_already_exists: 'Esta comunidad ya existe.',
|
||||
community_moderator_already_exists:
|
||||
'Este moderador de la comunidad ya existe.',
|
||||
community_follower_already_exists:
|
||||
'Este seguidor de la comunidad ya existe.',
|
||||
community_user_already_banned:
|
||||
'Este usuario de la comunidad ya fue expulsado.',
|
||||
couldnt_create_post: 'No se pudo crear la publicación.',
|
||||
couldnt_like_post: 'No se pudo gustar la publicación.',
|
||||
couldnt_find_post: 'No se pudo encontrar la publicación.',
|
||||
couldnt_get_posts: 'No se pudo obtener las publicaciones',
|
||||
couldnt_update_post: 'No se pudo actualizar la publicación',
|
||||
couldnt_save_post: 'No se pudo guardar la publicación.',
|
||||
no_slurs: 'Prohibido insultar.',
|
||||
not_an_admin: 'No es un administrador.',
|
||||
site_already_exists: 'El sitio ya existe.',
|
||||
couldnt_update_site: 'No se pudo actualizar el sitio.',
|
||||
couldnt_find_that_username_or_email:
|
||||
'No se pudo encontrar ese nombre de usuario o correo electrónico.',
|
||||
password_incorrect: 'Contraseña incorrecta.',
|
||||
passwords_dont_match: 'Las contraseñas no coinciden.',
|
||||
admin_already_created: 'Lo sentimos, ya hay un adminisitrador.',
|
||||
user_already_exists: 'El usuario ya existe.',
|
||||
email_already_exists: 'El correo ya está en uso.',
|
||||
couldnt_update_user: 'No se pudo actualizar el usuario.',
|
||||
system_err_login:
|
||||
'Error del sistema. Intente cerrar sesión e ingresar de nuevo.',
|
||||
couldnt_create_private_message: 'No se pudo crear el mensaje privado.',
|
||||
no_private_message_edit_allowed:
|
||||
'Sin permisos para editar el mensaje privado.',
|
||||
couldnt_update_private_message: 'No se pudo actualizar el mensaje privado.',
|
||||
old: 'Antiguo',
|
||||
time: 'Tiempo',
|
||||
action: 'Acción',
|
||||
},
|
||||
};
|
169
ui/src/translations/fa.ts
vendored
169
ui/src/translations/fa.ts
vendored
|
@ -1,169 +0,0 @@
|
|||
export const fa = {
|
||||
translation: {
|
||||
post: 'مطلب',
|
||||
remove_post: 'حذف مطلب',
|
||||
no_posts: 'بدون مطلب.',
|
||||
create_a_post: 'ایجاد یک مطلب',
|
||||
create_post: 'ایجاد مطلب',
|
||||
number_of_posts: '{{count}} مطلب',
|
||||
posts: 'مطالب',
|
||||
related_posts: 'این مطالب ممکن است مرتبط باشند',
|
||||
cross_posts: 'این پیوند در اینجا هم منتشر شده:',
|
||||
comments: 'نظرات',
|
||||
number_of_comments: '{{count}} نظر',
|
||||
remove_comment: 'حذف نظر',
|
||||
communities: 'جوامع',
|
||||
users: 'کاربران',
|
||||
create_a_community: 'ایجاد یک جامعه جدید',
|
||||
create_community: 'ایجاد جامعه',
|
||||
remove_community: 'حذف جامعه',
|
||||
list_of_communities: 'فهرست جوامع',
|
||||
number_of_communities: '{{count}} جامعه',
|
||||
community_reqs: 'حروف کوچک, زیرخط, و بدون فاصله.',
|
||||
edit: 'ویرایش',
|
||||
reply: 'پاسخ',
|
||||
cancel: 'لغو',
|
||||
preview: 'پیشنمایش',
|
||||
upload_image: 'بارگذاری تصویر',
|
||||
avatar: 'آواتار',
|
||||
upload_avatar: 'بارگذاری آواتار',
|
||||
show_avatars: 'نمایش آواتارها',
|
||||
formatting_help: 'راهنمای قالببندی',
|
||||
view_source: 'نمایش منبع',
|
||||
unlock: 'بازکردن قفل',
|
||||
lock: 'قفل کردن',
|
||||
sticky: 'چسبان',
|
||||
unsticky: 'غیرچسبان',
|
||||
link: 'پیوند',
|
||||
archive_link: 'بایگاهی پیوند',
|
||||
settings: 'تنظیمات',
|
||||
admin: 'مدیر',
|
||||
admins: 'مدیران',
|
||||
remove_as_admin: 'حذف به عنوان مدیر',
|
||||
appoint_as_admin: 'انتصاب به عنوان مدیر',
|
||||
remove: 'حذف',
|
||||
removed: 'حذف شد',
|
||||
locked: 'قفل شد',
|
||||
reason: 'دلیل',
|
||||
mark_as_read: 'علامتگذاری به عنوان خوانده شده',
|
||||
mark_as_unread: 'علامتگذاری به عنوان خوانده نشده',
|
||||
delete: 'پاک کردن',
|
||||
deleted: 'پاک شد',
|
||||
delete_account: 'پاک کردن حساب',
|
||||
delete_account_confirm:
|
||||
'هشدار: این کنش، تمام اطلاعات شما را برای همیشه پاک میکند. برای تایید، گذرواژه خود را وارد کنید.',
|
||||
restore: 'بازگردانی',
|
||||
save: 'ذخیره',
|
||||
unsave: 'عدم ذخیره',
|
||||
create: 'ایجاد',
|
||||
creator: 'سازنده',
|
||||
username: 'نامکاربری',
|
||||
email_or_username: 'رایانامه یا نامکاربری',
|
||||
number_of_users: '{{count}} کاربر',
|
||||
number_of_points: '{{count}} امتیاز',
|
||||
number_online: '{{count}} کاربر برخط',
|
||||
name: 'نام',
|
||||
title: 'عنوان',
|
||||
category: 'دستهبندی',
|
||||
prev: 'پیش',
|
||||
next: 'بعد',
|
||||
sidebar: 'نوار کناری',
|
||||
sort_type: 'نوع ترتیب',
|
||||
hot: 'داغ',
|
||||
new: 'تازه',
|
||||
top_day: 'بهترینهای روز',
|
||||
week: 'هفته',
|
||||
month: 'ماه',
|
||||
year: 'سال',
|
||||
all: 'همه',
|
||||
top: 'بالاترین',
|
||||
mark_all_as_read: 'علامت زدن همه به عنوان خوانده شده',
|
||||
type: 'نوع',
|
||||
unread: 'خواندهنشده',
|
||||
replies: 'پاسخها',
|
||||
mentions: 'اشارهها',
|
||||
reply_sent: 'پاسخ فرستاده شد',
|
||||
search: 'جستجو',
|
||||
overview: 'دید کلی',
|
||||
view: 'نما',
|
||||
logout: 'خروج',
|
||||
login_sign_up: 'ورود / نامنویسی',
|
||||
login: 'ورود',
|
||||
sign_up: 'نامنویسی',
|
||||
unread_messages: 'پیامهای خوانده نشده',
|
||||
password: 'گذرواژه',
|
||||
verify_password: 'تایید گذرواژه',
|
||||
old_password: 'پسورد پیشین',
|
||||
forgot_password: 'گذرواژه را فراموش کردهام',
|
||||
reset_password_mail_sent: 'رایانامهای برای بازنشانی گذرواژه فرستاده شد.',
|
||||
password_change: 'تغییر گذرواژه',
|
||||
new_password: 'گذرواژه جدید',
|
||||
email: 'رایانامه',
|
||||
send_notifications_to_email: 'فرستادن اعلانات به رایانامه',
|
||||
optional: 'انتخابی',
|
||||
expires: 'منقضی شود',
|
||||
language: 'زبان',
|
||||
browser_default: 'پیشفرض مرورگر',
|
||||
downvotes_disabled: 'رای پایین غیرفعال است',
|
||||
enable_downvotes: 'فعالسازی رای پایین',
|
||||
open_registration: 'باز کردن نامنویسی',
|
||||
registration_closed: 'نامنویسی بسته است',
|
||||
enable_nsfw: 'فعالسازی NSFW',
|
||||
chat: 'گپ',
|
||||
recent_comments: 'نظرات اخیر',
|
||||
no_results: 'بدون نتیجه.',
|
||||
setup: 'نصب',
|
||||
lemmy_instance_setup: 'نصب نمونهٔ لمی',
|
||||
setup_admin: 'نصب مدیریت پایگاه',
|
||||
your_site: 'پایگاه شما',
|
||||
modified: 'تغییر یافت',
|
||||
nsfw: 'NSFW',
|
||||
show_nsfw: 'نمایش محتوای NSFW',
|
||||
sponsors: 'حامیان',
|
||||
sponsors_of_lemmy: 'حامیان لمی',
|
||||
support_on_patreon: 'حمایت روی Patreon',
|
||||
donate_to_lemmy: 'اعطای اعانه به لمی',
|
||||
donate: 'اعانه',
|
||||
crypto: 'رمزارز',
|
||||
bitcoin: 'بیتکوین',
|
||||
ethereum: 'اتریوم',
|
||||
monero: 'مونرو',
|
||||
code: 'کد',
|
||||
transfer_community: 'انتقال جامعه',
|
||||
transfer_site: 'انتقال پایگاه',
|
||||
are_you_sure: 'مطمئنید؟',
|
||||
yes: 'بله',
|
||||
no: 'خیر',
|
||||
powered_by: 'نیرو گرفته از',
|
||||
not_logged_in: 'وارد نشدهاید.',
|
||||
community_ban: 'فعالیت شما در این جامعه ممنوع شده است.',
|
||||
site_ban: 'فعالیت شما در این پایگاه ممنوع شده است',
|
||||
couldnt_create_comment: 'ناتوانی در ایجاد نظر.',
|
||||
couldnt_like_comment: 'ناتوانی در پسنیدن نظر.',
|
||||
couldnt_update_comment: 'ناتوانی در بهروزرسانی نظر.',
|
||||
couldnt_save_comment: 'ناتوانی در ذخیره نظر.',
|
||||
no_comment_edit_allowed: 'مجاز به ویرایش نظر نیستید.',
|
||||
no_post_edit_allowed: 'مجاز به ویرایش مطلب نیستید.',
|
||||
no_community_edit_allowed: 'مجاز به ویرایش جامعه نیستید.',
|
||||
couldnt_find_community: 'ناتوانی در یافتن جامعه.',
|
||||
couldnt_update_community: 'ناتوانی در بهروزرسانی جامعه.',
|
||||
community_already_exists: 'این جامعه از قبل وجود داشته است.',
|
||||
couldnt_create_post: 'ناتوانی در ایجاد مطلب.',
|
||||
couldnt_like_post: 'ناتوانی در پسندیدن مطلب.',
|
||||
couldnt_find_post: 'ناتوانی در یافتن مطلب.',
|
||||
couldnt_get_posts: 'ناتوانی در دریافت مطالب',
|
||||
couldnt_update_post: 'ناتوای در بهروزرسانی مطلب',
|
||||
couldnt_save_post: 'ناتوانی در ذخیره مطلب.',
|
||||
not_an_admin: 'مدیر نیستید.',
|
||||
site_already_exists: 'این پایگاه از قبل وجود داشته است.',
|
||||
couldnt_update_site: 'ناتوانی در بهروزرسانی پایگاه.',
|
||||
couldnt_find_that_username_or_email:
|
||||
'ناتوانی در یافتن این نام کاربری یا رایانامه.',
|
||||
password_incorrect: 'گذرواژه نادرست.',
|
||||
passwords_dont_match: 'گذرواژهها با هم منطبق نیستند.',
|
||||
user_already_exists: 'این کاربر از قبل وجود دارد.',
|
||||
email_already_exists: 'این رایانامه از قبل وجود دارد.',
|
||||
couldnt_update_user: 'ناتوانی در بهروزرسانی کاربر.',
|
||||
system_err_login: 'خطای سامانه. سعی کنید خارج شده و دوباره وارد شوید.',
|
||||
},
|
||||
};
|
236
ui/src/translations/fi.ts
vendored
236
ui/src/translations/fi.ts
vendored
|
@ -1,236 +0,0 @@
|
|||
export const fi = {
|
||||
translation: {
|
||||
post: 'viesti',
|
||||
remove_post: 'Poista viesti',
|
||||
no_posts: 'Ei viestjä.',
|
||||
create_a_post: 'Luo viesti',
|
||||
create_post: 'Luo viesti',
|
||||
number_of_posts: '{{count}} viestiä',
|
||||
posts: 'Viestit',
|
||||
related_posts: 'Nämä viestit voivat liittyä toisiinsa',
|
||||
cross_posts: 'Tämä linkki on jaettu:',
|
||||
cross_post: 'jaa ristiin',
|
||||
comments: 'Kommentit',
|
||||
number_of_comments: '{{count}} kommenttia',
|
||||
remove_comment: 'Poista kommentti',
|
||||
communities: 'Yhteisöt',
|
||||
users: 'Käyttäjät',
|
||||
create_a_community: 'Luo yhteisö',
|
||||
create_community: 'Luo yhteisö',
|
||||
remove_community: 'Poista yhteisö',
|
||||
subscribed_to_communities: 'Tilatut <1>yhteisöt</1>',
|
||||
trending_communities: 'Nousevat <1>yhteisöt</1>',
|
||||
list_of_communities: 'Lista yhteisöistä',
|
||||
number_of_communities: '{{count}} yhteisöä',
|
||||
community_reqs:
|
||||
'pienillä kirjaimilla, alleviivauksella, eikä välilyöntejä.',
|
||||
create_private_message: 'Luo yksityisviesti',
|
||||
send_secure_message: 'Lähetä suojattu viesti',
|
||||
send_message: 'Lähetä viesti',
|
||||
message: 'Viesti',
|
||||
edit: 'muokkaa',
|
||||
reply: 'vastaa',
|
||||
cancel: 'Peru',
|
||||
preview: 'Esikatselu',
|
||||
upload_image: 'lataa kuva',
|
||||
avatar: 'avatar',
|
||||
upload_avatar: 'Lähetä avatar',
|
||||
show_avatars: 'Näytä avatarit',
|
||||
formatting_help: 'apua muotoiluun',
|
||||
view_source: 'näytä lähde',
|
||||
unlock: 'avaa',
|
||||
lock: 'lukitse',
|
||||
sticky: 'kiinnitä',
|
||||
unsticky: 'poista kiinnitys',
|
||||
link: 'linkitä',
|
||||
archive_link: 'arkistoi linkki',
|
||||
mod: 'moderaattori',
|
||||
mods: 'moderaattorit',
|
||||
moderates: 'Moderoi',
|
||||
settings: 'Asetukset',
|
||||
remove_as_mod: 'Poista moderaattorina',
|
||||
appoint_as_mod: 'Nimitä moderaattoriksi',
|
||||
modlog: 'Moderoinnin loki',
|
||||
admin: 'Ylläpitäjä',
|
||||
admins: 'ylläpitäjät',
|
||||
remove_as_admin: 'poista ylläpitäjänä',
|
||||
appoint_as_admin: 'nimitä ylläpitäjäksi',
|
||||
remove: 'poista',
|
||||
removed: 'poistettu',
|
||||
locked: 'lukittu',
|
||||
stickied: 'kiinnitetty',
|
||||
reason: 'Syy',
|
||||
mark_as_read: 'merkitse luetuksi',
|
||||
mark_as_unread: 'merkitse lukemattomaksi',
|
||||
delete: 'poista',
|
||||
deleted: 'deleted',
|
||||
delete_account: 'Poista tili',
|
||||
delete_account_confirm:
|
||||
'Varoitus: tämä poistaa pysyvästi kaiken datasi. Anna salasanasi varmistukseksi.',
|
||||
restore: 'palauta',
|
||||
ban: 'porttikielto',
|
||||
ban_from_site: 'aseta porttikielto sivulle',
|
||||
unban: 'poista porttikielto',
|
||||
unban_from_site: 'poista porttikielto sivulta',
|
||||
banned: 'asetettu porttikieltoon',
|
||||
save: 'tallenna',
|
||||
unsave: 'jätä tallentamatta',
|
||||
create: 'luo',
|
||||
creator: 'luoja',
|
||||
username: 'Käyttäjänimi',
|
||||
email_or_username: 'Sähköposti tai käyttäjätunnus',
|
||||
number_of_users: '{{count}} käyttäjää',
|
||||
number_of_subscribers: '{{count}} tilaajaa',
|
||||
number_of_points: '{{count}} pistettä',
|
||||
number_online: '{{count}} käyttäjää aktiivisena',
|
||||
name: 'Nimi',
|
||||
title: 'Kuvaus',
|
||||
category: 'Luokka',
|
||||
subscribers: 'Tilaajat',
|
||||
both: 'Molemmat',
|
||||
saved: 'Tallennettu',
|
||||
unsubscribe: 'Poista tilaus',
|
||||
subscribe: 'Tilaa',
|
||||
subscribed: 'Tilattu',
|
||||
prev: 'Edellinen',
|
||||
next: 'Seuraava',
|
||||
sidebar: 'Sivupalkki',
|
||||
sort_type: 'Lajittele tyypin mukaan',
|
||||
hot: 'Kuumat',
|
||||
new: 'Uudet',
|
||||
top_day: 'Päivän parhaimmat',
|
||||
week: 'Viikko',
|
||||
month: 'Kuukausi',
|
||||
year: 'Vuosi',
|
||||
all: 'Kaikki',
|
||||
top: 'Parhaimmat',
|
||||
api: 'API',
|
||||
docs: 'Dokumentaatio',
|
||||
inbox: 'Postilaatikko',
|
||||
inbox_for: 'Postilaatikko käyttäjällä <1>{{user}}</1>',
|
||||
mark_all_as_read: 'aseta kaikki luetuiksi',
|
||||
type: 'Tyyppi',
|
||||
unread: 'Lukematon',
|
||||
replies: 'Vastaukset',
|
||||
mentions: 'Maininnat',
|
||||
reply_sent: 'Vastaus lähetetty',
|
||||
message_sent: 'Viesti lähetetty',
|
||||
search: 'Etsi',
|
||||
overview: 'Yleiskatsaus',
|
||||
view: 'Katso',
|
||||
logout: 'Kirjaudu ulos',
|
||||
login_sign_up: 'Kirjaudu sisään / Rekisteröidy',
|
||||
login: 'Kirjaudu sisään',
|
||||
sign_up: 'Rekisteröidy',
|
||||
notifications_error:
|
||||
'Työpöydän ilmoitukset eivät ole saatavilla selaimellesi. Yritä Firefoxia tai Chromea.',
|
||||
unread_messages: 'Lukemattomat viestit',
|
||||
messages: 'Viestit',
|
||||
password: 'Salasana',
|
||||
verify_password: 'Vahvista salasana',
|
||||
old_password: 'Vanha salasana',
|
||||
forgot_password: 'unohdin salasanani',
|
||||
reset_password_mail_sent: 'Sähköposti lähetettiin salasanan nollaamiseksi.',
|
||||
password_change: 'Salasanan muutos',
|
||||
new_password: 'Uusi salasana',
|
||||
no_email_setup: 'Tämä palvelin ei ole asettanut sähköpostia oikein.',
|
||||
email: 'Sähköposti',
|
||||
matrix_user_id: ' Matrix-käyttäjä',
|
||||
private_message_disclaimer:
|
||||
'Varoitus: Yksityisviestit Lemmyssä eivät ole turvallisia. Luo tili <1>Riot.im</1> -palveluun turvallista viestintää varten.',
|
||||
send_notifications_to_email: 'Lähetä ilmoitukset sähköpostiin',
|
||||
optional: 'Valinnainen',
|
||||
expires: 'Umpeutuu',
|
||||
language: 'Kieli',
|
||||
browser_default: 'Selaimen oletus',
|
||||
downvotes_disabled: 'Alaäänet otettu pois päältä',
|
||||
enable_downvotes: 'Salli alaäänet',
|
||||
open_registration: 'Avaa rekisteröityminen',
|
||||
registration_closed: 'Rekisteröityminen suljettu',
|
||||
enable_nsfw: 'Salli NSFW',
|
||||
url: 'URL',
|
||||
body: 'Body',
|
||||
copy_suggested_title: 'kopioi ehdotettu otsikko: {{title}}',
|
||||
community: 'Yhteisö',
|
||||
expand_here: 'Laajenna tässä',
|
||||
subscribe_to_communities: 'Tilaa joitakin <1>yhteisöjä</1>.',
|
||||
chat: 'Chat',
|
||||
recent_comments: 'Viimeaikaiset kommentit',
|
||||
no_results: 'Ei tuloksia.',
|
||||
setup: 'Asetus',
|
||||
lemmy_instance_setup: 'Lemmy-instanssin asetus',
|
||||
setup_admin: 'Aseta sivuston ylläpitäjä',
|
||||
your_site: 'sivustosi',
|
||||
modified: 'muokattu',
|
||||
nsfw: 'NSFW',
|
||||
show_nsfw: 'Näytä NSFW-sisältö',
|
||||
theme: 'Teema',
|
||||
sponsors: 'Sponsorit',
|
||||
sponsors_of_lemmy: 'Lemmy-sponsorit',
|
||||
sponsor_message:
|
||||
'Lemmy on vapaa, <1>avoimen lähdekoodin</1> -ohjelmisto, eli mainontaa, rahantekemistä, tai pääomasijoitusta täällä ei tule ikinä olemaan. Lahjoituksesi tukevat suoraan projektin täysipäiväistä kehitystä. Kiitokset seuraaville ihmisille:',
|
||||
support_on_patreon: 'Tue Patreonissa',
|
||||
donate_to_lemmy: 'Lahjoita Lemmylle',
|
||||
donate: 'Lahjoita',
|
||||
general_sponsors:
|
||||
'Yleisiä sponsoreja ovat he, jotka lupaavat 10-39 dollaria Lemmylle.',
|
||||
crypto: 'Crypto',
|
||||
bitcoin: 'Bitcoin',
|
||||
ethereum: 'Ethereum',
|
||||
monero: 'Monero',
|
||||
code: 'Code',
|
||||
joined: 'Liittyi',
|
||||
by: 'käyttäjältä',
|
||||
to: 'yhteisössä',
|
||||
from: 'paikasta',
|
||||
transfer_community: 'siirron yhteisö',
|
||||
transfer_site: 'siirron määrä',
|
||||
are_you_sure: 'oletko varma?',
|
||||
yes: 'kyllä',
|
||||
no: 'ei',
|
||||
powered_by: 'Vauhdittajana',
|
||||
landing_0:
|
||||
'Lemmy on <1>linkinkerääjä</1> / Reddit-vaihtoehto, tarkoitettu toimimaan <2>fediversessä</2>.<3></3>Sitä voi isännöidä itse, siinä on tosiaikaisesti päivittyvät kommenttiketjut, ja se on pieni (<4>~80 kilotavua</4>). Federointi ActivityPub-verkkoon on suunnittelun alla. <5></5>Tämä on <6>hyvin varhainen betaversio</6>, ja monet ominaisuudet ovat toistaiseksi rikki tai poissa. <7></7>Ehdota uusia ominaisuuksia tai raportoi bugeja <8>tänne.</8><9></9>Tehty teknologioilla <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
||||
not_logged_in: 'Ei kirjautunut sisään.',
|
||||
logged_in: 'Kirjautunut sisään.',
|
||||
community_ban: 'Sinulle on asetettu porttikielto tähän yhteisöön.',
|
||||
site_ban: 'Sinut on asetettu porttikieltoon tältä sivustolta',
|
||||
couldnt_create_comment: 'Kommenttia ei pystytty luomaan.',
|
||||
couldnt_like_comment: 'Kommentista ei voitu tykätä.',
|
||||
couldnt_update_comment: 'Kommenttia ei voitu päivittää.',
|
||||
couldnt_save_comment: 'Kommenttia ei voitu tallentaa.',
|
||||
no_comment_edit_allowed: 'Et ole sallittu muokkaamaan kommenttia.',
|
||||
no_post_edit_allowed: 'Et ole sallittu muokkaamaan viestiä.',
|
||||
no_community_edit_allowed: 'Et ole sallittu muokkaamaan yhteisöä.',
|
||||
couldnt_find_community: 'Yhteisöä ei voitu löytää.',
|
||||
couldnt_update_community: 'Yhteisöä ei voitu päivittää.',
|
||||
community_already_exists: 'Yhteisö on jo olemassa.',
|
||||
community_moderator_already_exists: 'Yhteisön moderaattori on jo olemassa.',
|
||||
community_follower_already_exists: 'Yhteisön seuraaja on jo olemassa.',
|
||||
community_user_already_banned: 'Yhteisön käyttäjä on jo porttikiellossa.',
|
||||
couldnt_create_post: 'Ei voitu luoda viestiä.',
|
||||
couldnt_like_post: 'Viestistä ei voitu tykätä.',
|
||||
couldnt_find_post: 'Viestiä ei löytynyt.',
|
||||
couldnt_get_posts: 'Viestejä ei saatu',
|
||||
couldnt_update_post: 'Viestiä ei voitu päivittää',
|
||||
couldnt_save_post: 'Viestiä ei voitu tallentaa.',
|
||||
no_slurs: 'Ei loukkauksia.',
|
||||
not_an_admin: 'Ei ole ylläpitäjä.',
|
||||
site_already_exists: 'Sivusto on jo olemassa.',
|
||||
couldnt_update_site: 'Sivustoa ei voitu päivittää.',
|
||||
couldnt_find_that_username_or_email:
|
||||
'Käyttäjänimeä tai sähköpostia ei onnistuttu löytämään.',
|
||||
password_incorrect: 'Salasana on väärin.',
|
||||
passwords_dont_match: 'Salasanat eivät täsmää.',
|
||||
admin_already_created: 'Anteeksi, mutta täällä on jo ylläpitäjä.',
|
||||
user_already_exists: 'Käyttäjä on jo olemassa.',
|
||||
email_already_exists: 'Sähköposti on jo olemassa.',
|
||||
couldnt_update_user: 'Käyttäjää ei voitu päivittää.',
|
||||
system_err_login:
|
||||
'Järjestelmävirhe. Yritä kirjautua ulos ja kirjautua uudestaan sisään.',
|
||||
couldnt_create_private_message: 'Yksityisviestiä ei voitu luoda.',
|
||||
no_private_message_edit_allowed:
|
||||
'Et ole sallittu muokkaamaan yksityisviestiä.',
|
||||
couldnt_update_private_message: 'Yksityisviestiä ei voitu päivittää.',
|
||||
},
|
||||
};
|
199
ui/src/translations/fr.ts
vendored
199
ui/src/translations/fr.ts
vendored
|
@ -1,199 +0,0 @@
|
|||
export const fr = {
|
||||
translation: {
|
||||
post: 'publication',
|
||||
remove_post: 'Supprimer la publication',
|
||||
no_posts: 'Pas de publications.',
|
||||
create_a_post: 'Créer une publication',
|
||||
create_post: 'Créer la publication',
|
||||
number_of_posts: '{{count}} Publications',
|
||||
posts: 'Publications',
|
||||
related_posts: 'Ces sujets peuvent être corrélés',
|
||||
cross_posts: 'Ce sujet a également été posté sur :',
|
||||
cross_post: 'crosspost',
|
||||
comments: 'Commentaires',
|
||||
number_of_comments: '{{count}} Commentaires',
|
||||
remove_comment: 'Supprimer le commentaire',
|
||||
communities: 'Communautés',
|
||||
users: 'Utilisateurs',
|
||||
create_a_community: 'Créer une communauté',
|
||||
create_community: 'Créer la communauté',
|
||||
remove_community: 'Supprimer la Communauté',
|
||||
subscribed_to_communities: 'Abonné à ces <1>communautés</1>',
|
||||
trending_communities: '<1>Communautés</1> appréciées',
|
||||
list_of_communities: 'Liste des communautés',
|
||||
number_of_communities: '{{count}} communautés',
|
||||
community_reqs: 'en minuscule, sans espace et avec tiret du bas.',
|
||||
edit: 'éditer',
|
||||
reply: 'répondre',
|
||||
cancel: 'Annuler',
|
||||
preview: 'prévisualiser',
|
||||
upload_image: 'envoyer une image',
|
||||
formatting_help: 'aide au formattage',
|
||||
view_source: 'voir la source',
|
||||
unlock: 'débloquer',
|
||||
lock: 'bloquer',
|
||||
sticky: 'épingler',
|
||||
unsticky: 'décrocher',
|
||||
link: 'lien',
|
||||
mod: 'modérateur',
|
||||
mods: 'modérateurs',
|
||||
moderates: 'Modérer',
|
||||
settings: 'Paramètres',
|
||||
remove_as_mod: 'Supprimer comme modérateur',
|
||||
appoint_as_mod: 'Nommer comme modérateur',
|
||||
modlog: 'Historique de modération',
|
||||
admin: 'admin',
|
||||
admins: 'admins',
|
||||
remove_as_admin: 'Supprimer comme admin',
|
||||
appoint_as_admin: 'Nommer comme admin',
|
||||
remove: 'retirer',
|
||||
removed: 'retiré',
|
||||
locked: 'bloqué',
|
||||
stickied: 'épinglé',
|
||||
reason: 'Raison',
|
||||
mark_as_read: 'marquer comme lu',
|
||||
mark_as_unread: 'marquer comme non-lu',
|
||||
delete: 'supprimer',
|
||||
deleted: 'supprimé',
|
||||
delete_account: 'Supprimer le compte',
|
||||
delete_account_confirm:
|
||||
'Attention: cette action supprime toutes vos données de façons permanente ! Entrez votre mot de passe pour confirmer.',
|
||||
restore: 'restaurer',
|
||||
ban: 'bannir',
|
||||
ban_from_site: 'bannir du site',
|
||||
unban: 'pardon',
|
||||
unban_from_site: 'faire revenir sur le site',
|
||||
banned: 'banni',
|
||||
save: 'sauvegarder',
|
||||
unsave: 'retirer',
|
||||
create: 'créer',
|
||||
creator: 'createur',
|
||||
username: "Nom d'utilisateur",
|
||||
email_or_username: "Email ou Nom d'utilisateur",
|
||||
number_of_users: '{{count}} Utilisateurs',
|
||||
number_of_subscribers: '{{count}} Abonnés',
|
||||
number_of_points: '{{count}} Points',
|
||||
number_online: '{{count}} Utilisateurs en ligne',
|
||||
name: 'Nom',
|
||||
title: 'Titre',
|
||||
category: 'Catégorie',
|
||||
subscribers: 'Abonnés',
|
||||
both: 'Les deux',
|
||||
saved: 'Sauvegardé',
|
||||
unsubscribe: 'Se désabonner',
|
||||
subscribe: "S'abonner",
|
||||
subscribed: 'Abonnés',
|
||||
prev: 'Précédent',
|
||||
next: 'Suivant',
|
||||
sidebar: 'Texte latéral',
|
||||
sort_type: 'Trier',
|
||||
hot: 'Tendances',
|
||||
new: 'Nouveaux',
|
||||
top_day: 'Top du jour',
|
||||
week: 'Semaine',
|
||||
month: 'Mois',
|
||||
year: 'Année',
|
||||
all: 'Tout',
|
||||
top: 'Top',
|
||||
api: 'API',
|
||||
inbox: 'Boîte de réception',
|
||||
inbox_for: 'Boîte de réception de <1>{{user}}</1>',
|
||||
mark_all_as_read: 'Tout marquer comme lu',
|
||||
type: 'Type',
|
||||
unread: 'Non-lu',
|
||||
reply_sent: 'Réponse envoyée',
|
||||
search: 'Rechercher',
|
||||
overview: 'Général',
|
||||
view: 'Voir',
|
||||
logout: 'Se déconnecter',
|
||||
login_sign_up: "Se connecter / S'inscrire",
|
||||
login: 'Se connecter',
|
||||
sign_up: "S'inscrire",
|
||||
notifications_error:
|
||||
'Les notifications de bureau ne sont pas discponibles sur votre navigateur. Essayez Firefox ou Chrome.',
|
||||
unread_messages: 'Messages non-lu',
|
||||
password: 'Mot de passe',
|
||||
verify_password: 'Vérifiez le mot de passe',
|
||||
email: 'Email',
|
||||
optional: 'Optionnel',
|
||||
expires: 'Expire',
|
||||
url: 'URL',
|
||||
body: 'Texte',
|
||||
copy_suggested_title: 'Ajouter le titre suggéré: {{title}}',
|
||||
community: 'Communauté',
|
||||
expand_here: 'Développer ici',
|
||||
subscribe_to_communities: "S'abonner à quelques <1>communautés</1>.",
|
||||
chat: 'Chat',
|
||||
recent_comments: 'Commentaires récents',
|
||||
no_results: 'Pas de résultats.',
|
||||
setup: 'Installation',
|
||||
lemmy_instance_setup: "Installation d'une instance Lemmy",
|
||||
setup_admin: 'Créer un administrateur',
|
||||
your_site: 'votre site',
|
||||
modified: 'modifié',
|
||||
nsfw: 'Pas sûr pour le travail',
|
||||
show_nsfw: 'Afficher le contenu NSFW',
|
||||
theme: 'Thème',
|
||||
sponsors: 'Sponsors',
|
||||
sponsors_of_lemmy: 'Sponsors de Lemmy',
|
||||
sponsor_message:
|
||||
"Lemmy est gratuit et <1>open-source</1>, c'est à dire sans publicité et sans monétisation. Pour toujours. Vos dons soutiennent directement le développement du projet. Merci à nos soutiens.",
|
||||
support_on_patreon: 'Soutenir sur Patreon',
|
||||
support_on_liberapay: 'Soutenir sur Liberapay',
|
||||
general_sponsors:
|
||||
'General Sponsors are those that pledged $10 to $39 to Lemmy.',
|
||||
crypto: 'Cryptomonnaies',
|
||||
bitcoin: 'Bitcoin',
|
||||
ethereum: 'Ethereum',
|
||||
monero: 'Monero',
|
||||
code: 'Code',
|
||||
joined: 'Membre depuis',
|
||||
by: 'par',
|
||||
to: 'vers',
|
||||
transfer_community: 'transférer la communauté',
|
||||
transfer_site: 'transférer le site',
|
||||
are_you_sure: 'Êtes-vous sûr ?',
|
||||
yes: 'oui',
|
||||
no: 'non',
|
||||
powered_by: 'Propulsé par',
|
||||
landing_0:
|
||||
'Lemmy est un <1>aggrégateur de lien</1>, similaire à reddit et conçu pour fonctionner sur le <2>fédiverse</2>.<3></3>Il est auto-hébergeable, se met à jour en direct et est léger (<4>~80kB</4>). La fédération via Activitypub est prévue. <5></5>Lemmy est une <6>version beta très précoce</6>, et de nombreuses fonctionnalités sont manquantes ou non fonctionnelles. <7></7>Vous pouvez rapporter des bugs et suggérez de nouvelles fonctionnalités <8>ici.</8><9></9>Crée avec <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
||||
not_logged_in: "Vous n'êtes pas connecté.",
|
||||
community_ban: 'Vous avez été banni de cette communauté.',
|
||||
site_ban: 'Vous avez été banni du site',
|
||||
couldnt_create_comment: 'Impossible de poster le commentaire.',
|
||||
couldnt_like_comment: "Impossible d'aimer le commentaire.",
|
||||
couldnt_update_comment: 'Impossible de mettre à jour le commentaire.',
|
||||
couldnt_save_comment: 'Impossible de sauvegarder le commentaire.',
|
||||
no_comment_edit_allowed:
|
||||
"Vous n'êtes pas autorisé à éditer ce commentaire.",
|
||||
no_post_edit_allowed: "Vous n'êtes pas autorisé à éditer sujet.",
|
||||
no_community_edit_allowed:
|
||||
"Vous n'êtes pas autorisé à éditer cette communauté.",
|
||||
couldnt_find_community: 'Impossible de trouver cette communauté.',
|
||||
couldnt_update_community: "Impossible d'éditer cette communauté.",
|
||||
community_already_exists: 'Cette communauté existe déjà.',
|
||||
community_moderator_already_exists: 'Ce membre est déjà modérateur.',
|
||||
community_follower_already_exists: 'Ce membre est déjà abonné.',
|
||||
community_user_already_banned: 'Ce membre est déjà banni.',
|
||||
couldnt_create_post: 'Impossible de créer le sujet.',
|
||||
couldnt_like_post: "Impossible d'aimer le sujet.",
|
||||
couldnt_find_post: 'Impossible de trouver le sujet.',
|
||||
couldnt_get_posts: "Impossible d'obtenir les sujets",
|
||||
couldnt_update_post: 'Impossible de mettre à jour le sujet',
|
||||
couldnt_save_post: 'Impossible de sauvegarder le sujet.',
|
||||
no_slurs: "Pas d'insultes.",
|
||||
not_an_admin: 'Pas administrateur.',
|
||||
site_already_exists: 'Le site existe déjà.',
|
||||
couldnt_update_site: 'Impossible de mettre à jour le site.',
|
||||
couldnt_find_that_username_or_email:
|
||||
'Impossible de trouver cet utilisateur ou cet email.',
|
||||
password_incorrect: 'Mot de passe incorrect.',
|
||||
passwords_dont_match: 'Les mots de passes ne correspondent pas..',
|
||||
admin_already_created: 'Désolé, il y a déjà un admin.',
|
||||
user_already_exists: "L'utilisateur existe déjà.",
|
||||
couldnt_update_user: "Impossible de mettre à jour l'utilisateur.",
|
||||
system_err_login:
|
||||
'Erreur système. Essayez de vous déconneter puis de vous reconnecter.',
|
||||
},
|
||||
};
|
192
ui/src/translations/it.ts
vendored
192
ui/src/translations/it.ts
vendored
|
@ -1,192 +0,0 @@
|
|||
export const it = {
|
||||
translation: {
|
||||
post: 'post',
|
||||
remove_post: 'Rimuovi Post',
|
||||
no_posts: 'Nessun Post.',
|
||||
create_a_post: 'Crea un post',
|
||||
create_post: 'Crea Post',
|
||||
number_of_posts: '{{count}} Posts',
|
||||
posts: 'Posts',
|
||||
related_posts: 'Questi post potrebbero essere correlati',
|
||||
cross_posts: 'Questo link è stato postato anche in:',
|
||||
cross_post: 'cross-post',
|
||||
comments: 'Commenti',
|
||||
number_of_comments: '{{count}} Commenti',
|
||||
remove_comment: 'Rimuovi Commento',
|
||||
communities: 'Comunità',
|
||||
users: 'Utenti',
|
||||
create_a_community: 'Crea una Comunità',
|
||||
create_community: 'Crea Comunità',
|
||||
remove_community: 'Rimuovi Comunità',
|
||||
subscribed_to_communities: 'Iscritto alle <1>comunità</1>',
|
||||
trending_communities: '<1>Comunità</1> in crescita',
|
||||
list_of_communities: 'Lista di comunità',
|
||||
number_of_communities: '{{count}} Comunità',
|
||||
community_reqs: 'minuscole, trattini bassi e nessuno spazio.',
|
||||
edit: 'modifica',
|
||||
reply: 'rispondi',
|
||||
cancel: 'Annulla',
|
||||
preview: 'Anteprima',
|
||||
upload_image: 'carica immagine',
|
||||
formatting_help: 'aiuto formattazione',
|
||||
view_source: 'visualizza sorgente',
|
||||
unlock: 'sblocca',
|
||||
lock: 'blocca',
|
||||
sticky: 'evidenzia',
|
||||
unsticky: 'rimuovi evidenza',
|
||||
link: 'link',
|
||||
mod: 'moderatore',
|
||||
mods: 'moderatori',
|
||||
moderates: 'Moderatore di',
|
||||
settings: 'Impostazioni',
|
||||
remove_as_mod: 'rimuovi come moderatore',
|
||||
appoint_as_mod: 'nomina come moderatore',
|
||||
modlog: 'Registro di moderazione',
|
||||
admin: 'amministratore',
|
||||
admins: 'amministratori',
|
||||
remove_as_admin: 'rimuovi come amministratore',
|
||||
appoint_as_admin: 'nomina come amministratore',
|
||||
remove: 'rimuovi',
|
||||
removed: 'rimosso',
|
||||
locked: 'bloccato',
|
||||
stickied: 'evidenziato',
|
||||
reason: 'Ragione',
|
||||
mark_as_read: 'segna come letto',
|
||||
mark_as_unread: 'segna come non letto',
|
||||
delete: 'cancella',
|
||||
deleted: 'cancellato',
|
||||
delete_account: 'Cancella Account',
|
||||
delete_account_confirm: 'Attenzione: stai per cancellare permanentemente tutti i tuoi dati. Sei sicuro?',
|
||||
restore: 'ripristina',
|
||||
ban: 'ban',
|
||||
ban_from_site: 'banna dal sito',
|
||||
unban: 'rimuovi ban',
|
||||
unban_from_site: 'rimuove il ban dal sito',
|
||||
banned: 'bannato',
|
||||
save: 'salva',
|
||||
unsave: 'rimuovi',
|
||||
create: 'crea',
|
||||
creator: 'autore',
|
||||
username: 'Username',
|
||||
email_or_username: 'Email o Username',
|
||||
number_of_users: '{{count}} Utenti',
|
||||
number_of_subscribers: '{{count}} Iscritti',
|
||||
number_of_points: '{{count}} Punti',
|
||||
number_online: '{{count}} Utenti Online',
|
||||
name: 'Nome',
|
||||
title: 'Titolo',
|
||||
category: 'Categoria',
|
||||
subscribers: 'Iscritti',
|
||||
both: 'Entrambi',
|
||||
saved: 'Salvato',
|
||||
unsubscribe: 'Disiscriviti',
|
||||
subscribe: 'Iscriviti',
|
||||
subscribed: 'Iscritto',
|
||||
prev: 'Precedente',
|
||||
next: 'Prossima',
|
||||
sidebar: 'Barra laterale',
|
||||
sort_type: 'Ordina per',
|
||||
hot: 'Popolari',
|
||||
new: 'Nuovi',
|
||||
top_day: 'Migliori della giornata',
|
||||
week: 'Settimana',
|
||||
month: 'Mese',
|
||||
year: 'Anno',
|
||||
all: 'Tutti',
|
||||
top: 'Migliori',
|
||||
api: 'API',
|
||||
inbox: 'Posta in arrivo',
|
||||
inbox_for: 'Posta di <1>{{user}}</1>',
|
||||
mark_all_as_read: 'segna tutti come letti',
|
||||
type: 'Tipo',
|
||||
unread: 'Non letti',
|
||||
replies: 'Risposte',
|
||||
mentions: 'Menzioni',
|
||||
reply_sent: 'Risposta inviata',
|
||||
search: 'Cerca',
|
||||
overview: 'Panoramica',
|
||||
view: 'Visualizza',
|
||||
logout: 'Logout',
|
||||
login_sign_up: 'Login / Iscriviti',
|
||||
login: 'Login',
|
||||
sign_up: 'Iscriviti',
|
||||
notifications_error: 'Le notifiche desktop non sono supportate sul tuo browser. Prova Firefox o Chrome.',
|
||||
unread_messages: 'Messaggi Non Letti',
|
||||
password: 'Password',
|
||||
verify_password: 'Verifica Password',
|
||||
email: 'Email',
|
||||
optional: 'Opzionale',
|
||||
expires: 'Scade',
|
||||
url: 'URL',
|
||||
body: 'Contenuto',
|
||||
copy_suggested_title: 'copia titolo suggerito: {{title}}',
|
||||
community: 'Comunità',
|
||||
expand_here: 'Visualizza qui',
|
||||
subscribe_to_communities: 'Iscriviti ad una <1>comunità</1>.',
|
||||
chat: 'Chat',
|
||||
recent_comments: 'Commenti Recenti',
|
||||
no_results: 'Nessun risultato.',
|
||||
setup: 'Setup',
|
||||
lemmy_instance_setup: 'Setup dell\'istanza di Lemmy',
|
||||
setup_admin: 'Imposta Amministratore del Sito',
|
||||
your_site: 'il tuo sito',
|
||||
modified: 'modificato',
|
||||
nsfw: 'NSFW',
|
||||
show_nsfw: 'Mostra contenuto NSFW',
|
||||
theme: 'Tema',
|
||||
sponsors: 'Sponsors',
|
||||
sponsors_of_lemmy: 'Sponsors di Lemmy',
|
||||
sponsor_message: 'Lemmy è un software gratuito e <1>open-source</1>, il che significa nessuna pubblicità, monetizzazione o investitori esterni, per sempre. Le tue donazioni supportano direttamente lo sviluppo full-time del progetto. Si ringraziano le seguenti persone:',
|
||||
support_on_patreon: 'Supporta su Patreon',
|
||||
support_on_liberapay: 'Supporta su Liberapay',
|
||||
general_sponsors: 'I "General Sponsors" sono quelli che hanno investito dai 10$ ai 39$ su Lemmy.',
|
||||
crypto: 'Crypto',
|
||||
bitcoin: 'Bitcoin',
|
||||
ethereum: 'Ethereum',
|
||||
monero: 'Monero',
|
||||
code: 'Code',
|
||||
joined: 'Iscritto da',
|
||||
by: 'di',
|
||||
to: 'su',
|
||||
transfer_community: 'trasferisci comunità',
|
||||
transfer_site: 'trasferisci sito',
|
||||
are_you_sure: 'sei sicuro?',
|
||||
yes: 'si',
|
||||
no: 'no',
|
||||
powered_by: 'Powered by',
|
||||
landing_0: 'Lemmy è un <1>aggregatore di link</1> / alternativa a reddit, creato per integrarsi con il <2>fediverse</2>. <3></3>È self-hosted, i commenti sono aggiornati in tempo reale ed è molto piccolo (<4>~80kB</4>). La Federazione con la rete ActivityPub sarà implementata nel futuro. <5></5>Questa versione è una <6>beta molto giovane</6> e molte funzionalità sono incomplete o mancanti. <7></7>Suggerisci nuove funzionalità o segnala errori a <8>questa pagina.</8><9></9>Sviluppato con <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
||||
not_logged_in: 'Non hai effettuato l\'accesso.',
|
||||
community_ban: 'Sei stato bannato da questa comunità.',
|
||||
site_ban: 'Sei stato bannato dal sito',
|
||||
couldnt_create_comment: 'Impossibile creare il commento.',
|
||||
couldnt_like_comment: 'Impossibile mettere \'Mi piace\' al commento.',
|
||||
couldnt_update_comment: 'Impossibile aggiornare il commento.',
|
||||
couldnt_save_comment: 'Impossibile salvare il commento.',
|
||||
no_comment_edit_allowed: 'Non sei autorizzato a modificare il commento.',
|
||||
no_post_edit_allowed: 'Non sei autorizzato a modificare il post.',
|
||||
no_community_edit_allowed: 'Non sei autorizzato a modificare la comunità.',
|
||||
couldnt_find_community: 'Impossibile trovare la comunità.',
|
||||
couldnt_update_community: 'Impossibile aggiornare la comunità.',
|
||||
community_already_exists: 'La comunità esiste già.',
|
||||
community_moderator_already_exists: 'Questo utente è già moderatore della comunità.',
|
||||
community_follower_already_exists: 'Questo utente è già moderatore della comunità.',
|
||||
community_user_already_banned: 'L\'utente della comunità è già stato bannato.',
|
||||
couldnt_create_post: 'Impossibile creare il post.',
|
||||
couldnt_like_post: 'Impossibile mettere \'Mi piace\' post.',
|
||||
couldnt_find_post: 'Impossibile trovare il post.',
|
||||
couldnt_get_posts: 'Impossibile recuperare i post',
|
||||
couldnt_update_post: 'Impossibile aggiornare il post',
|
||||
couldnt_save_post: 'Impossibile salvare il post.',
|
||||
no_slurs: 'Niente offese.',
|
||||
not_an_admin: 'Non un amministratore.',
|
||||
site_already_exists: 'Il sito esiste già.',
|
||||
couldnt_update_site: 'Impossibile aggiornare il sito.',
|
||||
couldnt_find_that_username_or_email: 'L\'username o la email non sono stati trovati.',
|
||||
password_incorrect: 'Password non corretta.',
|
||||
passwords_dont_match: 'Le password non corrispondono.',
|
||||
admin_already_created: 'Spiacente, esiste già un amministratore.',
|
||||
user_already_exists: 'L\'utente esiste già.',
|
||||
couldnt_update_user: 'Impossibile aggiornare l\'utente.',
|
||||
system_err_login: 'Si è verificato un errore. Prova ad effettuare nuovamente il login.',
|
||||
},
|
||||
}
|
234
ui/src/translations/nl.ts
vendored
234
ui/src/translations/nl.ts
vendored
|
@ -1,234 +0,0 @@
|
|||
export const nl = {
|
||||
translation: {
|
||||
post: 'post',
|
||||
remove_post: 'Verwijder post',
|
||||
no_posts: 'Geen posts.',
|
||||
create_a_post: 'Plaats een post',
|
||||
create_post: 'Plaats post',
|
||||
number_of_posts: '{{count}} posts',
|
||||
posts: 'posts',
|
||||
related_posts: 'Deze posts kunnen gerelateerd zijn',
|
||||
cross_posts: 'Deze link is ook geplaatst in:',
|
||||
cross_post: 'cross-post',
|
||||
comments: 'Reacties',
|
||||
number_of_comments: '{{count}} reacties',
|
||||
remove_comment: 'Verwijder reactie',
|
||||
communities: 'Communities',
|
||||
users: 'Gebruikers',
|
||||
create_a_community: 'Maak een community',
|
||||
create_community: 'Maak community',
|
||||
remove_community: 'Verwijder community',
|
||||
subscribed_to_communities: 'Geabonneerd op <1>communities</1>',
|
||||
trending_communities: 'Populaire <1>communities</1>',
|
||||
list_of_communities: 'Lijst van communities',
|
||||
number_of_communities: '{{count}} communities',
|
||||
community_reqs: 'kleine letters, onderstrepingsteken en geen spaties',
|
||||
edit: 'bewerk',
|
||||
reply: 'reageer',
|
||||
cancel: 'Annuleer',
|
||||
unlock: 'ontsluiten',
|
||||
lock: 'sluiten',
|
||||
link: 'link',
|
||||
mod: 'moderator',
|
||||
mods: 'moderators',
|
||||
moderates: 'Modereert',
|
||||
settings: 'Instellingen',
|
||||
remove_as_mod: 'Verwijder als moderator',
|
||||
appoint_as_mod: 'Benoemen tot moderator',
|
||||
modlog: 'Moderatorlog',
|
||||
admin: 'beheerder',
|
||||
admins: 'beheerders',
|
||||
remove_as_admin: 'verwijder als beheerder',
|
||||
appoint_as_admin: 'benoemen tot beheerder',
|
||||
remove: 'weghalen',
|
||||
removed: 'weggehaald',
|
||||
locked: 'gesloten',
|
||||
reason: 'Reden',
|
||||
mark_as_read: 'markeer als gelezen',
|
||||
mark_as_unread: 'markeer als ongelezen',
|
||||
delete: 'verwijder',
|
||||
deleted: 'verwijderd',
|
||||
restore: 'herstellen',
|
||||
ban: 'verban',
|
||||
ban_from_site: 'verban van site',
|
||||
unban: 'verbanning opzeggen',
|
||||
unban_from_site: 'verbanning van site opzeggen',
|
||||
save: 'opslaan',
|
||||
unsave: 'unsave',
|
||||
create: 'maak',
|
||||
username: 'Gebruikersnaam',
|
||||
email_or_username: 'E-mail of gebruikersnaam',
|
||||
number_of_users: '{{count}} gebruikers',
|
||||
number_of_subscribers: '{{count}} abonnees',
|
||||
number_of_points: '{{count}} punten',
|
||||
name: 'Naam',
|
||||
title: 'Titel',
|
||||
category: 'Categorie',
|
||||
subscribers: 'Abonnees',
|
||||
both: 'Beide',
|
||||
saved: 'Opgeslagen',
|
||||
unsubscribe: 'Afmelden',
|
||||
subscribe: 'Abonneren',
|
||||
subscribed: 'Geabonneerd',
|
||||
prev: 'Vorige',
|
||||
next: 'Volgende',
|
||||
sidebar: 'Zijbalk',
|
||||
sort_type: 'Sorteertype',
|
||||
hot: 'Populair',
|
||||
new: 'Nieuw',
|
||||
top_day: 'Dagelijkse top',
|
||||
week: 'Week',
|
||||
month: 'Maand',
|
||||
year: 'Jaar',
|
||||
all: 'Alle',
|
||||
top: 'Top',
|
||||
api: 'API',
|
||||
inbox: 'Postvak-in',
|
||||
inbox_for: 'Postvak-in voor <1>{{user}}</1>',
|
||||
mark_all_as_read: 'markeer alle als gelezen',
|
||||
type: 'Type',
|
||||
unread: 'Ongelezen',
|
||||
reply_sent: 'Reactie gestuurd',
|
||||
search: 'Zoek',
|
||||
overview: 'Overzicht',
|
||||
view: 'Beeld',
|
||||
logout: 'Log uit',
|
||||
login_sign_up: 'Log in / Aanmelden',
|
||||
login: 'Log in',
|
||||
sign_up: 'Aanmelden',
|
||||
notifications_error:
|
||||
'Bureabladberichten niet beschikbaar in je browser. Probeer Firefox of Chrome.',
|
||||
unread_messages: 'Ongelezen berichten',
|
||||
password: 'Wachtwoord',
|
||||
verify_password: 'Herhaal wachtwoord',
|
||||
email: 'E-mail',
|
||||
optional: 'Optioneel',
|
||||
expires: 'Verloopt',
|
||||
url: 'url',
|
||||
body: 'Tekst',
|
||||
copy_suggested_title: 'neem voorgestelde titel over: {{title}}',
|
||||
community: 'Community',
|
||||
expand_here: 'Breid hier uit',
|
||||
subscribe_to_communities: 'Abonneer je op een paar <1>communities</1>.',
|
||||
chat: 'Praat',
|
||||
recent_comments: 'Recente reacties',
|
||||
no_results: 'Geen resultaten',
|
||||
setup: 'Installatie',
|
||||
lemmy_instance_setup: 'Installatie van Lemmy-instantie',
|
||||
setup_admin: 'Maak een administrator',
|
||||
your_site: 'jouw site',
|
||||
modified: 'bewerkt',
|
||||
nsfw: 'NSFW',
|
||||
show_nsfw: 'Laat NSFW-inhoud zien',
|
||||
sponsors: 'Sponsoren',
|
||||
sponsors_of_lemmy: 'Sponsoren van Lemmy',
|
||||
sponsor_message:
|
||||
'Lemmy is vrije, <1>open-source</1> software, dus zonder reclame, winstoogmerk en durfkapitaal, punt. Jouw donaties gaan direct naar de full-time-ontwikkeling van het project. Met veel dank aan de volgende mensen:',
|
||||
support_on_patreon: 'Ondersteun op Patreon',
|
||||
support_on_liberapay: 'Ondersteun op Liberapay',
|
||||
general_sponsors:
|
||||
'Algemene sponsors zijn sponsors die tussen de $10 en $39 hebben gegeven aan Lemmy.',
|
||||
crypto: 'Cryptovaluta',
|
||||
bitcoin: 'Bitcoin',
|
||||
ethereum: 'Ethereum',
|
||||
monero: 'Monero',
|
||||
code: 'Code',
|
||||
joined: 'toegetreden',
|
||||
by: 'door',
|
||||
to: 'aan',
|
||||
transfer_community: 'community overplaatsen',
|
||||
transfer_site: 'site overplaatsen',
|
||||
are_you_sure: 'weet je het zeker?',
|
||||
yes: 'ja',
|
||||
no: 'nee',
|
||||
powered_by: 'Mogelijk gemaakt door',
|
||||
landing_0:
|
||||
'Lemmy is een <1>linkverzameler</1> / reddit-alternatief, bedoeld om in de <2>fediverse</2> te werken.<3></3>Lemmy kan door om het even wie gehost worden, heeft live-bijgewerkte reacties en is superklein (<4>ca. 80 kB</4>). Federatie in hte ActivityPub-netwerk is gepland. <5></5>Dit is een <6>erg vroege bèta-versie</6>, en een hoop functies zijn stuk of afwezig. <7></7>Stel nieuwe functies voor of meldt fouten <8>hier</8>.<9></9>Gemaakt met <10>Rust</10>, <11>Actix</11>, <12>Inferno</12> en <13>Typescript</13>.',
|
||||
not_logged_in: 'Niet ingelogd.',
|
||||
community_ban: 'Je bent verbannen uit deze community.',
|
||||
site_ban: 'Je bent verbannen van deze site.',
|
||||
couldnt_create_comment: 'Kon reactie niet maken.',
|
||||
couldnt_like_comment: 'Kon reactie niet leuk vinden.',
|
||||
couldnt_update_comment: 'Kon reactie niet bijwerken.',
|
||||
couldnt_save_comment: 'Kon reactie niet opslaan.',
|
||||
no_comment_edit_allowed: 'Niet toegestaan om reactie te bewerken.',
|
||||
no_post_edit_allowed: 'Niet toegestaan om posts te bewerken.',
|
||||
no_community_edit_allowed: 'Niet toegestaan om community te bewerken.',
|
||||
couldnt_find_community: 'Kon community niet vinden.',
|
||||
couldnt_update_community: 'Kon community niet bijwerken.',
|
||||
community_already_exists: 'Community bestaat al.',
|
||||
community_moderator_already_exists: 'Community-moderator bestaat al.',
|
||||
community_follower_already_exists: 'Community-volger bestaat al.',
|
||||
community_user_already_banned: 'Community-gebruiker reeds verbannen.',
|
||||
couldnt_create_post: 'Kon post niet maken.',
|
||||
couldnt_like_post: 'Kon post niet leuk vinden.',
|
||||
couldnt_find_post: 'Kon post niet vinden.',
|
||||
couldnt_get_posts: 'Kon posts niet ophalen.',
|
||||
couldnt_update_post: 'Kon post niet bijwerken.',
|
||||
couldnt_save_post: 'Kon post niet opslaan.',
|
||||
no_slurs: 'Geen beledigingen.',
|
||||
not_an_admin: 'Niet een beheerder.',
|
||||
site_already_exists: 'Site bestaat al.',
|
||||
couldnt_update_site: 'Kon site niet bijwerken.',
|
||||
couldnt_find_that_username_or_email:
|
||||
'Kon gebruikersnaam of e-mailadres niet vinden.',
|
||||
password_incorrect: 'Wachtwoord incorrect.',
|
||||
passwords_dont_match: 'Wachtwoorden zijn niet gelijk.',
|
||||
admin_already_created: 'Sorry, er is al een beheerder.',
|
||||
user_already_exists: 'Gebruiker bestaat al.',
|
||||
couldnt_update_user: 'Kon gebruiker niet bijwerken.',
|
||||
system_err_login:
|
||||
'Systeemfout. Probeer uit te loggen en weer in te loggen.',
|
||||
preview: 'voorbeeld',
|
||||
upload_image: 'Afbeelding uploaden',
|
||||
avatar: 'Avatar',
|
||||
upload_avatar: 'Avatar uploaden',
|
||||
show_avatars: 'Toon avatars',
|
||||
formatting_help: 'Opmaak hulp',
|
||||
view_source: 'bekijk bron',
|
||||
sticky: 'vastplakken',
|
||||
unsticky: 'loshalen',
|
||||
archive_link: 'Archiveer link',
|
||||
stickied: 'vastgeplakt',
|
||||
delete_account: 'Verwijder account',
|
||||
delete_account_confirm: 'Waarschuwing: dit zal al uw data voorgoed verwijderen, vul uw wachtwoord in om te bevestigen.',
|
||||
banned: 'verbannen',
|
||||
creator: 'auteur',
|
||||
number_online: '{{count}} gebruikers online',
|
||||
docs: 'Documentatie',
|
||||
replies: 'Reacties',
|
||||
mentions: 'vermeldingen',
|
||||
old_password: 'Oud wachtwoord',
|
||||
forgot_password: 'wachtwoord vergeten',
|
||||
reset_password_mail_sent: 'Stuur een email om uw wachtwoord te resetten',
|
||||
password_change: 'Wachtwoord aanpassen',
|
||||
new_password: 'Nieuw wachtwoord',
|
||||
no_email_setup: 'Deze server heeft email niet correct opgezet',
|
||||
send_notifications_to_email: 'Stuur meldingen naar je email',
|
||||
language: 'Taal',
|
||||
browser_default: 'Browser standaard',
|
||||
downvotes_disabled: 'Downvotes geblokkeerd',
|
||||
enable_downvotes: 'Downvotes toestaan',
|
||||
open_registration: 'Open registratie',
|
||||
registration_closed: 'Registratie gesloten',
|
||||
enable_nsfw: 'NSFW toestaan',
|
||||
theme: 'Thema',
|
||||
create_private_message: 'Maak een beveiligd bericht',
|
||||
send_secure_message: 'Verstuur beveiligd bericht',
|
||||
send_message: 'Verstuur bericht',
|
||||
message: 'Bericht',
|
||||
old: 'Oud',
|
||||
message_sent: 'Bericht verstuurd',
|
||||
messages: 'Berichten',
|
||||
matrix_user_id: 'Matrix gebruikers-id',
|
||||
private_message_disclaimer: 'Waarschuwing: Privé berichten in Lemmy zijn niet beveiligd. Maak een account aan op <1>Riot.im</1> om veilig te communiceren',
|
||||
donate_to_lemmy: 'Doneer aan Lemmy',
|
||||
donate: 'Doneer',
|
||||
from: 'van',
|
||||
logged_in: 'Ingelogd',
|
||||
email_already_exists: 'Email bestaat al',
|
||||
couldnt_create_private_message: 'Kan beveiligd bericht niet maken',
|
||||
no_private_message_edit_allowed: 'Niet toegestaan om privé berichten te wijzigen',
|
||||
couldnt_update_private_message: 'Kan beveiligd bericht niet bijwerken'
|
||||
},
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue