mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-11-25 07:36:01 +00:00
Merge branch 'master' into iav-arm-musl-dessalines
This commit is contained in:
commit
983a45e178
34
RELEASES.md
vendored
34
RELEASES.md
vendored
|
@ -1,3 +1,37 @@
|
|||
# Lemmy v0.7.0 Release (2020-06-2X)
|
||||
|
||||
## Breaking Change to our image server: Pictshare to Pict-rs migration guide
|
||||
|
||||
This release replaces [pictshare](https://github.com/HaschekSolutions/pictshare) with [pict-rs](https://git.asonix.dog/asonix/pict-rs), and a script must be run on your server to upgrade.
|
||||
|
||||
To update, run:
|
||||
|
||||
```
|
||||
cd /lemmy
|
||||
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml
|
||||
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/migrate-pictshare-to-pictrs.bash
|
||||
sudo bash migrate-pictshare-to-pictrs.bash
|
||||
```
|
||||
|
||||
You'll also have to update your nginx config, use the [one here](https://github.com/LemmyNet/lemmy/blob/master/ansible/templates/nginx.conf).
|
||||
|
||||
*You'll have to log in again to pick up your avatar*
|
||||
|
||||
Apart from that, we've closed [~90 issues!](https://github.com/LemmyNet/lemmy/milestone/16?closed=1), including:
|
||||
|
||||
- Site-wide list of recent comments.
|
||||
- Reconnecting websockets.
|
||||
- Lots more themes, including a default light one.
|
||||
- Expandable embeds for post links (and thumbnails), from iframely.
|
||||
- Better icons.
|
||||
- Emoji autocomplete to post and message bodies, and an Emoji Picker.
|
||||
- Post body now searchable.
|
||||
- Community title and description is now searchable.
|
||||
- Simplified cross-posts.
|
||||
- Better documentation.
|
||||
- LOTS more languages.
|
||||
- Lots of bugs squashed.
|
||||
|
||||
# Lemmy v0.6.0 Release (2020-01-16)
|
||||
|
||||
`v0.6.0` is here, and we've closed [41 issues!](https://github.com/LemmyNet/lemmy/milestone/15?closed=1)
|
||||
|
|
2
ansible/VERSION
vendored
2
ansible/VERSION
vendored
|
@ -1 +1 @@
|
|||
v0.6.77
|
||||
v0.6.79
|
||||
|
|
1
ansible/ansible.cfg
vendored
1
ansible/ansible.cfg
vendored
|
@ -1,5 +1,6 @@
|
|||
[defaults]
|
||||
inventory=inventory
|
||||
interpreter_python=/usr/bin/python3
|
||||
|
||||
[ssh_connection]
|
||||
pipelining = True
|
||||
|
|
8
ansible/lemmy.yml
vendored
8
ansible/lemmy.yml
vendored
|
@ -24,10 +24,11 @@
|
|||
creates: '/etc/letsencrypt/live/{{domain}}/privkey.pem'
|
||||
|
||||
- name: create lemmy folder
|
||||
file: path={{item.path}} state=directory
|
||||
file: path={{item.path}} {{item.owner}} state=directory
|
||||
with_items:
|
||||
- { path: '/lemmy/' }
|
||||
- { path: '/lemmy/volumes/' }
|
||||
- { path: '/lemmy/', owner: 'root' }
|
||||
- { path: '/lemmy/volumes/', owner: 'root' }
|
||||
- { path: '/lemmy/volumes/pictrs/', owner: '991' }
|
||||
|
||||
- block:
|
||||
- name: add template files
|
||||
|
@ -59,6 +60,7 @@
|
|||
project_src: /lemmy/
|
||||
state: present
|
||||
pull: yes
|
||||
remove_orphans: yes
|
||||
|
||||
- name: reload nginx with new config
|
||||
shell: nginx -s reload
|
||||
|
|
8
ansible/lemmy_dev.yml
vendored
8
ansible/lemmy_dev.yml
vendored
|
@ -26,10 +26,11 @@
|
|||
creates: '/etc/letsencrypt/live/{{domain}}/privkey.pem'
|
||||
|
||||
- name: create lemmy folder
|
||||
file: path={{item.path}} state=directory
|
||||
file: path={{item.path}} owner={{item.owner}} state=directory
|
||||
with_items:
|
||||
- { path: '/lemmy/' }
|
||||
- { path: '/lemmy/volumes/' }
|
||||
- { path: '/lemmy/', owner: 'root' }
|
||||
- { path: '/lemmy/volumes/', owner: 'root' }
|
||||
- { path: '/lemmy/volumes/pictrs/', owner: '991' }
|
||||
|
||||
- block:
|
||||
- name: add template files
|
||||
|
@ -88,6 +89,7 @@
|
|||
project_src: /lemmy/
|
||||
state: present
|
||||
recreate: always
|
||||
remove_orphans: yes
|
||||
ignore_errors: yes
|
||||
|
||||
- name: reload nginx with new config
|
||||
|
|
11
ansible/templates/docker-compose.yml
vendored
11
ansible/templates/docker-compose.yml
vendored
|
@ -12,7 +12,7 @@ services:
|
|||
- ./lemmy.hjson:/config/config.hjson:ro
|
||||
depends_on:
|
||||
- postgres
|
||||
- pictshare
|
||||
- pictrs
|
||||
- iframely
|
||||
|
||||
postgres:
|
||||
|
@ -25,12 +25,13 @@ services:
|
|||
- ./volumes/postgres:/var/lib/postgresql/data
|
||||
restart: always
|
||||
|
||||
pictshare:
|
||||
image: hascheksolutions/pictshare:latest
|
||||
pictrs:
|
||||
image: asonix/pictrs:amd64-v0.1.0-r9
|
||||
user: 991:991
|
||||
ports:
|
||||
- "127.0.0.1:8537:80"
|
||||
- "127.0.0.1:8537:8080"
|
||||
volumes:
|
||||
- ./volumes/pictshare:/usr/share/nginx/html/data
|
||||
- ./volumes/pictrs:/mnt
|
||||
restart: always
|
||||
|
||||
iframely:
|
||||
|
|
26
ansible/templates/nginx.conf
vendored
26
ansible/templates/nginx.conf
vendored
|
@ -48,8 +48,8 @@ server {
|
|||
add_header X-Frame-Options "DENY";
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
|
||||
# Upload limit for pictshare
|
||||
client_max_body_size 50M;
|
||||
# Upload limit for pictrs
|
||||
client_max_body_size 20M;
|
||||
|
||||
location / {
|
||||
proxy_pass http://0.0.0.0:8536;
|
||||
|
@ -70,15 +70,21 @@ server {
|
|||
proxy_cache_min_uses 5;
|
||||
}
|
||||
|
||||
location /pictshare/ {
|
||||
proxy_pass http://0.0.0.0:8537/;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# Redirect pictshare images to pictrs
|
||||
location ~ /pictshare/(.*)$ {
|
||||
return 301 /pictrs/image/$1;
|
||||
}
|
||||
|
||||
if ($request_uri ~ \.(?:ico|gif|jpe?g|png|webp|bmp|mp4)$) {
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
}
|
||||
# pict-rs images
|
||||
location /pictrs {
|
||||
location /pictrs/image {
|
||||
proxy_pass http://0.0.0.0:8537/image;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
# Block the import
|
||||
return 403;
|
||||
}
|
||||
|
||||
location /iframely/ {
|
||||
|
|
32
docker/dev/docker-compose.yml
vendored
32
docker/dev/docker-compose.yml
vendored
|
@ -1,15 +1,6 @@
|
|||
version: '3.3'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:12-alpine
|
||||
environment:
|
||||
- POSTGRES_USER=lemmy
|
||||
- POSTGRES_PASSWORD=password
|
||||
- POSTGRES_DB=lemmy
|
||||
volumes:
|
||||
- ./volumes/postgres:/var/lib/postgresql/data
|
||||
restart: always
|
||||
|
||||
lemmy:
|
||||
build:
|
||||
|
@ -23,16 +14,27 @@ services:
|
|||
volumes:
|
||||
- ../lemmy.hjson:/config/config.hjson
|
||||
depends_on:
|
||||
- pictrs
|
||||
- postgres
|
||||
- pictshare
|
||||
- iframely
|
||||
|
||||
pictshare:
|
||||
image: hascheksolutions/pictshare:latest
|
||||
ports:
|
||||
- "127.0.0.1:8537:80"
|
||||
postgres:
|
||||
image: postgres:12-alpine
|
||||
environment:
|
||||
- POSTGRES_USER=lemmy
|
||||
- POSTGRES_PASSWORD=password
|
||||
- POSTGRES_DB=lemmy
|
||||
volumes:
|
||||
- ./volumes/pictshare:/usr/share/nginx/html/data
|
||||
- ./volumes/postgres:/var/lib/postgresql/data
|
||||
restart: always
|
||||
|
||||
pictrs:
|
||||
image: asonix/pictrs:v0.1.13-r0
|
||||
ports:
|
||||
- "127.0.0.1:8537:8080"
|
||||
user: 991:991
|
||||
volumes:
|
||||
- ./volumes/pictrs:/mnt
|
||||
restart: always
|
||||
|
||||
iframely:
|
||||
|
|
16
docker/prod/docker-compose.yml
vendored
16
docker/prod/docker-compose.yml
vendored
|
@ -12,7 +12,7 @@ services:
|
|||
restart: always
|
||||
|
||||
lemmy:
|
||||
image: dessalines/lemmy:v0.6.77
|
||||
image: dessalines/lemmy:v0.6.79
|
||||
ports:
|
||||
- "127.0.0.1:8536:8536"
|
||||
restart: always
|
||||
|
@ -22,17 +22,17 @@ services:
|
|||
- ./lemmy.hjson:/config/config.hjson
|
||||
depends_on:
|
||||
- postgres
|
||||
- pictshare
|
||||
- pictrs
|
||||
- iframely
|
||||
|
||||
pictshare:
|
||||
image: hascheksolutions/pictshare:latest
|
||||
ports:
|
||||
- "127.0.0.1:8537:80"
|
||||
pictrs:
|
||||
image: asonix/pictrs:v0.1.13-r0
|
||||
ports:
|
||||
- "127.0.0.1:8537:8080"
|
||||
user: 991:991
|
||||
volumes:
|
||||
- ./volumes/pictshare:/usr/share/nginx/html/data
|
||||
- ./volumes/pictrs:/mnt
|
||||
restart: always
|
||||
mem_limit: 100m
|
||||
|
||||
iframely:
|
||||
image: dogbin/iframely:latest
|
||||
|
|
60
docker/prod/migrate-pictshare-to-pictrs.bash
vendored
Normal file
60
docker/prod/migrate-pictshare-to-pictrs.bash
vendored
Normal file
|
@ -0,0 +1,60 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
if [[ $(id -u) != 0 ]]; then
|
||||
echo "This migration needs to be run as root"
|
||||
exit
|
||||
fi
|
||||
|
||||
if [[ ! -f docker-compose.yml ]]; then
|
||||
echo "No docker-compose.yml found in current directory. Is this the right folder?"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Fixing pictrs permissions
|
||||
mkdir -p volumes/pictrs
|
||||
sudo chown -R 991:991 volumes/pictrs
|
||||
|
||||
echo "Restarting docker-compose, making sure that pictrs is started and pictshare is removed"
|
||||
docker-compose up -d --remove-orphans
|
||||
|
||||
if [[ -z $(docker-compose ps | grep pictrs) ]]; then
|
||||
echo "Pict-rs is not running, make sure you update Lemmy first"
|
||||
exit
|
||||
fi
|
||||
|
||||
# echo "Stopping Lemmy so that users dont upload new images during the migration"
|
||||
# docker-compose stop lemmy
|
||||
|
||||
pushd volumes/pictshare/
|
||||
echo "Importing pictshare images to pict-rs..."
|
||||
IMAGE_NAMES=*
|
||||
for image in $IMAGE_NAMES; do
|
||||
IMAGE_PATH="$(pwd)/$image/$image"
|
||||
if [[ ! -f $IMAGE_PATH ]]; then
|
||||
continue
|
||||
fi
|
||||
echo -e "\nImporting $IMAGE_PATH"
|
||||
ret=0
|
||||
curl --silent --fail -F "images[]=@$IMAGE_PATH" http://127.0.0.1:8537/import || ret=$?
|
||||
if [[ $ret != 0 ]]; then
|
||||
echo "Error for $IMAGE_PATH : $ret"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Fixing permissions on pictshare folder"
|
||||
find . -type d -exec chmod 755 {} \;
|
||||
find . -type f -exec chmod 644 {} \;
|
||||
|
||||
popd
|
||||
|
||||
echo "Rewrite image links in Lemmy database"
|
||||
docker-compose exec -u postgres postgres psql -U lemmy -c "UPDATE user_ SET avatar = REPLACE(avatar, 'pictshare', 'pictrs/image') WHERE avatar is not null;"
|
||||
docker-compose exec -u postgres postgres psql -U lemmy -c "UPDATE post SET url = REPLACE(url, 'pictshare', 'pictrs/image') WHERE url is not null;"
|
||||
|
||||
echo "Moving pictshare data folder to pictshare_backup"
|
||||
mv volumes/pictshare volumes/pictshare_backup
|
||||
|
||||
echo "Migration done, starting Lemmy again"
|
||||
echo "If everything went well, you can delete ./volumes/pictshare_backup/"
|
||||
docker-compose start lemmy
|
|
@ -1,4 +1,5 @@
|
|||
use super::*;
|
||||
use crate::is_valid_community_name;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GetCommunity {
|
||||
|
@ -220,6 +221,10 @@ impl Perform for Oper<CreateCommunity> {
|
|||
}
|
||||
}
|
||||
|
||||
if !is_valid_community_name(&data.name) {
|
||||
return Err(APIError::err("invalid_community_name").into());
|
||||
}
|
||||
|
||||
let user_id = claims.id;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
@ -306,6 +311,10 @@ impl Perform for Oper<EditCommunity> {
|
|||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
if !is_valid_community_name(&data.name) {
|
||||
return Err(APIError::err("invalid_community_name").into());
|
||||
}
|
||||
|
||||
let user_id = claims.id;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
|
|
@ -18,7 +18,7 @@ use crate::db::user_mention_view::*;
|
|||
use crate::db::user_view::*;
|
||||
use crate::db::*;
|
||||
use crate::{
|
||||
extract_usernames, fetch_iframely_and_pictshare_data, generate_random_string, naive_from_unix,
|
||||
extract_usernames, fetch_iframely_and_pictrs_data, generate_random_string, naive_from_unix,
|
||||
naive_now, remove_slurs, send_email, slur_check, slurs_vec_to_str,
|
||||
};
|
||||
|
||||
|
|
|
@ -116,9 +116,9 @@ impl Perform 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());
|
||||
// Fetch Iframely and pictrs cached image
|
||||
let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
|
||||
fetch_iframely_and_pictrs_data(data.url.to_owned());
|
||||
|
||||
let post_form = PostForm {
|
||||
name: data.name.to_owned(),
|
||||
|
@ -135,7 +135,7 @@ impl Perform for Oper<CreatePost> {
|
|||
embed_title: iframely_title,
|
||||
embed_description: iframely_description,
|
||||
embed_html: iframely_html,
|
||||
thumbnail_url: pictshare_thumbnail,
|
||||
thumbnail_url: pictrs_thumbnail,
|
||||
};
|
||||
|
||||
let inserted_post = match Post::create(&conn, &post_form) {
|
||||
|
@ -450,9 +450,9 @@ impl Perform 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());
|
||||
// Fetch Iframely and Pictrs cached image
|
||||
let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
|
||||
fetch_iframely_and_pictrs_data(data.url.to_owned());
|
||||
|
||||
let post_form = PostForm {
|
||||
name: data.name.to_owned(),
|
||||
|
@ -469,7 +469,7 @@ impl Perform for Oper<EditPost> {
|
|||
embed_title: iframely_title,
|
||||
embed_description: iframely_description,
|
||||
embed_html: iframely_html,
|
||||
thumbnail_url: pictshare_thumbnail,
|
||||
thumbnail_url: pictrs_thumbnail,
|
||||
};
|
||||
|
||||
let _updated_post = match Post::update(&conn, data.edit_id, &post_form) {
|
||||
|
|
|
@ -187,25 +187,35 @@ pub fn fetch_iframely(url: &str) -> Result<IframelyResponse, failure::Error> {
|
|||
Ok(res)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct PictshareResponse {
|
||||
status: String,
|
||||
url: String,
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct PictrsResponse {
|
||||
files: Vec<PictrsFile>,
|
||||
msg: String,
|
||||
}
|
||||
|
||||
pub fn fetch_pictshare(image_url: &str) -> Result<PictshareResponse, failure::Error> {
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct PictrsFile {
|
||||
file: String,
|
||||
delete_token: String,
|
||||
}
|
||||
|
||||
pub fn fetch_pictrs(image_url: &str) -> Result<PictrsResponse, failure::Error> {
|
||||
is_image_content_type(image_url)?;
|
||||
|
||||
let fetch_url = format!(
|
||||
"http://pictshare/api/geturl.php?url={}",
|
||||
utf8_percent_encode(image_url, NON_ALPHANUMERIC)
|
||||
"http://pictrs:8080/image/download?url={}",
|
||||
utf8_percent_encode(image_url, NON_ALPHANUMERIC) // TODO this might not be needed
|
||||
);
|
||||
let text = attohttpc::get(&fetch_url).send()?.text()?;
|
||||
let res: PictshareResponse = serde_json::from_str(&text)?;
|
||||
Ok(res)
|
||||
let res: PictrsResponse = serde_json::from_str(&text)?;
|
||||
if res.msg == "ok" {
|
||||
Ok(res)
|
||||
} else {
|
||||
Err(format_err!("{}", &res.msg))
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_iframely_and_pictshare_data(
|
||||
fn fetch_iframely_and_pictrs_data(
|
||||
url: Option<String>,
|
||||
) -> (
|
||||
Option<String>,
|
||||
|
@ -225,20 +235,20 @@ fn fetch_iframely_and_pictshare_data(
|
|||
}
|
||||
};
|
||||
|
||||
// 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),
|
||||
// Fetch pictrs thumbnail
|
||||
let pictrs_thumbnail = match iframely_thumbnail_url {
|
||||
Some(iframely_thumbnail_url) => match fetch_pictrs(&iframely_thumbnail_url) {
|
||||
Ok(res) => Some(res.files[0].file.to_owned()),
|
||||
Err(e) => {
|
||||
error!("pictshare err: {}", e);
|
||||
error!("pictrs err: {}", e);
|
||||
None
|
||||
}
|
||||
},
|
||||
// Try to generate a small thumbnail if iframely is not supported
|
||||
None => match fetch_pictshare(&url) {
|
||||
Ok(res) => Some(res.url),
|
||||
None => match fetch_pictrs(&url) {
|
||||
Ok(res) => Some(res.files[0].file.to_owned()),
|
||||
Err(e) => {
|
||||
error!("pictshare err: {}", e);
|
||||
error!("pictrs err: {}", e);
|
||||
None
|
||||
}
|
||||
},
|
||||
|
@ -248,7 +258,7 @@ fn fetch_iframely_and_pictshare_data(
|
|||
iframely_title,
|
||||
iframely_description,
|
||||
iframely_html,
|
||||
pictshare_thumbnail,
|
||||
pictrs_thumbnail,
|
||||
)
|
||||
}
|
||||
None => (None, None, None, None),
|
||||
|
@ -273,11 +283,15 @@ pub fn is_valid_username(name: &str) -> bool {
|
|||
VALID_USERNAME_REGEX.is_match(name)
|
||||
}
|
||||
|
||||
pub fn is_valid_community_name(name: &str) -> bool {
|
||||
VALID_COMMUNITY_NAME_REGEX.is_match(name)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
extract_usernames, is_email_regex, is_image_content_type, is_valid_username, remove_slurs,
|
||||
slur_check, slurs_vec_to_str,
|
||||
extract_usernames, is_email_regex, is_image_content_type, is_valid_community_name,
|
||||
is_valid_username, remove_slurs, slur_check, slurs_vec_to_str,
|
||||
};
|
||||
|
||||
#[test]
|
||||
|
@ -304,6 +318,15 @@ mod tests {
|
|||
assert!(!is_valid_username(""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_community_name() {
|
||||
assert!(is_valid_community_name("example"));
|
||||
assert!(is_valid_community_name("example_community"));
|
||||
assert!(!is_valid_community_name("Example"));
|
||||
assert!(!is_valid_community_name("Ex"));
|
||||
assert!(!is_valid_community_name(""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slur_filter() {
|
||||
let test =
|
||||
|
@ -366,4 +389,5 @@ lazy_static! {
|
|||
static ref SLUR_REGEX: Regex = RegexBuilder::new(r"(fag(g|got|tard)?|maricos?|cock\s?sucker(s|ing)?|nig(\b|g?(a|er)?(s|z)?)\b|dindu(s?)|mudslime?s?|kikes?|mongoloids?|towel\s*heads?|\bspi(c|k)s?\b|\bchinks?|niglets?|beaners?|\bnips?\b|\bcoons?\b|jungle\s*bunn(y|ies?)|jigg?aboo?s?|\bpakis?\b|rag\s*heads?|gooks?|cunts?|bitch(es|ing|y)?|puss(y|ies?)|twats?|feminazis?|whor(es?|ing)|\bslut(s|t?y)?|\btrann?(y|ies?)|ladyboy(s?)|\b(b|re|r)tard(ed)?s?)").case_insensitive(true).build().unwrap();
|
||||
static ref USERNAME_MATCHES_REGEX: Regex = Regex::new(r"/u/[a-zA-Z][0-9a-zA-Z_]*").unwrap();
|
||||
static ref VALID_USERNAME_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9_]{3,20}$").unwrap();
|
||||
static ref VALID_COMMUNITY_NAME_REGEX: Regex = Regex::new(r"^[a-z0-9_]{3,20}$").unwrap();
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
pub const VERSION: &str = "v0.6.77";
|
||||
pub const VERSION: &str = "v0.6.79";
|
||||
|
|
45
ui/src/components/comment-form.tsx
vendored
45
ui/src/components/comment-form.tsx
vendored
|
@ -18,6 +18,7 @@ import {
|
|||
setupTribute,
|
||||
wsJsonToRes,
|
||||
emojiPicker,
|
||||
pictrsDeleteToast,
|
||||
} from '../utils';
|
||||
import { WebSocketService, UserService } from '../services';
|
||||
import autosize from 'autosize';
|
||||
|
@ -162,8 +163,9 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||
</button>
|
||||
{this.state.commentForm.content && (
|
||||
<button
|
||||
className={`btn btn-sm mr-2 btn-secondary ${this.state
|
||||
.previewMode && 'active'}`}
|
||||
className={`btn btn-sm mr-2 btn-secondary ${
|
||||
this.state.previewMode && 'active'
|
||||
}`}
|
||||
onClick={linkEvent(this, this.handlePreviewToggle)}
|
||||
>
|
||||
{i18n.t('preview')}
|
||||
|
@ -304,9 +306,9 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||
file = event;
|
||||
}
|
||||
|
||||
const imageUploadUrl = `/pictshare/api/upload.php`;
|
||||
const imageUploadUrl = `/pictrs/image`;
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('images[]', file);
|
||||
|
||||
i.state.imageLoading = true;
|
||||
i.setState(i.state);
|
||||
|
@ -317,16 +319,31 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
let url = `${window.location.origin}/pictshare/${res.url}`;
|
||||
let imageMarkdown =
|
||||
res.filetype == 'mp4' ? `[vid](${url}/raw)` : `![](${url})`;
|
||||
let content = i.state.commentForm.content;
|
||||
content = content ? `${content}\n${imageMarkdown}` : imageMarkdown;
|
||||
i.state.commentForm.content = content;
|
||||
i.state.imageLoading = false;
|
||||
i.setState(i.state);
|
||||
let textarea: any = document.getElementById(i.id);
|
||||
autosize.update(textarea);
|
||||
console.log('pictrs upload:');
|
||||
console.log(res);
|
||||
if (res.msg == 'ok') {
|
||||
let hash = res.files[0].file;
|
||||
let url = `${window.location.origin}/pictrs/image/${hash}`;
|
||||
let deleteToken = res.files[0].delete_token;
|
||||
let deleteUrl = `${window.location.origin}/pictrs/image/delete/${deleteToken}/${hash}`;
|
||||
let imageMarkdown = `![](${url})`;
|
||||
let content = i.state.commentForm.content;
|
||||
content = content ? `${content}\n${imageMarkdown}` : imageMarkdown;
|
||||
i.state.commentForm.content = content;
|
||||
i.state.imageLoading = false;
|
||||
i.setState(i.state);
|
||||
let textarea: any = document.getElementById(i.id);
|
||||
autosize.update(textarea);
|
||||
pictrsDeleteToast(
|
||||
i18n.t('click_to_delete_picture'),
|
||||
i18n.t('picture_deleted'),
|
||||
deleteUrl
|
||||
);
|
||||
} else {
|
||||
i.state.imageLoading = false;
|
||||
i.setState(i.state);
|
||||
toast(JSON.stringify(res), 'danger');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
i.state.imageLoading = false;
|
||||
|
|
6
ui/src/components/navbar.tsx
vendored
6
ui/src/components/navbar.tsx
vendored
|
@ -22,7 +22,7 @@ import {
|
|||
} from '../interfaces';
|
||||
import {
|
||||
wsJsonToRes,
|
||||
pictshareAvatarThumbnail,
|
||||
pictrsAvatarThumbnail,
|
||||
showAvatars,
|
||||
fetchLimit,
|
||||
isCommentType,
|
||||
|
@ -218,7 +218,7 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
<span>
|
||||
{UserService.Instance.user.avatar && showAvatars() && (
|
||||
<img
|
||||
src={pictshareAvatarThumbnail(
|
||||
src={pictrsAvatarThumbnail(
|
||||
UserService.Instance.user.avatar
|
||||
)}
|
||||
height="32"
|
||||
|
@ -381,7 +381,7 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
|
||||
requestNotificationPermission() {
|
||||
if (UserService.Instance.user) {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
if (!Notification) {
|
||||
toast(i18n.t('notifications_error'), 'danger');
|
||||
return;
|
||||
|
|
30
ui/src/components/post-form.tsx
vendored
30
ui/src/components/post-form.tsx
vendored
|
@ -35,6 +35,7 @@ import {
|
|||
setupTribute,
|
||||
setupTippy,
|
||||
emojiPicker,
|
||||
pictrsDeleteToast,
|
||||
} from '../utils';
|
||||
import autosize from 'autosize';
|
||||
import Tribute from 'tributejs/src/Tribute.js';
|
||||
|
@ -518,9 +519,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
file = event;
|
||||
}
|
||||
|
||||
const imageUploadUrl = `/pictshare/api/upload.php`;
|
||||
const imageUploadUrl = `/pictrs/image`;
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('images[]', file);
|
||||
|
||||
i.state.imageLoading = true;
|
||||
i.setState(i.state);
|
||||
|
@ -531,13 +532,26 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
let url = `${window.location.origin}/pictshare/${encodeURI(res.url)}`;
|
||||
if (res.filetype == 'mp4') {
|
||||
url += '/raw';
|
||||
console.log('pictrs upload:');
|
||||
console.log(res);
|
||||
if (res.msg == 'ok') {
|
||||
let hash = res.files[0].file;
|
||||
let url = `${window.location.origin}/pictrs/image/${hash}`;
|
||||
let deleteToken = res.files[0].delete_token;
|
||||
let deleteUrl = `${window.location.origin}/pictrs/image/delete/${deleteToken}/${hash}`;
|
||||
i.state.postForm.url = url;
|
||||
i.state.imageLoading = false;
|
||||
i.setState(i.state);
|
||||
pictrsDeleteToast(
|
||||
i18n.t('click_to_delete_picture'),
|
||||
i18n.t('picture_deleted'),
|
||||
deleteUrl
|
||||
);
|
||||
} else {
|
||||
i.state.imageLoading = false;
|
||||
i.setState(i.state);
|
||||
toast(JSON.stringify(res), 'danger');
|
||||
}
|
||||
i.state.postForm.url = url;
|
||||
i.state.imageLoading = false;
|
||||
i.setState(i.state);
|
||||
})
|
||||
.catch(error => {
|
||||
i.state.imageLoading = false;
|
||||
|
|
10
ui/src/components/post-listing.tsx
vendored
10
ui/src/components/post-listing.tsx
vendored
|
@ -28,7 +28,7 @@ import {
|
|||
isImage,
|
||||
isVideo,
|
||||
getUnixTime,
|
||||
pictshareImage,
|
||||
pictrsImage,
|
||||
setupTippy,
|
||||
previewLines,
|
||||
} from '../utils';
|
||||
|
@ -161,15 +161,15 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
getImage(thumbnail: boolean = false) {
|
||||
let post = this.props.post;
|
||||
if (isImage(post.url)) {
|
||||
if (post.url.includes('pictshare')) {
|
||||
return pictshareImage(post.url, thumbnail);
|
||||
if (post.url.includes('pictrs')) {
|
||||
return pictrsImage(post.url, thumbnail);
|
||||
} else if (post.thumbnail_url) {
|
||||
return pictshareImage(post.thumbnail_url, thumbnail);
|
||||
return pictrsImage(post.thumbnail_url, thumbnail);
|
||||
} else {
|
||||
return post.url;
|
||||
}
|
||||
} else if (post.thumbnail_url) {
|
||||
return pictshareImage(post.thumbnail_url, thumbnail);
|
||||
return pictrsImage(post.thumbnail_url, thumbnail);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
24
ui/src/components/private-message.tsx
vendored
24
ui/src/components/private-message.tsx
vendored
|
@ -5,12 +5,7 @@ import {
|
|||
EditPrivateMessageForm,
|
||||
} from '../interfaces';
|
||||
import { WebSocketService, UserService } from '../services';
|
||||
import {
|
||||
mdToHtml,
|
||||
pictshareAvatarThumbnail,
|
||||
showAvatars,
|
||||
toast,
|
||||
} from '../utils';
|
||||
import { mdToHtml, pictrsAvatarThumbnail, showAvatars, toast } from '../utils';
|
||||
import { MomentTime } from './moment-time';
|
||||
import { PrivateMessageForm } from './private-message-form';
|
||||
import { i18n } from '../i18next';
|
||||
|
@ -78,7 +73,7 @@ export class PrivateMessage extends Component<
|
|||
<img
|
||||
height="32"
|
||||
width="32"
|
||||
src={pictshareAvatarThumbnail(
|
||||
src={pictrsAvatarThumbnail(
|
||||
this.mine
|
||||
? message.recipient_avatar
|
||||
: message.creator_avatar
|
||||
|
@ -144,8 +139,9 @@ export class PrivateMessage extends Component<
|
|||
}
|
||||
>
|
||||
<svg
|
||||
class={`icon icon-inline ${message.read &&
|
||||
'text-success'}`}
|
||||
class={`icon icon-inline ${
|
||||
message.read && 'text-success'
|
||||
}`}
|
||||
>
|
||||
<use xlinkHref="#icon-check"></use>
|
||||
</svg>
|
||||
|
@ -188,8 +184,9 @@ export class PrivateMessage extends Component<
|
|||
}
|
||||
>
|
||||
<svg
|
||||
class={`icon icon-inline ${message.deleted &&
|
||||
'text-danger'}`}
|
||||
class={`icon icon-inline ${
|
||||
message.deleted && 'text-danger'
|
||||
}`}
|
||||
>
|
||||
<use xlinkHref="#icon-trash"></use>
|
||||
</svg>
|
||||
|
@ -204,8 +201,9 @@ export class PrivateMessage extends Component<
|
|||
data-tippy-content={i18n.t('view_source')}
|
||||
>
|
||||
<svg
|
||||
class={`icon icon-inline ${this.state.viewSource &&
|
||||
'text-success'}`}
|
||||
class={`icon icon-inline ${
|
||||
this.state.viewSource && 'text-success'
|
||||
}`}
|
||||
>
|
||||
<use xlinkHref="#icon-file-text"></use>
|
||||
</svg>
|
||||
|
|
2
ui/src/components/search.tsx
vendored
2
ui/src/components/search.tsx
vendored
|
@ -22,7 +22,7 @@ import {
|
|||
fetchLimit,
|
||||
routeSearchTypeToEnum,
|
||||
routeSortTypeToEnum,
|
||||
pictshareAvatarThumbnail,
|
||||
pictrsAvatarThumbnail,
|
||||
showAvatars,
|
||||
toast,
|
||||
createCommentLikeRes,
|
||||
|
|
2
ui/src/components/sidebar.tsx
vendored
2
ui/src/components/sidebar.tsx
vendored
|
@ -11,7 +11,7 @@ import { WebSocketService, UserService } from '../services';
|
|||
import {
|
||||
mdToHtml,
|
||||
getUnixTime,
|
||||
pictshareAvatarThumbnail,
|
||||
pictrsAvatarThumbnail,
|
||||
showAvatars,
|
||||
} from '../utils';
|
||||
import { CommunityForm } from './community-form';
|
||||
|
|
4
ui/src/components/user-listing.tsx
vendored
4
ui/src/components/user-listing.tsx
vendored
|
@ -1,7 +1,7 @@
|
|||
import { Component } from 'inferno';
|
||||
import { Link } from 'inferno-router';
|
||||
import { UserView } from '../interfaces';
|
||||
import { pictshareAvatarThumbnail, showAvatars } from '../utils';
|
||||
import { pictrsAvatarThumbnail, showAvatars } from '../utils';
|
||||
|
||||
interface UserOther {
|
||||
name: string;
|
||||
|
@ -25,7 +25,7 @@ export class UserListing extends Component<UserListingProps, any> {
|
|||
<img
|
||||
height="32"
|
||||
width="32"
|
||||
src={pictshareAvatarThumbnail(user.avatar)}
|
||||
src={pictrsAvatarThumbnail(user.avatar)}
|
||||
class="rounded-circle mr-2"
|
||||
/>
|
||||
)}
|
||||
|
|
23
ui/src/components/user.tsx
vendored
23
ui/src/components/user.tsx
vendored
|
@ -988,9 +988,9 @@ export class User extends Component<any, UserState> {
|
|||
handleImageUpload(i: User, event: any) {
|
||||
event.preventDefault();
|
||||
let file = event.target.files[0];
|
||||
const imageUploadUrl = `/pictshare/api/upload.php`;
|
||||
const imageUploadUrl = `/pictrs/image`;
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('images[]', file);
|
||||
|
||||
i.state.avatarLoading = true;
|
||||
i.setState(i.state);
|
||||
|
@ -1001,14 +1001,19 @@ export class User extends Component<any, UserState> {
|
|||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
let url = `${window.location.origin}/pictshare/${res.url}`;
|
||||
if (res.filetype == 'mp4') {
|
||||
url += '/raw';
|
||||
console.log('pictrs upload:');
|
||||
console.log(res);
|
||||
if (res.msg == 'ok') {
|
||||
let hash = res.files[0].file;
|
||||
let url = `${window.location.origin}/pictrs/image/${hash}`;
|
||||
i.state.userSettingsForm.avatar = url;
|
||||
i.state.avatarLoading = false;
|
||||
i.setState(i.state);
|
||||
} else {
|
||||
i.state.avatarLoading = false;
|
||||
i.setState(i.state);
|
||||
toast(JSON.stringify(res), 'danger');
|
||||
}
|
||||
i.state.userSettingsForm.avatar = url;
|
||||
console.log(url);
|
||||
i.state.avatarLoading = false;
|
||||
i.setState(i.state);
|
||||
})
|
||||
.catch(error => {
|
||||
i.state.avatarLoading = false;
|
||||
|
|
62
ui/src/utils.ts
vendored
62
ui/src/utils.ts
vendored
|
@ -414,12 +414,16 @@ export function setTheme(theme: string = 'darkly', loggedIn: boolean = false) {
|
|||
}
|
||||
|
||||
// if the user is not logged in, we load the default themes and let the browser decide
|
||||
if(!loggedIn) {
|
||||
document.getElementById("default-light").removeAttribute('disabled')
|
||||
document.getElementById("default-dark").removeAttribute('disabled')
|
||||
if (!loggedIn) {
|
||||
document.getElementById('default-light').removeAttribute('disabled');
|
||||
document.getElementById('default-dark').removeAttribute('disabled');
|
||||
} else {
|
||||
document.getElementById("default-light").setAttribute('disabled', 'disabled');
|
||||
document.getElementById("default-dark").setAttribute('disabled', 'disabled');
|
||||
document
|
||||
.getElementById('default-light')
|
||||
.setAttribute('disabled', 'disabled');
|
||||
document
|
||||
.getElementById('default-dark')
|
||||
.setAttribute('disabled', 'disabled');
|
||||
|
||||
// Load the theme dynamically
|
||||
let cssLoc = `/static/assets/css/themes/${theme}.min.css`;
|
||||
|
@ -449,10 +453,12 @@ export function objectFlip(obj: any) {
|
|||
return ret;
|
||||
}
|
||||
|
||||
export function pictshareAvatarThumbnail(src: string): string {
|
||||
// sample url: http://localhost:8535/pictshare/gs7xuu.jpg
|
||||
let split = src.split('pictshare');
|
||||
let out = `${split[0]}pictshare/${canUseWebP() ? 'webp/' : ''}96${split[1]}`;
|
||||
export function pictrsAvatarThumbnail(src: string): string {
|
||||
// sample url: http://localhost:8535/pictrs/image/thumbnail256/gs7xuu.jpg
|
||||
let split = src.split('/pictrs/image');
|
||||
let out = `${split[0]}/pictrs/image/${
|
||||
canUseWebP() ? 'webp/' : ''
|
||||
}thumbnail96${split[1]}`;
|
||||
return out;
|
||||
}
|
||||
|
||||
|
@ -464,21 +470,18 @@ export function showAvatars(): boolean {
|
|||
}
|
||||
|
||||
// Converts to image thumbnail
|
||||
export function pictshareImage(
|
||||
hash: string,
|
||||
thumbnail: boolean = false
|
||||
): string {
|
||||
let root = `/pictshare`;
|
||||
export function pictrsImage(hash: string, thumbnail: boolean = false): string {
|
||||
let root = `/pictrs/image`;
|
||||
|
||||
// Necessary for other servers / domains
|
||||
if (hash.includes('pictshare')) {
|
||||
let split = hash.split('/pictshare/');
|
||||
root = `${split[0]}/pictshare`;
|
||||
if (hash.includes('pictrs')) {
|
||||
let split = hash.split('/pictrs/image/');
|
||||
root = `${split[0]}/pictrs/image`;
|
||||
hash = split[1];
|
||||
}
|
||||
|
||||
let out = `${root}/${canUseWebP() ? 'webp/' : ''}${
|
||||
thumbnail ? '192/' : ''
|
||||
thumbnail ? 'thumbnail256/' : ''
|
||||
}${hash}`;
|
||||
return out;
|
||||
}
|
||||
|
@ -497,6 +500,29 @@ export function toast(text: string, background: string = 'success') {
|
|||
}).showToast();
|
||||
}
|
||||
|
||||
export function pictrsDeleteToast(
|
||||
clickToDeleteText: string,
|
||||
deletePictureText: string,
|
||||
deleteUrl: string
|
||||
) {
|
||||
let backgroundColor = `var(--light)`;
|
||||
let toast = Toastify({
|
||||
text: clickToDeleteText,
|
||||
backgroundColor: backgroundColor,
|
||||
gravity: 'top',
|
||||
position: 'right',
|
||||
duration: 0,
|
||||
onClick: () => {
|
||||
if (toast) {
|
||||
window.location.replace(deleteUrl);
|
||||
alert(deletePictureText);
|
||||
toast.hideToast();
|
||||
}
|
||||
},
|
||||
close: true,
|
||||
}).showToast();
|
||||
}
|
||||
|
||||
export function messageToastify(
|
||||
creator: string,
|
||||
avatar: string,
|
||||
|
|
2
ui/src/version.ts
vendored
2
ui/src/version.ts
vendored
|
@ -1 +1 @@
|
|||
export const version: string = 'v0.6.77';
|
||||
export const version: string = 'v0.6.79';
|
||||
|
|
3
ui/translations/en.json
vendored
3
ui/translations/en.json
vendored
|
@ -27,6 +27,7 @@
|
|||
"number_of_communities": "{{count}} Community",
|
||||
"number_of_communities_plural": "{{count}} Communities",
|
||||
"community_reqs": "lowercase, underscores, and no spaces.",
|
||||
"invalid_community_name": "Invalid name.",
|
||||
"create_private_message": "Create Private Message",
|
||||
"send_secure_message": "Send Secure Message",
|
||||
"send_message": "Send Message",
|
||||
|
@ -75,6 +76,8 @@
|
|||
"delete_account": "Delete Account",
|
||||
"delete_account_confirm":
|
||||
"Warning: this will permanently delete all your data. Enter your password to confirm.",
|
||||
"click_to_delete_picture": "Click to delete picture.",
|
||||
"picture_deleted": "Picture deleted.",
|
||||
"restore": "restore",
|
||||
"ban": "ban",
|
||||
"ban_from_site": "ban from site",
|
||||
|
|
16
ui/translations/es.json
vendored
16
ui/translations/es.json
vendored
|
@ -5,7 +5,7 @@
|
|||
"create_a_post": "Crear una publicación",
|
||||
"create_post": "Crear Publicación",
|
||||
"number_of_posts": "{{count}} Publicación",
|
||||
"number_of_posts_plural": "{{count}} Publicaciónes",
|
||||
"number_of_posts_plural": "{{count}} Publicaciones",
|
||||
"posts": "Publicaciones",
|
||||
"related_posts": "Estas publicaciones podrían estar relacionadas",
|
||||
"cross_posts": "Este link también ha sido publicado en:",
|
||||
|
@ -57,16 +57,16 @@
|
|||
"remove_as_admin": "eliminar como administrador",
|
||||
"appoint_as_admin": "designar como administrador",
|
||||
"remove": "eliminar",
|
||||
"removed": "eliminado",
|
||||
"removed": "eliminado por moderador",
|
||||
"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",
|
||||
"deleted": "eliminado por creador",
|
||||
"delete_account": "Eliminar Cuenta",
|
||||
"delete_account_confirm": "Aviso: esta acción eliminará permanentemente tu información. Introduce tu contraseña para continuar",
|
||||
"delete_account_confirm": "Advertencia: esta acción eliminará permanentemente toda tu información. Introduce tu contraseña para confirmar.",
|
||||
"restore": "restaurar",
|
||||
"ban": "expulsar",
|
||||
"ban_from_site": "expulsar del sitio",
|
||||
|
@ -169,7 +169,7 @@
|
|||
"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:",
|
||||
"sponsor_message": "Lemmy es software libre y de <1>código abierto</1>, lo que significa que nunca tendrá publicidad, monetización, ni capitales emprendedores. 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",
|
||||
|
@ -250,6 +250,8 @@
|
|||
"banned_users": "Usuarios Baneados",
|
||||
"support_on_open_collective": "Dona en OpenCollective",
|
||||
"site_saved": "Sitio Guardado.",
|
||||
"emoji_picker": "Emoji Picker",
|
||||
"admin_settings": "Panel de Administración"
|
||||
"emoji_picker": "Lista de emojis",
|
||||
"admin_settings": "Panel de Administración",
|
||||
"select_a_community": "Selecciona una comunidad",
|
||||
"invalid_username": "Nombre de usuario inválido."
|
||||
}
|
||||
|
|
2
ui/translations/fr.json
vendored
2
ui/translations/fr.json
vendored
|
@ -36,7 +36,7 @@
|
|||
"preview": "prévisualiser",
|
||||
"upload_image": "envoyer une image",
|
||||
"avatar": "Avatar",
|
||||
"upload_avatar": "Télécharger une avatar",
|
||||
"upload_avatar": "Télécharger un avatar",
|
||||
"show_avatars": "Afficher les avatars",
|
||||
"formatting_help": "aide au formattage",
|
||||
"view_source": "voir la source",
|
||||
|
|
95
ui/translations/hu.json
vendored
95
ui/translations/hu.json
vendored
|
@ -2,8 +2,8 @@
|
|||
"post": "Elküld",
|
||||
"remove_post": "Bejegyzés eltávolítása",
|
||||
"no_posts": "Nincs bejegyzés.",
|
||||
"create_post": "Új bejegyzés létrehozása",
|
||||
"create_a_post": "Új bejegyzés létrehozása",
|
||||
"create_post": "Bejegyzés létrehozása",
|
||||
"create_a_post": "Bejegyzés létrehozása",
|
||||
"number_of_posts": "{{count}} bejegyzés",
|
||||
"number_of_posts_plural": "{{count}} bejegyzés",
|
||||
"posts": "Bejegyzések",
|
||||
|
@ -14,5 +14,94 @@
|
|||
"remove_comment": "Hozzászólások eltávolítása",
|
||||
"cross_posted_to": "beküldve ide is: ",
|
||||
"number_of_comments": "{{count}} hozzászólás",
|
||||
"number_of_comments_plural": "{{count}} hozzászólás"
|
||||
"number_of_comments_plural": "{{count}} hozzászólás",
|
||||
"communities": "Közösségek",
|
||||
"users": "Felhasználók",
|
||||
"create_a_community": "Közösség létrehozása",
|
||||
"select_a_community": "Közösség kiválasztása",
|
||||
"create_community": "Közösség létrehozása",
|
||||
"remove_community": "Közösség eltávolítása",
|
||||
"trending_communities": "Népszerű <1>közösségek</1>",
|
||||
"list_of_communities": "Közösségek listája",
|
||||
"community_reqs": "Kisbetű és alsóvonás megengedett, szóköz nem.",
|
||||
"create_private_message": "Privát üzenet létrehozása",
|
||||
"send_secure_message": "Biztonságos üzenet küldése",
|
||||
"send_message": "Üzenet küldése",
|
||||
"message": "Üzenet",
|
||||
"edit": "szerkesztés",
|
||||
"reply": "válasz",
|
||||
"more": "több",
|
||||
"cancel": "Mégse",
|
||||
"preview": "Előnézet",
|
||||
"upload_image": "kép feltöltése",
|
||||
"avatar": "Avatár",
|
||||
"upload_avatar": "Avatár feltöltése",
|
||||
"show_avatars": "Avatárok mutatása",
|
||||
"show_context": "Összefüggés mutatása",
|
||||
"sorting_help": "rendezési segítség",
|
||||
"view_source": "forrás megtekintése",
|
||||
"unlock": "zárolás feloldása",
|
||||
"lock": "zárolás",
|
||||
"sticky": "rögzítés",
|
||||
"unsticky": "rögzítés feloldása",
|
||||
"link": "hivatkozás",
|
||||
"mod": "moderátor",
|
||||
"mods": "moderátorok",
|
||||
"moderates": "Moderált közösségek",
|
||||
"settings": "Beállítások",
|
||||
"admin_settings": "Adminisztrációs beállítások",
|
||||
"remove_as_mod": "moderátori jog eltávolítása",
|
||||
"appoint_as_mod": "kinevezés moderátornak",
|
||||
"modlog": "Moderációs napló",
|
||||
"admin": "admin",
|
||||
"admins": "adminok",
|
||||
"remove_as_admin": "adminjog eltávolítása",
|
||||
"appoint_as_admin": "kinevezés adminnak",
|
||||
"remove": "eltávolítás",
|
||||
"locked": "zárolva",
|
||||
"stickied": "rögzítve",
|
||||
"reason": "Indok",
|
||||
"mark_as_read": "megjelölés olvasottnak",
|
||||
"mark_as_unread": "megjelölés olvasatlannak",
|
||||
"delete": "törlés",
|
||||
"deleted": "eltávolítva a szerző által",
|
||||
"delete_account": "FIók törlése",
|
||||
"restore": "visszaállítás",
|
||||
"ban": "kitiltás",
|
||||
"ban_from_site": "kitiltás az oldalról",
|
||||
"unban": "kitiltás visszavonása",
|
||||
"unban_from_site": "az oldalról történő kitiltás visszavonása",
|
||||
"banned": "kitiltva",
|
||||
"banned_users": "Kitiltott felhasználók",
|
||||
"save": "mentés",
|
||||
"unsave": "mentés visszavonása",
|
||||
"create": "létrehozás",
|
||||
"creator": "szerző",
|
||||
"username": "Felhasználónév",
|
||||
"number_of_points": "{{count}} pont",
|
||||
"number_of_points_plural": "{{count}} pont",
|
||||
"number_of_subscribers": "{{count}} feliratkozó",
|
||||
"number_of_subscribers_plural": "{{count}} feliratkozó",
|
||||
"name": "Név",
|
||||
"title": "Cím",
|
||||
"category": "Kategória",
|
||||
"both": "Mindkettő",
|
||||
"saved": "Mentve",
|
||||
"unsubscribe": "Leiratkozás",
|
||||
"subscribe": "Feliratkozás",
|
||||
"subscribed": "Feliratkozva",
|
||||
"subscribed_to_communities": "Követett <1>közösségek</1>",
|
||||
"number_of_communities": "{{count}} közösség",
|
||||
"number_of_communities_plural": "{{count}} közösség",
|
||||
"formatting_help": "formázási segítség",
|
||||
"archive_link": "hivatkozás archiválása",
|
||||
"site_config": "Oldalbeállítások",
|
||||
"removed": "eltávolítva egy mod által",
|
||||
"delete_account_confirm": "Figyelmeztetés: ez véglegesen törölni fogja az összes adatodat. A megerősítéshez írd be a jelszavad!",
|
||||
"email_or_username": "Email vagy felhasználónév",
|
||||
"number_of_users": "{{count}} felhasználó",
|
||||
"number_of_users_plural": "{{count}} felhasználó",
|
||||
"number_online": "{{count}} online felhasználó",
|
||||
"number_online_plural": "{{count}} online felhasználó",
|
||||
"subscribers": "Feliratkozók"
|
||||
}
|
||||
|
|
12
ui/translations/zh.json
vendored
12
ui/translations/zh.json
vendored
|
@ -35,13 +35,13 @@
|
|||
"remove_as_admin": "移除管理权限",
|
||||
"appoint_as_admin": "添加管理权限",
|
||||
"remove": "移除",
|
||||
"removed": "已移除",
|
||||
"removed": "已被管理员移除",
|
||||
"locked": "已加锁",
|
||||
"reason": "原因",
|
||||
"mark_as_read": "标记未读",
|
||||
"mark_as_unread": "标记已读",
|
||||
"delete": "删除",
|
||||
"deleted": "已删除",
|
||||
"deleted": "作者已删除",
|
||||
"restore": "恢复",
|
||||
"ban": "禁止",
|
||||
"ban_from_site": "禁止此站点",
|
||||
|
@ -235,5 +235,11 @@
|
|||
"time": "时间",
|
||||
"action": "行动",
|
||||
"block_leaving": "确定要离开吗?",
|
||||
"show_context": "显示上下文"
|
||||
"show_context": "显示上下文",
|
||||
"admin_settings": "管理员设置",
|
||||
"site_config": "网站配置",
|
||||
"banned_users": "被禁止用户",
|
||||
"site_saved": "网站已保存",
|
||||
"emoji_picker": "选择表情",
|
||||
"invalid_username": "用户名无效"
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue