From f167906a74acad009506663290e45b498dc76da5 Mon Sep 17 00:00:00 2001 From: olivia maia Date: Wed, 5 Feb 2020 14:16:01 -0300 Subject: [PATCH 01/77] added Portuguese translation --- ui/src/translations/pt.ts | 237 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 ui/src/translations/pt.ts diff --git a/ui/src/translations/pt.ts b/ui/src/translations/pt.ts new file mode 100644 index 000000000..a455d6c21 --- /dev/null +++ b/ui/src/translations/pt.ts @@ -0,0 +1,237 @@ +export const pt = { + translation: { + post: 'publicação', + remove_post: 'Apagar publicação', + no_posts: 'Sem publicações.', + create_a_post: 'Criar uma publicação', + create_post: 'Criar publicação', + number_of_posts: '{{count}} publicações', + posts: 'Publicações', + related_posts: 'Essas publicações podem estar relacionadas', + cross_posts: 'Esse link também foi publicado em:', + cross_post: 're-publicar', + cross_posted_to: 'Publicado também em: ', + comments: 'Comentários', + number_of_comments: '{{count}} comentários', + remove_comment: 'Apagar comentário', + communities: 'Comunidades', + users: 'Usuários', + create_a_community: 'Criar uma comunidade', + create_community: 'Criar comunidade', + remove_community: 'Apagar comunidade', + subscribed_to_communities: 'Inscrito em <1>comunidades', + trending_communities: '<1>Comunidades em tendência', + list_of_communities: 'Lista de comunidades', + number_of_communities: '{{count}} comunidades', + community_reqs: 'minúsculas, sublinhados e sem espaços.', + create_private_message: 'Criar mensagem privada', + send_secure_message: 'Enviar mensagem segura', + send_message: 'Enviar mensagem', + message: 'Mensagem', + edit: 'editar', + reply: 'responder', + cancel: 'Cancelar', + preview: 'Pré-visualização', + upload_image: 'fazer upload de imagem', + avatar: 'Avatar', + upload_avatar: 'Fazer upload de avatar', + show_avatars: 'Mostrar Avatars', + formatting_help: 'ajuda de formatação', + view_source: 'ver fonte', + unlock: 'desbloquear', + lock: 'bloquear', + sticky: 'fixar', + unsticky: 'desafixar', + link: 'link', + archive_link: 'arquivar link', + mod: 'moderador', + mods: 'moderadores', + moderates: 'Modera', + settings: 'Configurações', + remove_as_mod: 'remover como moderador', + appoint_as_mod: 'designar como moderador', + modlog: 'Registro de moderação', + admin: 'administrador', + admins: 'administradores', + remove_as_admin: 'remover como administrador', + appoint_as_admin: 'designar como administrador', + remove: 'remover', + removed: 'removido', + locked: 'trancado', + stickied: 'fixado', + reason: 'Motivo', + mark_as_read: 'marcar como lido', + mark_as_unread: 'marcar como não lido', + delete: 'apagar', + deleted: 'apagado', + delete_account: 'Apagar conta', + delete_account_confirm: + 'Aviso: isso vai apagar seus dados de forma permanente. Escreva sua senha para confirmar.', + restore: 'restaurar', + ban: 'banir', + ban_from_site: 'banido do site', + unban: 'readmitido', + unban_from_site: 'readmitido ao site', + banned: 'banido', + save: 'guardar', + unsave: 'descartar', + create: 'criar', + creator: 'criador', + username: 'nome de usuário', + email_or_username: 'E-mail ou nome de usuário', + number_of_users: '{{count}} usuários', + number_of_subscribers: '{{count}} inscritos', + number_of_points: '{{count}} pontos', + number_online: '{{count}} usuários online', + name: 'Nome', + title: 'Título', + category: 'Categoria', + subscribers: 'Inscritos', + both: 'Ambos', + saved: 'Guardado', + unsubscribe: 'Cancelar inscrição', + subscribe: 'Inscrever-se', + subscribed: 'Inscrito', + prev: 'Anterior', + next: 'Próximo', + sidebar: 'Barra lateral', + sort_type: 'Ordenação', + hot: 'Popular', + new: 'Novo', + old: 'Velho', + top_day: 'Top do dia', + week: 'Semana', + month: 'Mês', + year: 'Ano', + all: 'Tudo', + top: 'Top', + api: 'API', + docs: 'Docs', + inbox: 'Caixa de entrada', + inbox_for: 'Caixa de entrada de <1>{{user}}', + mark_all_as_read: 'marcar tudo como lido', + type: 'Tipo', + unread: 'Não lido', + replies: 'Respostas', + mentions: 'Menções', + reply_sent: 'Resposta enviada', + message_sent: 'Mensagem enviada', + search: 'Busca', + overview: 'Visão geral', + view: 'Visualização', + logout: 'Sair', + login_sign_up: 'Entrar / Inscrever-se', + login: 'Entrar', + sign_up: 'Inscrever-se', + notifications_error: + 'Seu navegador não oferece notificações para a área de trabalho. Tente o Firefox ou o Chrome.', + unread_messages: 'Mensagens não lidas', + messages: 'Mensagens', + password: 'Senha', + verify_password: 'Verifique a senha', + old_password: 'Senha antiga', + forgot_password: 'esqueci a senha', + reset_password_mail_sent: 'Enviado um e-mail para a alteração da senha.', + password_change: 'Alteração de senha', + new_password: 'Nova senha', + no_email_setup: "Esse servidor não configurou corretamente o e-mail.", + email: 'E-mail', + matrix_user_id: 'Usuário Matrix', + private_message_disclaimer: + 'Aviso: mensagens privadas no Lemmy não são seguras. Crie uma conta em <1>Riot.im para troca segura de mensagens.', + send_notifications_to_email: 'Enviar notificações para o e-mail', + optional: 'Opcional', + expires: 'Expira', + language: 'Idioma', + browser_default: 'Padrão do navegador', + downvotes_disabled: 'Votos negativos desativados', + enable_downvotes: 'Permitir votos negativos', + open_registration: 'Permitir registro', + registration_closed: 'Registros desativados', + enable_nsfw: 'Permitir NSFW', + url: 'URL', + body: 'Conteúdo', + copy_suggested_title: 'copiar título sugerido: {{title}}', + community: 'Comunidade', + expand_here: 'Expandir aqui', + subscribe_to_communities: 'Inscreva-se em algumas <1>comunidades.', + chat: 'Chat', + recent_comments: 'Últimos comentários', + no_results: 'Nenhum resultado.', + setup: 'Instalação', + lemmy_instance_setup: 'Criação de instância Lemmy', + setup_admin: 'Configurar administrador do site', + your_site: 'seu site', + modified: 'modificado', + nsfw: 'NSFW', + show_nsfw: 'Mostrar conteúdo NSFW', + theme: 'Tema', + sponsors: 'Patrocinadores', + sponsors_of_lemmy: 'Patrocinadores do Lemmy', + sponsor_message: + 'Lemmy é um programa livre e de código aberto, o que significa que não haverá publicidade, monetização ou capital de risco, jamais. Suas doações apoiam de forma direta o desenvolvimento em tempo integral do projeto. Muitos agradecimentos às sequintes pessoas:', + support_on_patreon: 'Colabore no Patreon', + donate_to_lemmy: 'Faça uma doação ao Lemmy', + donate: 'Doar', + general_sponsors: + 'Patrocinadores são aqueles que doaram entre $10 e $39 ao Lemmy.', + crypto: 'Crypto', + bitcoin: 'Bitcoin', + ethereum: 'Ethereum', + monero: 'Monero', + code: 'Code', + joined: 'Entrou', + by: 'por', + to: 'para', + from: 'de', + transfer_community: 'transferir comunidade', + transfer_site: 'transferir site', + are_you_sure: 'tem certeza?', + yes: 'sim', + no: 'não', + powered_by: 'Powered by', + landing_0: + "Lemmy é um <1>agregador de links / alternativa ao reddit, com a intenção de funcionar junto ao <2>fediverso.<3>Pode ser hospedado em servidor próprio, tem atualização de comentários em tempo real e é minúsculo (<4>~80kB). A federação com a rede ActivityPub está no roteiro do projeto. <5>Esta é uma <6>versão beta bastante antecipada, e muitas funcionalidades ainda estão quebradas ou ausentes. <7>Sugira novas funcionalidades ou reporte erros <8>aqui.<9>Feito com <10>Rust, <11>Actix, <12>Inferno, <13>Typescript.", + not_logged_in: 'Não autenticado.', + logged_in: 'Autenticado.', + community_ban: 'Você foi banido desta comunidade.', + site_ban: 'Você foi banido do site', + couldnt_create_comment: "Não foi possível criar o comentário.", + couldnt_like_comment: "Não foi possível curtir o comentário.", + couldnt_update_comment: "Não foi possível atualizar o comentário.", + couldnt_save_comment: "Não foi possível guardar o comentário.", + no_comment_edit_allowed: 'Sem permissão para editar de comentário.', + no_post_edit_allowed: 'Sem permissão para editar publicação.', + no_community_edit_allowed: 'Sem permissão para editar comunidade.', + couldnt_find_community: "Não foi possível encontrar a comunidade.", + couldnt_update_community: "Não foi possível atualizar a comunidade.", + community_already_exists: 'Esta comunidade já existe.', + community_moderator_already_exists: 'Este moderador da comunidade já existe.', + community_follower_already_exists: 'Este seguidor da comunidade já existe.', + community_user_already_banned: 'Este usuário da comunidade já foi banido.', + couldnt_create_post: "Não foi possível criar a publicação.", + couldnt_like_post: "Não foi possível curtir a publicação.", + couldnt_find_post: "Não foi possível encontrar a publicação.", + couldnt_get_posts: "Não foi possível obter as publicações", + couldnt_update_post: "Não foi possível atualizar a publicação", + couldnt_save_post: "Não foi possível guardar a publicação.", + no_slurs: 'Sem insultos.', + not_an_admin: 'Não é administrador.', + site_already_exists: 'O site já existe.', + couldnt_update_site: "Não foi possível atualizar o site.", + couldnt_find_that_username_or_email: + "Não foi possível encontrar esse usuário ou e-mail.", + password_incorrect: 'Senha incorreta.', + passwords_dont_match: 'As senhas não são iguais.', + admin_already_created: "Desculpe, já há um administrador.", + user_already_exists: 'Este usuário já existe.', + email_already_exists: 'Este e-mail já existe.', + couldnt_update_user: "Não foi possível atualizar o usuário.", + system_err_login: 'Erro no sistema. Tente sair e autenticar-se outra vez.', + couldnt_create_private_message: "Não foi possível criar mensagem privada.", + no_private_message_edit_allowed: 'Sem permissão para editar mensagem privada.', + couldnt_update_private_message: "Não foi possível atualizar a mensagem privada.", + time: 'Tempo', + action: 'Ação', + }, +}; \ No newline at end of file From b0399da27b798b720c258b935c28c7595f2c9c7d Mon Sep 17 00:00:00 2001 From: Dessalines Date: Wed, 5 Feb 2020 12:56:01 -0500 Subject: [PATCH 02/77] Fix i18n issue with no communities. --- ui/src/components/post-listings.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ui/src/components/post-listings.tsx b/ui/src/components/post-listings.tsx index 65db3727c..005c4fe03 100644 --- a/ui/src/components/post-listings.tsx +++ b/ui/src/components/post-listings.tsx @@ -3,6 +3,7 @@ import { Link } from 'inferno-router'; import { Post } from '../interfaces'; import { PostListing } from './post-listing'; import { i18n } from '../i18next'; +import { T } from 'inferno-i18next'; interface PostListingsProps { posts: Array; @@ -36,11 +37,9 @@ export class PostListings extends Component { <>
{i18n.t('no_posts')}
{this.props.showCommunity !== undefined && ( -
- - {i18n.t('subscribe_to_communities')} - -
+ + ## + )} )} From 6ec79d2696229c3dd7da6cbb7ba275b136c97d6a Mon Sep 17 00:00:00 2001 From: Dessalines Date: Wed, 5 Feb 2020 13:12:13 -0500 Subject: [PATCH 03/77] Adding language and etc for pt-br. --- README.md | 3 +- ui/src/i18next.ts | 2 + ui/src/translations/pt.ts | 237 ---------------------------------- ui/src/translations/pt_br.ts | 240 +++++++++++++++++++++++++++++++++++ ui/src/utils.ts | 4 + ui/translation_report.ts | 2 + 6 files changed, 250 insertions(+), 238 deletions(-) delete mode 100644 ui/src/translations/pt.ts create mode 100644 ui/src/translations/pt_br.ts diff --git a/README.md b/README.md index 226f3c9f1..cef3681d6 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ Lemmy is free, open-source software, meaning no advertising, monetizing, or vent If you'd like to add translations, take a look at the [English translation file](ui/src/translations/en.ts). -- Languages supported: 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`). +- 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`). @@ -176,6 +176,7 @@ fi | 98% | cross_posted_to,old,time,action fr | 82% | 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,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action it | 83% | 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,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action nl | 99% | cross_posted_to,time,action +pt-br | 100% | ru | 71% | 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,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action sv | 82% | 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,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,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action diff --git a/ui/src/i18next.ts b/ui/src/i18next.ts index 0d3ab177a..4311b09f9 100644 --- a/ui/src/i18next.ts +++ b/ui/src/i18next.ts @@ -13,6 +13,7 @@ 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'; // https://github.com/nimbusec-oss/inferno-i18next/blob/master/tests/T.test.js#L66 const resources = { @@ -29,6 +30,7 @@ const resources = { fi, ca, fa, + pt_BR, }; function format(value: any, format: any, lng: any): any { diff --git a/ui/src/translations/pt.ts b/ui/src/translations/pt.ts deleted file mode 100644 index a455d6c21..000000000 --- a/ui/src/translations/pt.ts +++ /dev/null @@ -1,237 +0,0 @@ -export const pt = { - translation: { - post: 'publicação', - remove_post: 'Apagar publicação', - no_posts: 'Sem publicações.', - create_a_post: 'Criar uma publicação', - create_post: 'Criar publicação', - number_of_posts: '{{count}} publicações', - posts: 'Publicações', - related_posts: 'Essas publicações podem estar relacionadas', - cross_posts: 'Esse link também foi publicado em:', - cross_post: 're-publicar', - cross_posted_to: 'Publicado também em: ', - comments: 'Comentários', - number_of_comments: '{{count}} comentários', - remove_comment: 'Apagar comentário', - communities: 'Comunidades', - users: 'Usuários', - create_a_community: 'Criar uma comunidade', - create_community: 'Criar comunidade', - remove_community: 'Apagar comunidade', - subscribed_to_communities: 'Inscrito em <1>comunidades', - trending_communities: '<1>Comunidades em tendência', - list_of_communities: 'Lista de comunidades', - number_of_communities: '{{count}} comunidades', - community_reqs: 'minúsculas, sublinhados e sem espaços.', - create_private_message: 'Criar mensagem privada', - send_secure_message: 'Enviar mensagem segura', - send_message: 'Enviar mensagem', - message: 'Mensagem', - edit: 'editar', - reply: 'responder', - cancel: 'Cancelar', - preview: 'Pré-visualização', - upload_image: 'fazer upload de imagem', - avatar: 'Avatar', - upload_avatar: 'Fazer upload de avatar', - show_avatars: 'Mostrar Avatars', - formatting_help: 'ajuda de formatação', - view_source: 'ver fonte', - unlock: 'desbloquear', - lock: 'bloquear', - sticky: 'fixar', - unsticky: 'desafixar', - link: 'link', - archive_link: 'arquivar link', - mod: 'moderador', - mods: 'moderadores', - moderates: 'Modera', - settings: 'Configurações', - remove_as_mod: 'remover como moderador', - appoint_as_mod: 'designar como moderador', - modlog: 'Registro de moderação', - admin: 'administrador', - admins: 'administradores', - remove_as_admin: 'remover como administrador', - appoint_as_admin: 'designar como administrador', - remove: 'remover', - removed: 'removido', - locked: 'trancado', - stickied: 'fixado', - reason: 'Motivo', - mark_as_read: 'marcar como lido', - mark_as_unread: 'marcar como não lido', - delete: 'apagar', - deleted: 'apagado', - delete_account: 'Apagar conta', - delete_account_confirm: - 'Aviso: isso vai apagar seus dados de forma permanente. Escreva sua senha para confirmar.', - restore: 'restaurar', - ban: 'banir', - ban_from_site: 'banido do site', - unban: 'readmitido', - unban_from_site: 'readmitido ao site', - banned: 'banido', - save: 'guardar', - unsave: 'descartar', - create: 'criar', - creator: 'criador', - username: 'nome de usuário', - email_or_username: 'E-mail ou nome de usuário', - number_of_users: '{{count}} usuários', - number_of_subscribers: '{{count}} inscritos', - number_of_points: '{{count}} pontos', - number_online: '{{count}} usuários online', - name: 'Nome', - title: 'Título', - category: 'Categoria', - subscribers: 'Inscritos', - both: 'Ambos', - saved: 'Guardado', - unsubscribe: 'Cancelar inscrição', - subscribe: 'Inscrever-se', - subscribed: 'Inscrito', - prev: 'Anterior', - next: 'Próximo', - sidebar: 'Barra lateral', - sort_type: 'Ordenação', - hot: 'Popular', - new: 'Novo', - old: 'Velho', - top_day: 'Top do dia', - week: 'Semana', - month: 'Mês', - year: 'Ano', - all: 'Tudo', - top: 'Top', - api: 'API', - docs: 'Docs', - inbox: 'Caixa de entrada', - inbox_for: 'Caixa de entrada de <1>{{user}}', - mark_all_as_read: 'marcar tudo como lido', - type: 'Tipo', - unread: 'Não lido', - replies: 'Respostas', - mentions: 'Menções', - reply_sent: 'Resposta enviada', - message_sent: 'Mensagem enviada', - search: 'Busca', - overview: 'Visão geral', - view: 'Visualização', - logout: 'Sair', - login_sign_up: 'Entrar / Inscrever-se', - login: 'Entrar', - sign_up: 'Inscrever-se', - notifications_error: - 'Seu navegador não oferece notificações para a área de trabalho. Tente o Firefox ou o Chrome.', - unread_messages: 'Mensagens não lidas', - messages: 'Mensagens', - password: 'Senha', - verify_password: 'Verifique a senha', - old_password: 'Senha antiga', - forgot_password: 'esqueci a senha', - reset_password_mail_sent: 'Enviado um e-mail para a alteração da senha.', - password_change: 'Alteração de senha', - new_password: 'Nova senha', - no_email_setup: "Esse servidor não configurou corretamente o e-mail.", - email: 'E-mail', - matrix_user_id: 'Usuário Matrix', - private_message_disclaimer: - 'Aviso: mensagens privadas no Lemmy não são seguras. Crie uma conta em <1>Riot.im para troca segura de mensagens.', - send_notifications_to_email: 'Enviar notificações para o e-mail', - optional: 'Opcional', - expires: 'Expira', - language: 'Idioma', - browser_default: 'Padrão do navegador', - downvotes_disabled: 'Votos negativos desativados', - enable_downvotes: 'Permitir votos negativos', - open_registration: 'Permitir registro', - registration_closed: 'Registros desativados', - enable_nsfw: 'Permitir NSFW', - url: 'URL', - body: 'Conteúdo', - copy_suggested_title: 'copiar título sugerido: {{title}}', - community: 'Comunidade', - expand_here: 'Expandir aqui', - subscribe_to_communities: 'Inscreva-se em algumas <1>comunidades.', - chat: 'Chat', - recent_comments: 'Últimos comentários', - no_results: 'Nenhum resultado.', - setup: 'Instalação', - lemmy_instance_setup: 'Criação de instância Lemmy', - setup_admin: 'Configurar administrador do site', - your_site: 'seu site', - modified: 'modificado', - nsfw: 'NSFW', - show_nsfw: 'Mostrar conteúdo NSFW', - theme: 'Tema', - sponsors: 'Patrocinadores', - sponsors_of_lemmy: 'Patrocinadores do Lemmy', - sponsor_message: - 'Lemmy é um programa livre e de código aberto, o que significa que não haverá publicidade, monetização ou capital de risco, jamais. Suas doações apoiam de forma direta o desenvolvimento em tempo integral do projeto. Muitos agradecimentos às sequintes pessoas:', - support_on_patreon: 'Colabore no Patreon', - donate_to_lemmy: 'Faça uma doação ao Lemmy', - donate: 'Doar', - general_sponsors: - 'Patrocinadores são aqueles que doaram entre $10 e $39 ao Lemmy.', - crypto: 'Crypto', - bitcoin: 'Bitcoin', - ethereum: 'Ethereum', - monero: 'Monero', - code: 'Code', - joined: 'Entrou', - by: 'por', - to: 'para', - from: 'de', - transfer_community: 'transferir comunidade', - transfer_site: 'transferir site', - are_you_sure: 'tem certeza?', - yes: 'sim', - no: 'não', - powered_by: 'Powered by', - landing_0: - "Lemmy é um <1>agregador de links / alternativa ao reddit, com a intenção de funcionar junto ao <2>fediverso.<3>Pode ser hospedado em servidor próprio, tem atualização de comentários em tempo real e é minúsculo (<4>~80kB). A federação com a rede ActivityPub está no roteiro do projeto. <5>Esta é uma <6>versão beta bastante antecipada, e muitas funcionalidades ainda estão quebradas ou ausentes. <7>Sugira novas funcionalidades ou reporte erros <8>aqui.<9>Feito com <10>Rust, <11>Actix, <12>Inferno, <13>Typescript.", - not_logged_in: 'Não autenticado.', - logged_in: 'Autenticado.', - community_ban: 'Você foi banido desta comunidade.', - site_ban: 'Você foi banido do site', - couldnt_create_comment: "Não foi possível criar o comentário.", - couldnt_like_comment: "Não foi possível curtir o comentário.", - couldnt_update_comment: "Não foi possível atualizar o comentário.", - couldnt_save_comment: "Não foi possível guardar o comentário.", - no_comment_edit_allowed: 'Sem permissão para editar de comentário.', - no_post_edit_allowed: 'Sem permissão para editar publicação.', - no_community_edit_allowed: 'Sem permissão para editar comunidade.', - couldnt_find_community: "Não foi possível encontrar a comunidade.", - couldnt_update_community: "Não foi possível atualizar a comunidade.", - community_already_exists: 'Esta comunidade já existe.', - community_moderator_already_exists: 'Este moderador da comunidade já existe.', - community_follower_already_exists: 'Este seguidor da comunidade já existe.', - community_user_already_banned: 'Este usuário da comunidade já foi banido.', - couldnt_create_post: "Não foi possível criar a publicação.", - couldnt_like_post: "Não foi possível curtir a publicação.", - couldnt_find_post: "Não foi possível encontrar a publicação.", - couldnt_get_posts: "Não foi possível obter as publicações", - couldnt_update_post: "Não foi possível atualizar a publicação", - couldnt_save_post: "Não foi possível guardar a publicação.", - no_slurs: 'Sem insultos.', - not_an_admin: 'Não é administrador.', - site_already_exists: 'O site já existe.', - couldnt_update_site: "Não foi possível atualizar o site.", - couldnt_find_that_username_or_email: - "Não foi possível encontrar esse usuário ou e-mail.", - password_incorrect: 'Senha incorreta.', - passwords_dont_match: 'As senhas não são iguais.', - admin_already_created: "Desculpe, já há um administrador.", - user_already_exists: 'Este usuário já existe.', - email_already_exists: 'Este e-mail já existe.', - couldnt_update_user: "Não foi possível atualizar o usuário.", - system_err_login: 'Erro no sistema. Tente sair e autenticar-se outra vez.', - couldnt_create_private_message: "Não foi possível criar mensagem privada.", - no_private_message_edit_allowed: 'Sem permissão para editar mensagem privada.', - couldnt_update_private_message: "Não foi possível atualizar a mensagem privada.", - time: 'Tempo', - action: 'Ação', - }, -}; \ No newline at end of file diff --git a/ui/src/translations/pt_br.ts b/ui/src/translations/pt_br.ts new file mode 100644 index 000000000..3f1c21fdd --- /dev/null +++ b/ui/src/translations/pt_br.ts @@ -0,0 +1,240 @@ +export const pt_BR = { + translation: { + post: 'publicação', + remove_post: 'Apagar publicação', + no_posts: 'Sem publicações.', + create_a_post: 'Criar uma publicação', + create_post: 'Criar publicação', + number_of_posts: '{{count}} publicações', + posts: 'Publicações', + related_posts: 'Essas publicações podem estar relacionadas', + cross_posts: 'Esse link também foi publicado em:', + cross_post: 're-publicar', + cross_posted_to: 'Publicado também em: ', + comments: 'Comentários', + number_of_comments: '{{count}} comentários', + remove_comment: 'Apagar comentário', + communities: 'Comunidades', + users: 'Usuários', + create_a_community: 'Criar uma comunidade', + create_community: 'Criar comunidade', + remove_community: 'Apagar comunidade', + subscribed_to_communities: 'Inscrito em <1>comunidades', + trending_communities: '<1>Comunidades em tendência', + list_of_communities: 'Lista de comunidades', + number_of_communities: '{{count}} comunidades', + community_reqs: 'minúsculas, sublinhados e sem espaços.', + create_private_message: 'Criar mensagem privada', + send_secure_message: 'Enviar mensagem segura', + send_message: 'Enviar mensagem', + message: 'Mensagem', + edit: 'editar', + reply: 'responder', + cancel: 'Cancelar', + preview: 'Pré-visualização', + upload_image: 'fazer upload de imagem', + avatar: 'Avatar', + upload_avatar: 'Fazer upload de avatar', + show_avatars: 'Mostrar Avatars', + formatting_help: 'ajuda de formatação', + view_source: 'ver fonte', + unlock: 'desbloquear', + lock: 'bloquear', + sticky: 'fixar', + unsticky: 'desafixar', + link: 'link', + archive_link: 'arquivar link', + mod: 'moderador', + mods: 'moderadores', + moderates: 'Modera', + settings: 'Configurações', + remove_as_mod: 'remover como moderador', + appoint_as_mod: 'designar como moderador', + modlog: 'Registro de moderação', + admin: 'administrador', + admins: 'administradores', + remove_as_admin: 'remover como administrador', + appoint_as_admin: 'designar como administrador', + remove: 'remover', + removed: 'removido', + locked: 'trancado', + stickied: 'fixado', + reason: 'Motivo', + mark_as_read: 'marcar como lido', + mark_as_unread: 'marcar como não lido', + delete: 'apagar', + deleted: 'apagado', + delete_account: 'Apagar conta', + delete_account_confirm: + 'Aviso: isso vai apagar seus dados de forma permanente. Escreva sua senha para confirmar.', + restore: 'restaurar', + ban: 'banir', + ban_from_site: 'banido do site', + unban: 'readmitido', + unban_from_site: 'readmitido ao site', + banned: 'banido', + save: 'guardar', + unsave: 'descartar', + create: 'criar', + creator: 'criador', + username: 'nome de usuário', + email_or_username: 'E-mail ou nome de usuário', + number_of_users: '{{count}} usuários', + number_of_subscribers: '{{count}} inscritos', + number_of_points: '{{count}} pontos', + number_online: '{{count}} usuários online', + name: 'Nome', + title: 'Título', + category: 'Categoria', + subscribers: 'Inscritos', + both: 'Ambos', + saved: 'Guardado', + unsubscribe: 'Cancelar inscrição', + subscribe: 'Inscrever-se', + subscribed: 'Inscrito', + prev: 'Anterior', + next: 'Próximo', + sidebar: 'Barra lateral', + sort_type: 'Ordenação', + hot: 'Popular', + new: 'Novo', + old: 'Velho', + top_day: 'Top do dia', + week: 'Semana', + month: 'Mês', + year: 'Ano', + all: 'Tudo', + top: 'Top', + api: 'API', + docs: 'Docs', + inbox: 'Caixa de entrada', + inbox_for: 'Caixa de entrada de <1>{{user}}', + mark_all_as_read: 'marcar tudo como lido', + type: 'Tipo', + unread: 'Não lido', + replies: 'Respostas', + mentions: 'Menções', + reply_sent: 'Resposta enviada', + message_sent: 'Mensagem enviada', + search: 'Busca', + overview: 'Visão geral', + view: 'Visualização', + logout: 'Sair', + login_sign_up: 'Entrar / Inscrever-se', + login: 'Entrar', + sign_up: 'Inscrever-se', + notifications_error: + 'Seu navegador não oferece notificações para a área de trabalho. Tente o Firefox ou o Chrome.', + unread_messages: 'Mensagens não lidas', + messages: 'Mensagens', + password: 'Senha', + verify_password: 'Verifique a senha', + old_password: 'Senha antiga', + forgot_password: 'esqueci a senha', + reset_password_mail_sent: 'Enviado um e-mail para a alteração da senha.', + password_change: 'Alteração de senha', + new_password: 'Nova senha', + no_email_setup: 'Esse servidor não configurou corretamente o e-mail.', + email: 'E-mail', + matrix_user_id: 'Usuário Matrix', + private_message_disclaimer: + 'Aviso: mensagens privadas no Lemmy não são seguras. Crie uma conta em <1>Riot.im para troca segura de mensagens.', + send_notifications_to_email: 'Enviar notificações para o e-mail', + optional: 'Opcional', + expires: 'Expira', + language: 'Idioma', + browser_default: 'Padrão do navegador', + downvotes_disabled: 'Votos negativos desativados', + enable_downvotes: 'Permitir votos negativos', + open_registration: 'Permitir registro', + registration_closed: 'Registros desativados', + enable_nsfw: 'Permitir NSFW', + url: 'URL', + body: 'Conteúdo', + copy_suggested_title: 'copiar título sugerido: {{title}}', + community: 'Comunidade', + expand_here: 'Expandir aqui', + subscribe_to_communities: 'Inscreva-se em algumas <1>comunidades.', + chat: 'Chat', + recent_comments: 'Últimos comentários', + no_results: 'Nenhum resultado.', + setup: 'Instalação', + lemmy_instance_setup: 'Criação de instância Lemmy', + setup_admin: 'Configurar administrador do site', + your_site: 'seu site', + modified: 'modificado', + nsfw: 'NSFW', + show_nsfw: 'Mostrar conteúdo NSFW', + theme: 'Tema', + sponsors: 'Patrocinadores', + sponsors_of_lemmy: 'Patrocinadores do Lemmy', + sponsor_message: + 'Lemmy é um programa livre e de código aberto, o que significa que não haverá publicidade, monetização ou capital de risco, jamais. Suas doações apoiam de forma direta o desenvolvimento em tempo integral do projeto. Muitos agradecimentos às sequintes pessoas:', + support_on_patreon: 'Colabore no Patreon', + donate_to_lemmy: 'Faça uma doação ao Lemmy', + donate: 'Doar', + general_sponsors: + 'Patrocinadores são aqueles que doaram entre $10 e $39 ao Lemmy.', + crypto: 'Crypto', + bitcoin: 'Bitcoin', + ethereum: 'Ethereum', + monero: 'Monero', + code: 'Code', + joined: 'Entrou', + by: 'por', + to: 'para', + from: 'de', + transfer_community: 'transferir comunidade', + transfer_site: 'transferir site', + are_you_sure: 'tem certeza?', + yes: 'sim', + no: 'não', + powered_by: 'Powered by', + landing_0: + 'Lemmy é um <1>agregador de links / alternativa ao reddit, com a intenção de funcionar junto ao <2>fediverso.<3>Pode ser hospedado em servidor próprio, tem atualização de comentários em tempo real e é minúsculo (<4>~80kB). A federação com a rede ActivityPub está no roteiro do projeto. <5>Esta é uma <6>versão beta bastante antecipada, e muitas funcionalidades ainda estão quebradas ou ausentes. <7>Sugira novas funcionalidades ou reporte erros <8>aqui.<9>Feito com <10>Rust, <11>Actix, <12>Inferno, <13>Typescript.', + not_logged_in: 'Não autenticado.', + logged_in: 'Autenticado.', + community_ban: 'Você foi banido desta comunidade.', + site_ban: 'Você foi banido do site', + couldnt_create_comment: 'Não foi possível criar o comentário.', + couldnt_like_comment: 'Não foi possível curtir o comentário.', + couldnt_update_comment: 'Não foi possível atualizar o comentário.', + couldnt_save_comment: 'Não foi possível guardar o comentário.', + no_comment_edit_allowed: 'Sem permissão para editar de comentário.', + no_post_edit_allowed: 'Sem permissão para editar publicação.', + no_community_edit_allowed: 'Sem permissão para editar comunidade.', + couldnt_find_community: 'Não foi possível encontrar a comunidade.', + couldnt_update_community: 'Não foi possível atualizar a comunidade.', + community_already_exists: 'Esta comunidade já existe.', + community_moderator_already_exists: + 'Este moderador da comunidade já existe.', + community_follower_already_exists: 'Este seguidor da comunidade já existe.', + community_user_already_banned: 'Este usuário da comunidade já foi banido.', + couldnt_create_post: 'Não foi possível criar a publicação.', + couldnt_like_post: 'Não foi possível curtir a publicação.', + couldnt_find_post: 'Não foi possível encontrar a publicação.', + couldnt_get_posts: 'Não foi possível obter as publicações', + couldnt_update_post: 'Não foi possível atualizar a publicação', + couldnt_save_post: 'Não foi possível guardar a publicação.', + no_slurs: 'Sem insultos.', + not_an_admin: 'Não é administrador.', + site_already_exists: 'O site já existe.', + couldnt_update_site: 'Não foi possível atualizar o site.', + couldnt_find_that_username_or_email: + 'Não foi possível encontrar esse usuário ou e-mail.', + password_incorrect: 'Senha incorreta.', + passwords_dont_match: 'As senhas não são iguais.', + admin_already_created: 'Desculpe, já há um administrador.', + user_already_exists: 'Este usuário já existe.', + email_already_exists: 'Este e-mail já existe.', + couldnt_update_user: 'Não foi possível atualizar o usuário.', + system_err_login: 'Erro no sistema. Tente sair e autenticar-se outra vez.', + couldnt_create_private_message: 'Não foi possível criar mensagem privada.', + no_private_message_edit_allowed: + 'Sem permissão para editar mensagem privada.', + couldnt_update_private_message: + 'Não foi possível atualizar a mensagem privada.', + time: 'Tempo', + action: 'Ação', + }, +}; diff --git a/ui/src/utils.ts b/ui/src/utils.ts index 19cb28a20..c53ee76a1 100644 --- a/ui/src/utils.ts +++ b/ui/src/utils.ts @@ -10,6 +10,7 @@ import 'moment/locale/it'; import 'moment/locale/fi'; import 'moment/locale/ca'; import 'moment/locale/fa'; +import 'moment/locale/pt-br'; import { UserOperation, @@ -260,6 +261,7 @@ export const languages = [ { code: 'es', name: 'Español' }, { code: 'de', name: 'Deutsch' }, { code: 'fa', name: 'فارسی' }, + { code: 'pt_BR', name: 'Português Brasileiro' }, { code: 'zh', name: '中文' }, { code: 'fi', name: 'Suomi' }, { code: 'fr', name: 'Français' }, @@ -310,6 +312,8 @@ export function getMomentLanguage(): string { lang = 'ca'; } else if (lang.startsWith('fa')) { lang = 'fa'; + } else if (lang.startsWith('pt')) { + lang = 'pt-br'; } else { lang = 'en'; } diff --git a/ui/translation_report.ts b/ui/translation_report.ts index 1956b7921..c010aee0e 100644 --- a/ui/translation_report.ts +++ b/ui/translation_report.ts @@ -11,6 +11,7 @@ import { nl } from './src/translations/nl'; import { it } from './src/translations/it'; import { fi } from './src/translations/fi'; import { ca } from './src/translations/ca'; +import { pt_BR } from './src/translations/pt_br'; import fs from 'fs'; const files = [ @@ -23,6 +24,7 @@ const files = [ { t: fr, n: 'fr' }, { t: it, n: 'it' }, { t: nl, n: 'nl' }, + { t: pt_BR, n: 'pt-br' }, { t: ru, n: 'ru' }, { t: sv, n: 'sv' }, { t: zh, n: 'zh' }, From 5601ad5283ddc4d7bd174ffb4825fd7ccef7b457 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Wed, 5 Feb 2020 14:14:05 -0500 Subject: [PATCH 04/77] Adding Liberapay. --- README.md | 28 ++++++++++++++-------------- ui/src/components/sponsors.tsx | 8 +++++++- ui/src/translations/en.ts | 1 + 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index cef3681d6..84b3dba98 100644 --- a/README.md +++ b/README.md @@ -147,9 +147,9 @@ ansible-playbook lemmy.yml --become Lemmy is free, open-source software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. +- [Support on Liberapay.](https://liberapay.com/Lemmy) - [Support on Patreon](https://www.patreon.com/dessalines). - [List of Sponsors](https://dev.lemmy.ml/sponsors). -- Soon to add either liberapay or opencollective. ### Crypto @@ -167,19 +167,19 @@ If you'd like to add translations, take a look at the [English translation file] lang | done | missing ---- | ---- | ------- -ca | 98% | cross_posted_to,old,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,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action -fa | 72% | 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,general_sponsors,joined,by,to,from,landing_0,logged_in,community_moderator_already_exists,community_follower_already_exists,community_user_already_banned,no_slurs,admin_already_created,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action -eo | 74% | 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,donate_to_lemmy,donate,from,are_you_sure,yes,no,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action -es | 100% | cross_posted_to -fi | 98% | cross_posted_to,old,time,action -fr | 82% | 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,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action -it | 83% | 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,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action -nl | 99% | cross_posted_to,time,action -pt-br | 100% | -ru | 71% | 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,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action -sv | 82% | 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,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,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action +ca | 98% | cross_posted_to,old,support_on_liberapay,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,support_on_liberapay,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action +fa | 72% | 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,community_moderator_already_exists,community_follower_already_exists,community_user_already_banned,no_slurs,admin_already_created,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action +eo | 74% | 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,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action +es | 99% | cross_posted_to,support_on_liberapay +fi | 98% | cross_posted_to,old,support_on_liberapay,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,support_on_liberapay,donate_to_lemmy,donate,from,logged_in,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,support_on_liberapay,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action +nl | 98% | cross_posted_to,support_on_liberapay,time,action +pt-br | 100% | support_on_liberapay +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,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,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,support_on_liberapay,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action If you'd like to update this report, run: diff --git a/ui/src/components/sponsors.tsx b/ui/src/components/sponsors.tsx index 988ad4e06..347cb718c 100644 --- a/ui/src/components/sponsors.tsx +++ b/ui/src/components/sponsors.tsx @@ -47,7 +47,13 @@ export class Sponsors extends Component { ##

- + + {i18n.t('support_on_liberapay')} + + {i18n.t('support_on_patreon')} diff --git a/ui/src/translations/en.ts b/ui/src/translations/en.ts index e103df7ec..e58e94e50 100644 --- a/ui/src/translations/en.ts +++ b/ui/src/translations/en.ts @@ -171,6 +171,7 @@ export const en = { sponsor_message: 'Lemmy is free, <1>open-source 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: From ee60465643ef7a7a703caf51f47a74001887833c Mon Sep 17 00:00:00 2001 From: Dessalines Date: Wed, 5 Feb 2020 14:24:35 -0500 Subject: [PATCH 05/77] Adding if found to catch errors. --- ui/src/components/community.tsx | 30 +++++++++++------------ ui/src/components/main.tsx | 31 ++++++++++++------------ ui/src/components/post.tsx | 42 +++++++++++++++++++-------------- 3 files changed, 55 insertions(+), 48 deletions(-) diff --git a/ui/src/components/community.tsx b/ui/src/components/community.tsx index 0fa8f7b53..069f9158d 100644 --- a/ui/src/components/community.tsx +++ b/ui/src/components/community.tsx @@ -283,12 +283,12 @@ export class Community extends Component { } else if (res.op == UserOperation.EditPost) { let data = res.data as PostResponse; let found = this.state.posts.find(c => c.id == data.post.id); - - found.url = data.post.url; - found.name = data.post.name; - found.nsfw = data.post.nsfw; - - this.setState(this.state); + if (found) { + found.url = data.post.url; + found.name = data.post.name; + found.nsfw = data.post.nsfw; + this.setState(this.state); + } } else if (res.op == UserOperation.CreatePost) { let data = res.data as PostResponse; this.state.posts.unshift(data.post); @@ -296,16 +296,16 @@ export class Community extends Component { } else if (res.op == UserOperation.CreatePostLike) { let data = res.data as PostResponse; let found = this.state.posts.find(c => c.id == data.post.id); - - found.score = data.post.score; - found.upvotes = data.post.upvotes; - found.downvotes = data.post.downvotes; - if (data.post.my_vote !== null) { - found.my_vote = data.post.my_vote; - found.upvoteLoading = false; - found.downvoteLoading = false; + if (found) { + found.score = data.post.score; + found.upvotes = data.post.upvotes; + found.downvotes = data.post.downvotes; + if (data.post.my_vote !== null) { + found.my_vote = data.post.my_vote; + found.upvoteLoading = false; + found.downvoteLoading = false; + } } - this.setState(this.state); } else if (res.op == UserOperation.AddModToCommunity) { let data = res.data as AddModToCommunityResponse; diff --git a/ui/src/components/main.tsx b/ui/src/components/main.tsx index 0970381b5..161f5df45 100644 --- a/ui/src/components/main.tsx +++ b/ui/src/components/main.tsx @@ -590,26 +590,27 @@ export class Main extends Component { } else if (res.op == UserOperation.EditPost) { let data = res.data as PostResponse; let found = this.state.posts.find(c => c.id == data.post.id); + if (found) { + found.url = data.post.url; + found.name = data.post.name; + found.nsfw = data.post.nsfw; - found.url = data.post.url; - found.name = data.post.name; - found.nsfw = data.post.nsfw; - - this.setState(this.state); + this.setState(this.state); + } } else if (res.op == UserOperation.CreatePostLike) { let data = res.data as PostResponse; let found = this.state.posts.find(c => c.id == data.post.id); - - found.score = data.post.score; - found.upvotes = data.post.upvotes; - found.downvotes = data.post.downvotes; - if (data.post.my_vote !== null) { - found.my_vote = data.post.my_vote; - found.upvoteLoading = false; - found.downvoteLoading = false; + if (found) { + found.score = data.post.score; + found.upvotes = data.post.upvotes; + found.downvotes = data.post.downvotes; + if (data.post.my_vote !== null) { + found.my_vote = data.post.my_vote; + found.upvoteLoading = false; + found.downvoteLoading = false; + } + this.setState(this.state); } - - this.setState(this.state); } else if (res.op == UserOperation.AddAdmin) { let data = res.data as AddAdminResponse; this.state.siteRes.admins = data.admins; diff --git a/ui/src/components/post.tsx b/ui/src/components/post.tsx index b399d3b28..922fc01ea 100644 --- a/ui/src/components/post.tsx +++ b/ui/src/components/post.tsx @@ -409,33 +409,39 @@ export class Post extends Component { } else if (res.op == UserOperation.EditComment) { let data = res.data as CommentResponse; let found = this.state.comments.find(c => c.id == data.comment.id); - found.content = data.comment.content; - found.updated = data.comment.updated; - found.removed = data.comment.removed; - found.deleted = data.comment.deleted; - found.upvotes = data.comment.upvotes; - found.downvotes = data.comment.downvotes; - found.score = data.comment.score; - found.read = data.comment.read; + if (found) { + found.content = data.comment.content; + found.updated = data.comment.updated; + found.removed = data.comment.removed; + found.deleted = data.comment.deleted; + found.upvotes = data.comment.upvotes; + found.downvotes = data.comment.downvotes; + found.score = data.comment.score; + found.read = data.comment.read; - this.setState(this.state); + this.setState(this.state); + } } else if (res.op == UserOperation.SaveComment) { let data = res.data as CommentResponse; let found = this.state.comments.find(c => c.id == data.comment.id); - found.saved = data.comment.saved; - this.setState(this.state); + if (found) { + found.saved = data.comment.saved; + this.setState(this.state); + } } else if (res.op == UserOperation.CreateCommentLike) { let data = res.data as CommentResponse; let found: Comment = this.state.comments.find( c => c.id === data.comment.id ); - found.score = data.comment.score; - found.upvotes = data.comment.upvotes; - found.downvotes = data.comment.downvotes; - if (data.comment.my_vote !== null) { - found.my_vote = data.comment.my_vote; - found.upvoteLoading = false; - found.downvoteLoading = false; + if (found) { + found.score = data.comment.score; + found.upvotes = data.comment.upvotes; + found.downvotes = data.comment.downvotes; + if (data.comment.my_vote !== null) { + found.my_vote = data.comment.my_vote; + found.upvoteLoading = false; + found.downvoteLoading = false; + } } this.setState(this.state); } else if (res.op == UserOperation.CreatePostLike) { From 8b048976326ef6e0fd83a82f8c41523a955b04fc Mon Sep 17 00:00:00 2001 From: Richie Zhang <12566991+StaticallyTypedRice@users.noreply.github.com> Date: Wed, 5 Feb 2020 17:23:07 -0800 Subject: [PATCH 06/77] Add liberapay to FUNDING.yml --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index e2aa05474..be4c3f629 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,4 @@ # These are supported funding model platforms patreon: dessalines +liberapay: Lemmy From 53a662e3b293e86af3340c77093bf3e4a41d64f7 Mon Sep 17 00:00:00 2001 From: Richie Zhang <12566991+StaticallyTypedRice@users.noreply.github.com> Date: Wed, 5 Feb 2020 17:35:09 -0800 Subject: [PATCH 07/77] Translate support_on_liberapay to German --- ui/src/translations/de.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/translations/de.ts b/ui/src/translations/de.ts index 7a1b0f5d1..d8a4a8500 100644 --- a/ui/src/translations/de.ts +++ b/ui/src/translations/de.ts @@ -150,6 +150,7 @@ export const de = { sponsor_message: 'Lemmy ist freie <1>Open-Source 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', From fe1db54a93763e64e8ab08cb17a122635356f26e Mon Sep 17 00:00:00 2001 From: Richie Zhang <12566991+StaticallyTypedRice@users.noreply.github.com> Date: Wed, 5 Feb 2020 17:36:22 -0800 Subject: [PATCH 08/77] Translate support_on_liberapay to Chinese --- ui/src/translations/zh.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/translations/zh.ts b/ui/src/translations/zh.ts index 52fdfabfd..e4111996d 100644 --- a/ui/src/translations/zh.ts +++ b/ui/src/translations/zh.ts @@ -116,6 +116,7 @@ export const zh = { sponsor_message: 'Lemmy is free, <1>open-source 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: '在 Patreon 赞助', + support_on_liberapay: '在 on 赞助', general_sponsors: 'General Sponsors are those that pledged $10 to $39 to Lemmy.', crypto: '加密', From 43c187cf08f589fd242eb1825e05bbbf7bf5f946 Mon Sep 17 00:00:00 2001 From: Richie Zhang <12566991+StaticallyTypedRice@users.noreply.github.com> Date: Wed, 5 Feb 2020 17:38:04 -0800 Subject: [PATCH 09/77] Translate support_on_liberapay to Spanish. --- ui/src/translations/es.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/translations/es.ts b/ui/src/translations/es.ts index 53d7636d8..1b1b5d67c 100644 --- a/ui/src/translations/es.ts +++ b/ui/src/translations/es.ts @@ -169,6 +169,7 @@ export const es = { sponsor_message: 'Lemmy es software libre y de <1>código abierto, 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: From 45241cc5dfef814816da5bafbd67ef8fd9d9d759 Mon Sep 17 00:00:00 2001 From: Richie Zhang <12566991+StaticallyTypedRice@users.noreply.github.com> Date: Wed, 5 Feb 2020 17:40:43 -0800 Subject: [PATCH 10/77] Translate support_on_liberapay to French --- ui/src/translations/fr.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/translations/fr.ts b/ui/src/translations/fr.ts index 771f44e8d..663b7dcb9 100644 --- a/ui/src/translations/fr.ts +++ b/ui/src/translations/fr.ts @@ -139,6 +139,7 @@ export const fr = { sponsor_message: "Lemmy est gratuit et <1>open-source, 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', From 1f96b73e517f34b1b5c5450d0ecceb2e54aff364 Mon Sep 17 00:00:00 2001 From: Richie Zhang <12566991+StaticallyTypedRice@users.noreply.github.com> Date: Wed, 5 Feb 2020 17:41:43 -0800 Subject: [PATCH 11/77] Translate support_on_liberapay to Italian --- ui/src/translations/it.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/translations/it.ts b/ui/src/translations/it.ts index db22eeb45..53d1894df 100644 --- a/ui/src/translations/it.ts +++ b/ui/src/translations/it.ts @@ -138,6 +138,7 @@ export const it = { sponsors_of_lemmy: 'Sponsors di Lemmy', sponsor_message: 'Lemmy è un software gratuito e <1>open-source, 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', From 07fdb17557c2b5efe6d12916e84052d8615a0474 Mon Sep 17 00:00:00 2001 From: Richie Zhang <12566991+StaticallyTypedRice@users.noreply.github.com> Date: Wed, 5 Feb 2020 17:42:38 -0800 Subject: [PATCH 12/77] Translate support_on_liberapay to Dutch. --- ui/src/translations/nl.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/translations/nl.ts b/ui/src/translations/nl.ts index 41aa24385..4bf1fae5f 100644 --- a/ui/src/translations/nl.ts +++ b/ui/src/translations/nl.ts @@ -125,6 +125,7 @@ export const nl = { sponsor_message: 'Lemmy is vrije, <1>open-source 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', From 8a3f5032c300583d0671e9e0f266532e7f0f4f83 Mon Sep 17 00:00:00 2001 From: Richie Zhang <12566991+StaticallyTypedRice@users.noreply.github.com> Date: Wed, 5 Feb 2020 17:44:52 -0800 Subject: [PATCH 13/77] Translate support_on_liberapay to Brazilian Portuguese. --- ui/src/translations/pt_br.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/translations/pt_br.ts b/ui/src/translations/pt_br.ts index 3f1c21fdd..aed781e87 100644 --- a/ui/src/translations/pt_br.ts +++ b/ui/src/translations/pt_br.ts @@ -171,6 +171,7 @@ export const pt_BR = { sponsor_message: 'Lemmy é um programa livre e de código aberto, o que significa que não haverá publicidade, monetização ou capital de risco, jamais. Suas doações apoiam de forma direta o desenvolvimento em tempo integral do projeto. Muitos agradecimentos às sequintes pessoas:', support_on_patreon: 'Colabore no Patreon', + support_on_liberapay: 'Colabore no Liberapay', donate_to_lemmy: 'Faça uma doação ao Lemmy', donate: 'Doar', general_sponsors: From a8ef9f87262110ab7119d799aa2d370544e63a7a Mon Sep 17 00:00:00 2001 From: Richie Zhang <12566991+StaticallyTypedRice@users.noreply.github.com> Date: Wed, 5 Feb 2020 18:15:28 -0800 Subject: [PATCH 14/77] Create db-init.sh --- server/db-init.sh | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 server/db-init.sh diff --git a/server/db-init.sh b/server/db-init.sh new file mode 100644 index 000000000..77b9a6d71 --- /dev/null +++ b/server/db-init.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +username=lemmy +dbname=lemmy +port=5432 + +read -p "Enter database password: " -s password +echo + +psql -c "CREATE USER $username WITH PASSWORD '$password' SUPERUSER;" -U postgres +psql -c 'CREATE DATABASE $dbname WITH OWNER $username;' -U postgres +export LEMMY_DATABASE_URL=postgres://$username:$password@localhost:$port/$dbname From 5614ed7a9321b20d7ca11093fa4c0b2e036a508c Mon Sep 17 00:00:00 2001 From: Dessalines Date: Thu, 6 Feb 2020 11:26:11 -0500 Subject: [PATCH 15/77] Reworking README.md. Fixes #513. --- README.md | 133 +++++++----------- docs/src/about.md | 16 ++- docs/src/about_features.md | 7 + docs/src/administration.md | 4 +- docs/src/administration_configuration.md | 2 + docs/src/administration_install_ansible.md | 2 + docs/src/administration_install_docker.md | 8 +- docs/src/administration_install_kubernetes.md | 2 + docs/src/contributing_docker_development.md | 6 +- docs/src/contributing_websocket_http_api.md | 1 + 10 files changed, 91 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index 84b3dba98..1c5709745 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,35 @@ -

- - -

- -

Lemmy

-
-![GitHub stars](https://img.shields.io/github/stars/dessalines/lemmy?style=social) -[![Mastodon Follow](https://img.shields.io/mastodon/follow/810572?domain=https%3A%2F%2Fmastodon.social&style=social)](https://mastodon.social/@LemmyDev) -[![Matrix](https://img.shields.io/matrix/rust-reddit-fediverse:matrix.org.svg?label=matrix-chat)](https://riot.im/app/#/room/#rust-reddit-fediverse:matrix.org) ![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/dessalines/lemmy.svg) [![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/) -![GitHub commit activity](https://img.shields.io/github/commit-activity/m/dessalines/lemmy.svg) -![GitHub repo size](https://img.shields.io/github/repo-size/dessalines/lemmy.svg) [![License](https://img.shields.io/github/license/dessalines/lemmy.svg)](LICENSE) -[![Patreon](https://img.shields.io/badge/-Support%20on%20Patreon-blueviolet.svg)](https://www.patreon.com/dessalines) +![GitHub stars](https://img.shields.io/github/stars/dessalines/lemmy?style=social)
---- +

+ + -

A link aggregator / reddit clone for the fediverse. -
+

Lemmy

+

+ A link aggregator / reddit clone for the fediverse. +
+
+ View Site + · + Documentation + · + Report Bug + · + Request Feature + · + Releases +

-[Lemmy Dev instance](https://dev.lemmy.ml) *This data is being backed up, and once federation is working, it will be the basis for a main instance.* - -This is a **very early beta version**, and a lot of features are currently broken or in active development, such as federation. +## About The Project Front Page|Post ---|--- @@ -42,17 +43,22 @@ The overall goal is to create an easily self-hostable, decentralized alternative Each lemmy server can set its own moderation policy; appointing site-wide admins, and community moderators to keep out the trolls, and foster a healthy, non-toxic environment where all can feel comfortable contributing. -Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Inferno](https://infernojs.org), [Typescript](https://www.typescriptlang.org/) and [Diesel](http://diesel.rs/). +*Note: Federation is still in active development* -- [Documentation](https://dev.lemmy.ml/docs/index.html) -- [Releases / Changelog](/RELEASES.md) -- [Contributing](https://dev.lemmy.ml/docs/contributing.html) +### Why's it called Lemmy? -## Repository Mirrors +- Lead singer from [Motörhead](https://invidio.us/watch?v=pWB5JZRGl0U). +- The old school [video game](). +- The [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa). +- The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/). -- [GitHub](https://github.com/dessalines/lemmy) -- [Gitea](https://yerbamate.dev/dessalines/lemmy) -- [GitLab](https://gitlab.com/dessalines/lemmy) +### Built With + +- [Rust](https://www.rust-lang.org) +- [Actix](https://actix.rs/) +- [Diesel](http://diesel.rs/) +- [Inferno](https://infernojs.org) +- [Typescript](https://www.typescriptlang.org/) ## Features @@ -89,60 +95,11 @@ Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Infern - Front end is `~80kB` gzipped. - Supports arm64 / Raspberry Pi. -## Why's it called Lemmy? - -- Lead singer from [Motörhead](https://invidio.us/watch?v=pWB5JZRGl0U). -- The old school [video game](). -- The [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa). -- The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/). - -## Install - -### Docker - -Make sure you have both docker and docker-compose(>=`1.24.0`) installed: - -```bash -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 -# Edit lemmy.hjson to do more configuration -docker-compose up -d -``` - -and go to http://localhost:8536. - -[A sample nginx config](/ansible/templates/nginx.conf) (Image uploading won't work without this), could be setup with: - -```bash -wget https://raw.githubusercontent.com/dessalines/lemmy/master/ansible/templates/nginx.conf -# Replace the {{ vars }} -sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf -``` -#### Updating - -To update to the newest version, run: - -```bash -wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml -docker-compose up -d -``` - -### Ansible - -First, you need to [install Ansible on your local computer](https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html) (e.g. using `sudo apt install ansible`) or the equivalent for you platform. - -Then run the following commands on your local computer: - -```bash -git clone https://github.com/dessalines/lemmy.git -cd lemmy/ansible/ -cp inventory.example inventory -nano inventory # enter your server, domain, contact email -ansible-playbook lemmy.yml --become -``` +## Installation +- [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. @@ -157,7 +114,13 @@ Lemmy is free, open-source software, meaning no advertising, monetizing, or vent - ethereum: `0x400c96c96acbC6E7B3B43B1dc1BB446540a88A01` - monero: `41taVyY6e1xApqKyMVDRVxJ76sPkfZhALLTjRvVKpaAh2pBd4wv9RgYj1tSPrx8wc6iE1uWUfjtQdTmTy2FGMeChGVKPQuV` -## Translations +## Contributing + +- [Contributing instructions](https://dev.lemmy.ml/docs/contributing.html) +- [Docker Development](https://dev.lemmy.ml/docs/contributing_docker_development.html) +- [Local Development](https://dev.lemmy.ml/docs/contributing_local_development.html) + +### Translations If you'd like to add translations, take a look at the [English translation file](ui/src/translations/en.ts). @@ -189,6 +152,14 @@ cd ui ts-node translation_report.ts ``` +## Contact + +- [Mastodon](https://mastodon.social/@LemmyDev) - [![Mastodon Follow](https://img.shields.io/mastodon/follow/810572?domain=https%3A%2F%2Fmastodon.social&style=social)](https://mastodon.social/@LemmyDev) +- [Matrix](https://riot.im/app/#/room/#rust-reddit-fediverse:matrix.org) - [![Matrix](https://img.shields.io/matrix/rust-reddit-fediverse:matrix.org.svg?label=matrix-chat)](https://riot.im/app/#/room/#rust-reddit-fediverse:matrix.org) +- [GitHub](https://github.com/dessalines/lemmy) +- [Gitea](https://yerbamate.dev/dessalines/lemmy) +- [GitLab](https://gitlab.com/dessalines/lemmy) + ## Credits Logo made by Andy Cuccaro (@andycuccaro) under the CC-BY-SA 4.0 license. diff --git a/docs/src/about.md b/docs/src/about.md index 7aa1be27a..33ecb211a 100644 --- a/docs/src/about.md +++ b/docs/src/about.md @@ -1,6 +1,8 @@ -# Lemmy - A link aggregator / reddit clone for the fediverse. +## About The Project -[Lemmy Dev instance](https://dev.lemmy.ml) *for testing purposes only* +Front Page|Post +---|--- +![main screen](https://i.imgur.com/kZSRcRu.png)|![chat screen](https://i.imgur.com/4XghNh6.png) [Lemmy](https://github.com/dessalines/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse). @@ -10,6 +12,8 @@ The overall goal is to create an easily self-hostable, decentralized alternative Each lemmy server can set its own moderation policy; appointing site-wide admins, and community moderators to keep out the trolls, and foster a healthy, non-toxic environment where all can feel comfortable contributing. +*Note: Federation is still in active development* + ### Why's it called Lemmy? - Lead singer from [Motörhead](https://invidio.us/watch?v=pWB5JZRGl0U). @@ -17,4 +21,10 @@ Each lemmy server can set its own moderation policy; appointing site-wide admins - The [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa). - The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/). -Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Inferno](https://infernojs.org), [Typescript](https://www.typescriptlang.org/) and [Diesel](http://diesel.rs/). +### Built With + +- [Rust](https://www.rust-lang.org) +- [Actix](https://actix.rs/) +- [Diesel](http://diesel.rs/) +- [Inferno](https://infernojs.org) +- [Typescript](https://www.typescriptlang.org/) diff --git a/docs/src/about_features.md b/docs/src/about_features.md index 5c70c978e..a8371fc4d 100644 --- a/docs/src/about_features.md +++ b/docs/src/about_features.md @@ -1,20 +1,27 @@ # Features + - Open source, [AGPL License](/LICENSE). - Self hostable, easy to deploy. - Comes with [Docker](#docker), [Ansible](#ansible), [Kubernetes](#kubernetes). - Clean, mobile-friendly interface. + - Only a minimum of a username and password is required to sign up! + - User avatar support. - Live-updating Comment threads. - Full vote scores `(+/-)` like old reddit. - Themes, including light, dark, and solarized. - Emojis with autocomplete support. Start typing `:` - User tagging using `@`, Community tagging using `#`. + - Integrated image uploading in both posts and comments. + - 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. - i18n / internationalization support. - RSS / Atom feeds for `All`, `Subscribed`, `Inbox`, `User`, and `Community`. - Cross-posting support. - A *similar post search* when creating new posts. Great for question / answer communities. - Moderation abilities. - Public Moderation Logs. + - Can sticky posts to the top of communities. - Both site admins, and community moderators, who can appoint other moderators. - Can lock, remove, and restore posts and comments. - Can ban and unban users from communities and the site. diff --git a/docs/src/administration.md b/docs/src/administration.md index c4c2b01f1..9851232d9 100644 --- a/docs/src/administration.md +++ b/docs/src/administration.md @@ -1 +1,3 @@ -Information for Lemmy instance admins, and those who want to start an instance. \ No newline at end of file +# Admin info + +Information for Lemmy instance admins, and those who want to start an instance. diff --git a/docs/src/administration_configuration.md b/docs/src/administration_configuration.md index 73ea35042..600fc3d20 100644 --- a/docs/src/administration_configuration.md +++ b/docs/src/administration_configuration.md @@ -1,3 +1,5 @@ +# Configuration + The configuration is based on the file [defaults.hjson](server/config/defaults.hjson). This file also contains documentation for all the available options. To override the defaults, you can copy the options you want to change into your local `config.hjson` file. Additionally, you can override any config files with environment variables. These have the same name as the config options, and are prefixed with `LEMMY_`. For example, you can override the `database.password` with diff --git a/docs/src/administration_install_ansible.md b/docs/src/administration_install_ansible.md index 03642b897..875dae6a1 100644 --- a/docs/src/administration_install_ansible.md +++ b/docs/src/administration_install_ansible.md @@ -1,3 +1,5 @@ +# Ansible Installation + First, you need to [install Ansible on your local computer](https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html) (e.g. using `sudo apt install ansible`) or the equivalent for you platform. Then run the following commands on your local computer: diff --git a/docs/src/administration_install_docker.md b/docs/src/administration_install_docker.md index 64abe737e..f92cbd5be 100644 --- a/docs/src/administration_install_docker.md +++ b/docs/src/administration_install_docker.md @@ -1,3 +1,5 @@ +# Docker Installation + Make sure you have both docker and docker-compose(>=`1.24.0`) installed: ```bash @@ -5,20 +7,20 @@ 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 -# Edit lemmy.hjson to do more configuration +# Edit lemmy.hjson, and docker-compose.yml to do more configuration (like adding a custom password) docker-compose up -d ``` and go to http://localhost:8536. -[A sample nginx config](/ansible/templates/nginx.conf), could be setup with: +[A sample nginx config](/ansible/templates/nginx.conf) (Note: Avatar / Image uploading won't work without this), could be setup with: ```bash wget https://raw.githubusercontent.com/dessalines/lemmy/master/ansible/templates/nginx.conf # Replace the {{ vars }} sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf ``` -#### Updating +## Updating To update to the newest version, run: diff --git a/docs/src/administration_install_kubernetes.md b/docs/src/administration_install_kubernetes.md index 886558dce..729cb1558 100644 --- a/docs/src/administration_install_kubernetes.md +++ b/docs/src/administration_install_kubernetes.md @@ -1,3 +1,5 @@ +# Kubernetes Installation + You'll need to have an existing Kubernetes cluster and [storage class](https://kubernetes.io/docs/concepts/storage/storage-classes/). Setting this up will vary depending on your provider. To try it locally, you can use [MicroK8s](https://microk8s.io/) or [Minikube](https://kubernetes.io/docs/tasks/tools/install-minikube/). diff --git a/docs/src/contributing_docker_development.md b/docs/src/contributing_docker_development.md index 0ed5bde5e..d5ab58293 100644 --- a/docs/src/contributing_docker_development.md +++ b/docs/src/contributing_docker_development.md @@ -1,4 +1,6 @@ -Run: +# Docker Development + +## Running ```bash git clone https://github.com/dessalines/lemmy @@ -8,4 +10,4 @@ cd lemmy/docker/dev and go to http://localhost:8536. -Note that compile times are relatively long with Docker, because builds can't be properly cached. If this is a problem for you, you should use [Local Development](contributing_local_development.md). \ No newline at end of file +Note that compile times when changing `Cargo.toml` are relatively long with Docker, because builds can't be incrementally cached. If this is a problem for you, you should use [Local Development](contributing_local_development.md). diff --git a/docs/src/contributing_websocket_http_api.md b/docs/src/contributing_websocket_http_api.md index 9e87d4faa..a73a1c133 100644 --- a/docs/src/contributing_websocket_http_api.md +++ b/docs/src/contributing_websocket_http_api.md @@ -1,4 +1,5 @@ # Lemmy API + *Note: this may lag behind the actual API endpoints [here](../server/src/api).* From 6fce9911f963263fa92dbb65325256f034f0a149 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Thu, 6 Feb 2020 13:10:43 -0500 Subject: [PATCH 16/77] Increasing post title length limit to 200. Fixes #515 --- README.md | 26 ++-- .../down.sql | 132 +++++++++++++++++ .../up.sql | 133 ++++++++++++++++++ server/src/api/post.rs | 20 ++- ui/src/components/post-form.tsx | 9 +- ui/src/translations/en.ts | 1 + 6 files changed, 304 insertions(+), 17 deletions(-) create mode 100644 server/migrations/2020-02-06-165953_change_post_title_length/down.sql create mode 100644 server/migrations/2020-02-06-165953_change_post_title_length/up.sql diff --git a/README.md b/README.md index 1c5709745..fcb07e72d 100644 --- a/README.md +++ b/README.md @@ -130,19 +130,19 @@ If you'd like to add translations, take a look at the [English translation file] lang | done | missing ---- | ---- | ------- -ca | 98% | cross_posted_to,old,support_on_liberapay,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,support_on_liberapay,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action -fa | 72% | 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,community_moderator_already_exists,community_follower_already_exists,community_user_already_banned,no_slurs,admin_already_created,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action -eo | 74% | 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,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action -es | 99% | cross_posted_to,support_on_liberapay -fi | 98% | cross_posted_to,old,support_on_liberapay,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,support_on_liberapay,donate_to_lemmy,donate,from,logged_in,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,support_on_liberapay,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action -nl | 98% | cross_posted_to,support_on_liberapay,time,action -pt-br | 100% | support_on_liberapay -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,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,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,support_on_liberapay,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action +ca | 97% | cross_posted_to,old,support_on_liberapay,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,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,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 | 74% | 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,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,post_title_too_long +fi | 97% | cross_posted_to,old,support_on_liberapay,post_title_too_long,time,action +fr | 82% | 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,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,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,post_title_too_long,time,action +pt-br | 100% | 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,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,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,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action If you'd like to update this report, run: diff --git a/server/migrations/2020-02-06-165953_change_post_title_length/down.sql b/server/migrations/2020-02-06-165953_change_post_title_length/down.sql new file mode 100644 index 000000000..2bc765f85 --- /dev/null +++ b/server/migrations/2020-02-06-165953_change_post_title_length/down.sql @@ -0,0 +1,132 @@ +-- Drop the dependent views +drop view post_view; +drop view post_mview; +drop materialized view post_aggregates_mview; +drop view post_aggregates_view; +drop view mod_remove_post_view; +drop view mod_sticky_post_view; +drop view mod_lock_post_view; +drop view mod_remove_comment_view; + +alter table post alter column name type varchar(100); + +-- 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), p.published) as hot_rank +from post p +left join post_like pl on p.id = pl.post_id +group by p.id; + +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 +; + +-- The mod views + +create view mod_remove_post_view as +select mrp.*, +(select name from user_ u where mrp.mod_user_id = u.id) as mod_user_name, +(select name from post p where mrp.post_id = p.id) as post_name, +(select c.id from post p, community c where mrp.post_id = p.id and p.community_id = c.id) as community_id, +(select c.name from post p, community c where mrp.post_id = p.id and p.community_id = c.id) as community_name +from mod_remove_post mrp; + +create view mod_lock_post_view as +select mlp.*, +(select name from user_ u where mlp.mod_user_id = u.id) as mod_user_name, +(select name from post p where mlp.post_id = p.id) as post_name, +(select c.id from post p, community c where mlp.post_id = p.id and p.community_id = c.id) as community_id, +(select c.name from post p, community c where mlp.post_id = p.id and p.community_id = c.id) as community_name +from mod_lock_post mlp; + +create view mod_remove_comment_view as +select mrc.*, +(select name from user_ u where mrc.mod_user_id = u.id) as mod_user_name, +(select c.id from comment c where mrc.comment_id = c.id) as comment_user_id, +(select name from user_ u, comment c where mrc.comment_id = c.id and u.id = c.creator_id) as comment_user_name, +(select content from comment c where mrc.comment_id = c.id) as comment_content, +(select p.id from post p, comment c where mrc.comment_id = c.id and c.post_id = p.id) as post_id, +(select p.name from post p, comment c where mrc.comment_id = c.id and c.post_id = p.id) as post_name, +(select co.id from comment c, post p, community co where mrc.comment_id = c.id and c.post_id = p.id and p.community_id = co.id) as community_id, +(select co.name from comment c, post p, community co where mrc.comment_id = c.id and c.post_id = p.id and p.community_id = co.id) as community_name +from mod_remove_comment mrc; + +create view mod_sticky_post_view as +select msp.*, +(select name from user_ u where msp.mod_user_id = u.id) as mod_user_name, +(select name from post p where msp.post_id = p.id) as post_name, +(select c.id from post p, community c where msp.post_id = p.id and p.community_id = c.id) as community_id, +(select c.name from post p, community c where msp.post_id = p.id and p.community_id = c.id) as community_name +from mod_sticky_post msp; diff --git a/server/migrations/2020-02-06-165953_change_post_title_length/up.sql b/server/migrations/2020-02-06-165953_change_post_title_length/up.sql new file mode 100644 index 000000000..006a7d049 --- /dev/null +++ b/server/migrations/2020-02-06-165953_change_post_title_length/up.sql @@ -0,0 +1,133 @@ +-- Drop the dependent views +drop view post_view; +drop view post_mview; +drop materialized view post_aggregates_mview; +drop view post_aggregates_view; +drop view mod_remove_post_view; +drop view mod_sticky_post_view; +drop view mod_lock_post_view; +drop view mod_remove_comment_view; + +-- Add the extra post limit +alter table post alter column name type varchar(200); + +-- 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), p.published) as hot_rank +from post p +left join post_like pl on p.id = pl.post_id +group by p.id; + +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 +; + +-- The mod views + +create view mod_remove_post_view as +select mrp.*, +(select name from user_ u where mrp.mod_user_id = u.id) as mod_user_name, +(select name from post p where mrp.post_id = p.id) as post_name, +(select c.id from post p, community c where mrp.post_id = p.id and p.community_id = c.id) as community_id, +(select c.name from post p, community c where mrp.post_id = p.id and p.community_id = c.id) as community_name +from mod_remove_post mrp; + +create view mod_lock_post_view as +select mlp.*, +(select name from user_ u where mlp.mod_user_id = u.id) as mod_user_name, +(select name from post p where mlp.post_id = p.id) as post_name, +(select c.id from post p, community c where mlp.post_id = p.id and p.community_id = c.id) as community_id, +(select c.name from post p, community c where mlp.post_id = p.id and p.community_id = c.id) as community_name +from mod_lock_post mlp; + +create view mod_remove_comment_view as +select mrc.*, +(select name from user_ u where mrc.mod_user_id = u.id) as mod_user_name, +(select c.id from comment c where mrc.comment_id = c.id) as comment_user_id, +(select name from user_ u, comment c where mrc.comment_id = c.id and u.id = c.creator_id) as comment_user_name, +(select content from comment c where mrc.comment_id = c.id) as comment_content, +(select p.id from post p, comment c where mrc.comment_id = c.id and c.post_id = p.id) as post_id, +(select p.name from post p, comment c where mrc.comment_id = c.id and c.post_id = p.id) as post_name, +(select co.id from comment c, post p, community co where mrc.comment_id = c.id and c.post_id = p.id and p.community_id = co.id) as community_id, +(select co.name from comment c, post p, community co where mrc.comment_id = c.id and c.post_id = p.id and p.community_id = co.id) as community_name +from mod_remove_comment mrc; + +create view mod_sticky_post_view as +select msp.*, +(select name from user_ u where msp.mod_user_id = u.id) as mod_user_name, +(select name from post p where msp.post_id = p.id) as post_name, +(select c.id from post p, community c where msp.post_id = p.id and p.community_id = c.id) as community_id, +(select c.name from post p, community c where msp.post_id = p.id and p.community_id = c.id) as community_name +from mod_sticky_post msp; diff --git a/server/src/api/post.rs b/server/src/api/post.rs index bd276be5a..00bf8e114 100644 --- a/server/src/api/post.rs +++ b/server/src/api/post.rs @@ -126,7 +126,15 @@ impl Perform for Oper { let inserted_post = match Post::create(&conn, &post_form) { Ok(post) => post, - Err(_e) => return Err(APIError::err("couldnt_create_post").into()), + Err(e) => { + let err_type = if e.to_string() == "value too long for type character varying(200)" { + "post_title_too_long" + } else { + "couldnt_create_post" + }; + + return Err(APIError::err(err_type).into()); + } }; // They like their own post by default @@ -361,7 +369,15 @@ impl Perform for Oper { let _updated_post = match Post::update(&conn, data.edit_id, &post_form) { Ok(post) => post, - Err(_e) => return Err(APIError::err("couldnt_update_post").into()), + Err(e) => { + let err_type = if e.to_string() == "value too long for type character varying(200)" { + "post_title_too_long" + } else { + "couldnt_update_post" + }; + + return Err(APIError::err(err_type).into()); + } }; // Mod tables diff --git a/ui/src/components/post-form.tsx b/ui/src/components/post-form.tsx index 5d9c0f3cb..35d4e5956 100644 --- a/ui/src/components/post-form.tsx +++ b/ui/src/components/post-form.tsx @@ -37,6 +37,8 @@ import autosize from 'autosize'; import Tribute from 'tributejs/src/Tribute.js'; import { i18n } from '../i18next'; +const MAX_POST_TITLE_LENGTH = 200; + interface PostFormProps { post?: Post; // If a post is given, that means this is an edit params?: PostFormParams; @@ -232,7 +234,7 @@ export class PostForm extends Component { required rows={2} minLength={3} - maxLength={100} + maxLength={MAX_POST_TITLE_LENGTH} /> {this.state.suggestedPosts.length > 0 && ( <> @@ -360,7 +362,10 @@ export class PostForm extends Component { } copySuggestedTitle(i: PostForm) { - i.state.postForm.name = i.state.suggestedTitle; + i.state.postForm.name = i.state.suggestedTitle.substring( + 0, + MAX_POST_TITLE_LENGTH + ); i.state.suggestedTitle = undefined; i.setState(i.state); } diff --git a/ui/src/translations/en.ts b/ui/src/translations/en.ts index e58e94e50..f71c203bc 100644 --- a/ui/src/translations/en.ts +++ b/ui/src/translations/en.ts @@ -211,6 +211,7 @@ export const en = { 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", From 7612a548940fe1a1a69ba6e7cf2197cc407020b4 Mon Sep 17 00:00:00 2001 From: Richie Zhang <12566991+StaticallyTypedRice@users.noreply.github.com> Date: Thu, 6 Feb 2020 12:25:13 -0800 Subject: [PATCH 17/77] Add instructions for unning db-init.sh to contributing_local_development.md --- docs/src/contributing_local_development.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/src/contributing_local_development.md b/docs/src/contributing_local_development.md index c19bcba84..7d782925a 100644 --- a/docs/src/contributing_local_development.md +++ b/docs/src/contributing_local_development.md @@ -7,9 +7,18 @@ #### Set up Postgres DB ```bash - psql -c "create user lemmy with password 'password' superuser;" -U postgres - psql -c 'create database lemmy with owner lemmy;' -U postgres - export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy +# Start at the root of the Lemmy repository + +cd server +./db-init.sh +``` + +Or run the commands manually: + +```bash +psql -c "create user lemmy with password 'password' superuser;" -U postgres +psql -c 'create database lemmy with owner lemmy;' -U postgres +export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy ``` #### Running From 510387874112f4bf91df7357b731cf0e055f1761 Mon Sep 17 00:00:00 2001 From: Richie Zhang <12566991+StaticallyTypedRice@users.noreply.github.com> Date: Thu, 6 Feb 2020 12:26:01 -0800 Subject: [PATCH 18/77] Add instructions for unning db-init.sh to administration_configuration.md --- docs/src/administration_configuration.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/src/administration_configuration.md b/docs/src/administration_configuration.md index 73ea35042..c47d32362 100644 --- a/docs/src/administration_configuration.md +++ b/docs/src/administration_configuration.md @@ -4,3 +4,12 @@ Additionally, you can override any config files with environment variables. Thes `LEMMY__DATABASE__POOL_SIZE=10`. An additional option `LEMMY_DATABASE_URL` is available, which can be used with a PostgreSQL connection string like `postgres://lemmy:password@lemmy_db:5432/lemmy`, passing all connection details at once. + +If the Docker container is not used, manually create the database specified above by running the following commands: + +```bash +# Start at the root of the Lemmy repository + +cd server +./db-init.sh +``` From 1d4dc19d6f2af133d8a4b6bd77a33cdb8d73aebc Mon Sep 17 00:00:00 2001 From: Richie Zhang <12566991+StaticallyTypedRice@users.noreply.github.com> Date: Thu, 6 Feb 2020 13:07:34 -0800 Subject: [PATCH 19/77] Implement password verification in db-init.sh. --- server/db-init.sh | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/server/db-init.sh b/server/db-init.sh index 77b9a6d71..c9150e9de 100644 --- a/server/db-init.sh +++ b/server/db-init.sh @@ -4,9 +4,40 @@ username=lemmy dbname=lemmy port=5432 -read -p "Enter database password: " -s password -echo +password="" +password_confirm="" +password_valid=0 + +while [ "$password_valid" == 0 ] +do + read -p "Enter database password: " -s password + echo + + read -p "Verify database password: " -s password_confirm + echo + echo + + # Start the loop from the top if either check fails + if [ -z "$password" ] + then + echo "Error: Password cannot be empty." 1>&2 + echo + continue + fi + if [ "$password" != "$password_confirm" ] + then + echo "Error: Passwords don't match." 1>&2 + echo + continue + fi + + # Set the password_valid variable to break out of the loop + password_valid=1 +done + psql -c "CREATE USER $username WITH PASSWORD '$password' SUPERUSER;" -U postgres psql -c 'CREATE DATABASE $dbname WITH OWNER $username;' -U postgres export LEMMY_DATABASE_URL=postgres://$username:$password@localhost:$port/$dbname + +echo $LEMMY_DATABASE_URL From d5b483d4d1eaa1d22f4bf8564614e250f5029d9c Mon Sep 17 00:00:00 2001 From: Dessalines Date: Thu, 6 Feb 2020 16:07:59 -0500 Subject: [PATCH 20/77] Fixing rate limit checking to only ping after a success. Fixes #516 --- README.md | 14 ++-- docker/lemmy.hjson | 2 +- server/config/defaults.hjson | 2 +- server/src/websocket/server.rs | 141 ++++++++++++++++++++++----------- 4 files changed, 103 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 1c5709745..27637b8aa 100644 --- a/README.md +++ b/README.md @@ -131,18 +131,18 @@ If you'd like to add translations, take a look at the [English translation file] lang | done | missing ---- | ---- | ------- ca | 98% | cross_posted_to,old,support_on_liberapay,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,support_on_liberapay,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,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,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action fa | 72% | 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,community_moderator_already_exists,community_follower_already_exists,community_user_already_banned,no_slurs,admin_already_created,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action eo | 74% | 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,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action -es | 99% | cross_posted_to,support_on_liberapay +es | 100% | cross_posted_to fi | 98% | cross_posted_to,old,support_on_liberapay,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,support_on_liberapay,donate_to_lemmy,donate,from,logged_in,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,support_on_liberapay,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action -nl | 98% | cross_posted_to,support_on_liberapay,time,action -pt-br | 100% | support_on_liberapay +fr | 82% | 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,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action +it | 83% | 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,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action +nl | 99% | cross_posted_to,time,action +pt-br | 100% | 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,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,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,support_on_liberapay,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,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,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action If you'd like to update this report, run: diff --git a/docker/lemmy.hjson b/docker/lemmy.hjson index fce4470ca..55c2f2b76 100644 --- a/docker/lemmy.hjson +++ b/docker/lemmy.hjson @@ -29,7 +29,7 @@ # rate limits for various user actions, by user ip rate_limit: { # maximum number of messages created in interval - message: 30 + message: 180 # interval length for message limit message_per_second: 60 # maximum number of posts created in interval diff --git a/server/config/defaults.hjson b/server/config/defaults.hjson index 9a7ad49db..2b37f3bb3 100644 --- a/server/config/defaults.hjson +++ b/server/config/defaults.hjson @@ -30,7 +30,7 @@ # rate limits for various user actions, by user ip rate_limit: { # maximum number of messages created in interval - message: 30 + message: 180 # interval length for message limit message_per_second: 60 # maximum number of posts created in interval diff --git a/server/src/websocket/server.rs b/server/src/websocket/server.rs index a26c8144e..fc838c1fc 100644 --- a/server/src/websocket/server.rs +++ b/server/src/websocket/server.rs @@ -12,6 +12,7 @@ use serde_json::Value; use std::collections::{HashMap, HashSet}; use std::str::FromStr; use std::time::SystemTime; +use strum::IntoEnumIterator; use crate::api::comment::*; use crate::api::community::*; @@ -71,6 +72,13 @@ pub struct SessionInfo { pub ip: IPAddr, } +#[derive(Eq, PartialEq, Hash, Debug, EnumIter, Copy, Clone)] +pub enum RateLimitType { + Message, + Register, + Post, +} + /// `ChatServer` manages chat rooms and responsible for coordinating chat /// session. pub struct ChatServer { @@ -87,8 +95,8 @@ pub struct ChatServer { /// sessions (IE clients) user_rooms: HashMap>, - /// Rate limiting based on IP addr - rate_limits: HashMap, + /// Rate limiting based on rate type and IP addr + rate_limit_buckets: HashMap>, rng: ThreadRng, db: Pool>, @@ -98,7 +106,7 @@ impl ChatServer { pub fn startup(db: Pool>) -> ChatServer { ChatServer { sessions: HashMap::new(), - rate_limits: HashMap::new(), + rate_limit_buckets: HashMap::new(), post_rooms: HashMap::new(), community_rooms: HashMap::new(), user_rooms: HashMap::new(), @@ -259,60 +267,82 @@ impl ChatServer { to_json_string(&user_operation, post) } - fn check_rate_limit_register(&mut self, id: usize) -> Result<(), Error> { + fn check_rate_limit_register(&mut self, id: usize, check_only: bool) -> Result<(), Error> { self.check_rate_limit_full( + RateLimitType::Register, id, Settings::get().rate_limit.register, Settings::get().rate_limit.register_per_second, + check_only, ) } - fn check_rate_limit_post(&mut self, id: usize) -> Result<(), Error> { + fn check_rate_limit_post(&mut self, id: usize, check_only: bool) -> Result<(), Error> { self.check_rate_limit_full( + RateLimitType::Post, id, Settings::get().rate_limit.post, Settings::get().rate_limit.post_per_second, + check_only, ) } - fn check_rate_limit_message(&mut self, id: usize) -> Result<(), Error> { + fn check_rate_limit_message(&mut self, id: usize, check_only: bool) -> Result<(), Error> { self.check_rate_limit_full( + RateLimitType::Message, id, Settings::get().rate_limit.message, Settings::get().rate_limit.message_per_second, + check_only, ) } #[allow(clippy::float_cmp)] - fn check_rate_limit_full(&mut self, id: usize, rate: i32, per: i32) -> Result<(), Error> { + fn check_rate_limit_full( + &mut self, + type_: RateLimitType, + id: usize, + rate: i32, + per: i32, + check_only: bool, + ) -> Result<(), Error> { if let Some(info) = self.sessions.get(&id) { - if let Some(rate_limit) = self.rate_limits.get_mut(&info.ip) { - // The initial value - if rate_limit.allowance == -2f64 { - rate_limit.allowance = rate as f64; - }; + if let Some(bucket) = self.rate_limit_buckets.get_mut(&type_) { + if let Some(rate_limit) = bucket.get_mut(&info.ip) { + let current = SystemTime::now(); + let time_passed = current.duration_since(rate_limit.last_checked)?.as_secs() as f64; - let current = SystemTime::now(); - let time_passed = current.duration_since(rate_limit.last_checked)?.as_secs() as f64; - rate_limit.last_checked = current; - rate_limit.allowance += time_passed * (rate as f64 / per as f64); - if rate_limit.allowance > rate as f64 { - rate_limit.allowance = rate as f64; - } + // The initial value + if rate_limit.allowance == -2f64 { + rate_limit.allowance = rate as f64; + }; - if rate_limit.allowance < 1.0 { - println!( - "Rate limited IP: {}, time_passed: {}, allowance: {}", - &info.ip, time_passed, rate_limit.allowance - ); - Err( - APIError { - message: format!("Too many requests. {} per {} seconds", rate, per), + rate_limit.last_checked = current; + if !check_only { + rate_limit.allowance += time_passed * (rate as f64 / per as f64); + if rate_limit.allowance > rate as f64 { + rate_limit.allowance = rate as f64; } - .into(), - ) + } + + if rate_limit.allowance < 1.0 { + println!( + "Rate limited IP: {}, time_passed: {}, allowance: {}", + &info.ip, time_passed, rate_limit.allowance + ); + Err( + APIError { + message: format!("Too many requests. {} per {} seconds", rate, per), + } + .into(), + ) + } else { + if !check_only { + rate_limit.allowance -= 1.0; + } + Ok(()) + } } else { - rate_limit.allowance -= 1.0; Ok(()) } } else { @@ -350,14 +380,24 @@ impl Handler for ChatServer { }, ); - if self.rate_limits.get(&msg.ip).is_none() { - self.rate_limits.insert( - msg.ip, - RateLimitBucket { - last_checked: SystemTime::now(), - allowance: -2f64, - }, - ); + for rate_limit_type in RateLimitType::iter() { + if self.rate_limit_buckets.get(&rate_limit_type).is_none() { + self + .rate_limit_buckets + .insert(rate_limit_type, HashMap::new()); + } + + if let Some(bucket) = self.rate_limit_buckets.get_mut(&rate_limit_type) { + if bucket.get(&msg.ip).is_none() { + bucket.insert( + msg.ip.to_owned(), + RateLimitBucket { + last_checked: SystemTime::now(), + allowance: -2f64, + }, + ); + } + } } id @@ -446,11 +486,18 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result do_user_operation::(user_operation, data, &conn), UserOperation::Register => { - chat.check_rate_limit_register(msg.id)?; - do_user_operation::(user_operation, data, &conn) + chat.check_rate_limit_register(msg.id, true)?; + let register: Register = serde_json::from_str(data)?; + let res = Oper::new(register).perform(&conn)?; + chat.check_rate_limit_register(msg.id, false)?; + to_json_string(&user_operation, &res) } UserOperation::GetUserDetails => { do_user_operation::(user_operation, data, &conn) @@ -503,8 +550,11 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result(user_operation, data, &conn) } UserOperation::CreateCommunity => { - chat.check_rate_limit_register(msg.id)?; - do_user_operation::(user_operation, data, &conn) + chat.check_rate_limit_register(msg.id, true)?; + let create_community: CreateCommunity = serde_json::from_str(data)?; + let res = Oper::new(create_community).perform(&conn)?; + chat.check_rate_limit_register(msg.id, false)?; + to_json_string(&user_operation, &res) } UserOperation::EditCommunity => { let edit_community: EditCommunity = serde_json::from_str(data)?; @@ -566,14 +616,14 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result { - chat.check_rate_limit_post(msg.id)?; + chat.check_rate_limit_post(msg.id, true)?; let create_post: CreatePost = serde_json::from_str(data)?; let res = Oper::new(create_post).perform(&conn)?; + chat.check_rate_limit_post(msg.id, false)?; chat.post_sends(UserOperation::CreatePost, res, msg.id) } UserOperation::CreatePostLike => { - chat.check_rate_limit_message(msg.id)?; let create_post_like: CreatePostLike = serde_json::from_str(data)?; let res = Oper::new(create_post_like).perform(&conn)?; @@ -589,7 +639,6 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result(user_operation, data, &conn) } UserOperation::CreateComment => { - chat.check_rate_limit_message(msg.id)?; let create_comment: CreateComment = serde_json::from_str(data)?; let res = Oper::new(create_comment).perform(&conn)?; @@ -605,7 +654,6 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result(user_operation, data, &conn) } UserOperation::CreateCommentLike => { - chat.check_rate_limit_message(msg.id)?; let create_comment_like: CreateCommentLike = serde_json::from_str(data)?; let res = Oper::new(create_comment_like).perform(&conn)?; @@ -649,7 +697,6 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result(user_operation, data, &conn) } UserOperation::CreatePrivateMessage => { - chat.check_rate_limit_message(msg.id)?; let create_private_message: CreatePrivateMessage = serde_json::from_str(data)?; let recipient_id = create_private_message.recipient_id; let res = Oper::new(create_private_message).perform(&conn)?; From 9fb5d55569a1c1a5107d696f92eafb63c9c609e4 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Thu, 6 Feb 2020 16:16:51 -0500 Subject: [PATCH 21/77] Version v0.6.12 --- ansible/VERSION | 2 +- docker/prod/docker-compose.yml | 2 +- server/src/version.rs | 2 +- ui/src/version.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ansible/VERSION b/ansible/VERSION index 882307ccd..00a9fa57a 100644 --- a/ansible/VERSION +++ b/ansible/VERSION @@ -1 +1 @@ -v0.6.11 +v0.6.12 diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml index da78126c3..52689cc40 100644 --- a/docker/prod/docker-compose.yml +++ b/docker/prod/docker-compose.yml @@ -11,7 +11,7 @@ services: - lemmy_db:/var/lib/postgresql/data restart: always lemmy: - image: dessalines/lemmy:v0.6.11 + image: dessalines/lemmy:v0.6.12 ports: - "127.0.0.1:8536:8536" restart: always diff --git a/server/src/version.rs b/server/src/version.rs index 28373cf57..2f84f1b80 100644 --- a/server/src/version.rs +++ b/server/src/version.rs @@ -1 +1 @@ -pub const VERSION: &str = "v0.6.11"; +pub const VERSION: &str = "v0.6.12"; diff --git a/ui/src/version.ts b/ui/src/version.ts index f851d7188..3f3f923fc 100644 --- a/ui/src/version.ts +++ b/ui/src/version.ts @@ -1 +1 @@ -export const version: string = 'v0.6.11'; +export const version: string = 'v0.6.12'; From c9060f76b47dfef5ec1a82b7d3943df2e8e38752 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Thu, 6 Feb 2020 16:26:03 -0500 Subject: [PATCH 22/77] Minor fixes to docs. --- docs/src/administration_configuration.md | 2 -- docs/src/contributing_local_development.md | 2 -- 2 files changed, 4 deletions(-) diff --git a/docs/src/administration_configuration.md b/docs/src/administration_configuration.md index 55f93f05b..8900ce8d5 100644 --- a/docs/src/administration_configuration.md +++ b/docs/src/administration_configuration.md @@ -10,8 +10,6 @@ An additional option `LEMMY_DATABASE_URL` is available, which can be used with a If the Docker container is not used, manually create the database specified above by running the following commands: ```bash -# Start at the root of the Lemmy repository - cd server ./db-init.sh ``` diff --git a/docs/src/contributing_local_development.md b/docs/src/contributing_local_development.md index 7d782925a..175b000c7 100644 --- a/docs/src/contributing_local_development.md +++ b/docs/src/contributing_local_development.md @@ -7,8 +7,6 @@ #### Set up Postgres DB ```bash -# Start at the root of the Lemmy repository - cd server ./db-init.sh ``` From bbc7159ede28e4ae335504f6d7d6e0170b2a2a46 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Thu, 6 Feb 2020 22:24:23 -0500 Subject: [PATCH 23/77] Fix expanded image height. Fixes #455 --- ui/assets/css/main.css | 5 +++++ ui/src/components/post-listing.tsx | 2 +- ui/src/utils.ts | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/ui/assets/css/main.css b/ui/assets/css/main.css index 2703d550c..b1ad884a5 100644 --- a/ui/assets/css/main.css +++ b/ui/assets/css/main.css @@ -37,6 +37,7 @@ } .md-div img { + max-height: 90vh; max-width: 100%; height: auto; } @@ -170,3 +171,7 @@ hr { -o-filter: blur(10px); -ms-filter: blur(10px); } + +.img-expanded { + max-height: 90vh; +} diff --git a/ui/src/components/post-listing.tsx b/ui/src/components/post-listing.tsx index 9b3141c42..f11d9e144 100644 --- a/ui/src/components/post-listing.tsx +++ b/ui/src/components/post-listing.tsx @@ -237,7 +237,7 @@ export class PostListing extends Component { class="pointer" onClick={linkEvent(this, this.handleImageExpandClick)} > - + diff --git a/ui/src/utils.ts b/ui/src/utils.ts index c53ee76a1..9ad0920f4 100644 --- a/ui/src/utils.ts +++ b/ui/src/utils.ts @@ -384,7 +384,7 @@ export function showAvatars(): boolean { export function imageThumbnailer(url: string): string { let split = url.split('pictshare'); if (split.length > 1) { - let out = `${split[0]}pictshare/140x140${split[1]}`; + let out = `${split[0]}pictshare/192x192${split[1]}`; return out; } else { return url; From 514c1ab298d858b24d30af67578251db19bbc5d8 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Thu, 6 Feb 2020 22:48:43 -0500 Subject: [PATCH 24/77] Fixing rate limiting. --- server/src/websocket/server.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/server/src/websocket/server.rs b/server/src/websocket/server.rs index fc838c1fc..bf3249e1d 100644 --- a/server/src/websocket/server.rs +++ b/server/src/websocket/server.rs @@ -318,11 +318,9 @@ impl ChatServer { }; rate_limit.last_checked = current; - if !check_only { - rate_limit.allowance += time_passed * (rate as f64 / per as f64); - if rate_limit.allowance > rate as f64 { - rate_limit.allowance = rate as f64; - } + rate_limit.allowance += time_passed * (rate as f64 / per as f64); + if !check_only && rate_limit.allowance > rate as f64 { + rate_limit.allowance = rate as f64; } if rate_limit.allowance < 1.0 { From 1c182e381b4de718e1504186e4bd418552d9e34a Mon Sep 17 00:00:00 2001 From: Dessalines Date: Thu, 6 Feb 2020 23:02:26 -0500 Subject: [PATCH 25/77] Version v0.6.13 --- ansible/VERSION | 2 +- docker/prod/docker-compose.yml | 2 +- server/src/version.rs | 2 +- ui/src/version.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ansible/VERSION b/ansible/VERSION index 00a9fa57a..1adfd94d3 100644 --- a/ansible/VERSION +++ b/ansible/VERSION @@ -1 +1 @@ -v0.6.12 +v0.6.13 diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml index 52689cc40..a5c2918ab 100644 --- a/docker/prod/docker-compose.yml +++ b/docker/prod/docker-compose.yml @@ -11,7 +11,7 @@ services: - lemmy_db:/var/lib/postgresql/data restart: always lemmy: - image: dessalines/lemmy:v0.6.12 + image: dessalines/lemmy:v0.6.13 ports: - "127.0.0.1:8536:8536" restart: always diff --git a/server/src/version.rs b/server/src/version.rs index 2f84f1b80..2d9ff73cd 100644 --- a/server/src/version.rs +++ b/server/src/version.rs @@ -1 +1 @@ -pub const VERSION: &str = "v0.6.12"; +pub const VERSION: &str = "v0.6.13"; diff --git a/ui/src/version.ts b/ui/src/version.ts index 3f3f923fc..845bbef1e 100644 --- a/ui/src/version.ts +++ b/ui/src/version.ts @@ -1 +1 @@ -export const version: string = 'v0.6.12'; +export const version: string = 'v0.6.13'; From 68ac96147c32f6bc9f119cfda3f8da9697f77c43 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Fri, 7 Feb 2020 10:12:05 -0500 Subject: [PATCH 26/77] Fix issue with post creating redirecting other posts. Fixes #520 --- ui/src/components/post-form.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/ui/src/components/post-form.tsx b/ui/src/components/post-form.tsx index 35d4e5956..7984c2a85 100644 --- a/ui/src/components/post-form.tsx +++ b/ui/src/components/post-form.tsx @@ -516,12 +516,16 @@ export class PostForm extends Component { this.setState(this.state); } else if (res.op == UserOperation.CreatePost) { let data = res.data as PostResponse; - this.state.loading = false; - this.props.onCreate(data.post.id); + if (data.post.creator_id == UserService.Instance.user.id) { + this.state.loading = false; + this.props.onCreate(data.post.id); + } } else if (res.op == UserOperation.EditPost) { let data = res.data as PostResponse; - this.state.loading = false; - this.props.onEdit(data.post); + if (data.post.creator_id == UserService.Instance.user.id) { + this.state.loading = false; + this.props.onEdit(data.post); + } } else if (res.op == UserOperation.Search) { let data = res.data as SearchResponse; From 65145b719c342732d9d7c6f6376e3d4ebbed241e Mon Sep 17 00:00:00 2001 From: Dessalines Date: Fri, 7 Feb 2020 11:17:15 -0500 Subject: [PATCH 27/77] Adding post body searching. Fixes #507 --- server/src/db/post_view.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/src/db/post_view.rs b/server/src/db/post_view.rs index c80d16967..4d09308d8 100644 --- a/server/src/db/post_view.rs +++ b/server/src/db/post_view.rs @@ -178,7 +178,11 @@ impl<'a> PostQueryBuilder<'a> { pub fn search_term>(mut self, search_term: T) -> Self { use super::post_view::post_mview::dsl::*; if let Some(search_term) = search_term.get_optional() { - self.query = self.query.filter(name.ilike(fuzzy_search(&search_term))); + let searcher = fuzzy_search(&search_term); + self.query = self + .query + .filter(name.ilike(searcher.to_owned())) + .or_filter(body.ilike(searcher)); } self } From 1e157dececec89ec1c13b1102cfc976e55776555 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Fri, 7 Feb 2020 11:28:20 -0500 Subject: [PATCH 28/77] Version v0.6.14 --- ansible/VERSION | 2 +- docker/prod/docker-compose.yml | 2 +- server/src/version.rs | 2 +- ui/src/version.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ansible/VERSION b/ansible/VERSION index 1adfd94d3..dae7cf429 100644 --- a/ansible/VERSION +++ b/ansible/VERSION @@ -1 +1 @@ -v0.6.13 +v0.6.14 diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml index a5c2918ab..7b04dc260 100644 --- a/docker/prod/docker-compose.yml +++ b/docker/prod/docker-compose.yml @@ -11,7 +11,7 @@ services: - lemmy_db:/var/lib/postgresql/data restart: always lemmy: - image: dessalines/lemmy:v0.6.13 + image: dessalines/lemmy:v0.6.14 ports: - "127.0.0.1:8536:8536" restart: always diff --git a/server/src/version.rs b/server/src/version.rs index 2d9ff73cd..4efa3b59f 100644 --- a/server/src/version.rs +++ b/server/src/version.rs @@ -1 +1 @@ -pub const VERSION: &str = "v0.6.13"; +pub const VERSION: &str = "v0.6.14"; diff --git a/ui/src/version.ts b/ui/src/version.ts index 845bbef1e..f31c87acd 100644 --- a/ui/src/version.ts +++ b/ui/src/version.ts @@ -1 +1 @@ -export const version: string = 'v0.6.13'; +export const version: string = 'v0.6.14'; From 049556f146b7810847c3d9d5224af37c09240e0c Mon Sep 17 00:00:00 2001 From: Dessalines Date: Fri, 7 Feb 2020 23:05:15 -0500 Subject: [PATCH 29/77] Add new comments views to main and community pages. Fixes #480 --- README.md | 26 +-- .../down.sql | 206 ++++++++++++++++ .../up.sql | 220 ++++++++++++++++++ server/src/api/comment.rs | 53 +++++ server/src/db/comment_view.rs | 56 ++++- server/src/db/post_view.rs | 50 ++-- server/src/db/user_mention_view.rs | 12 +- server/src/routes/index.rs | 7 +- server/src/websocket/mod.rs | 1 + server/src/websocket/server.rs | 25 ++ ui/src/components/comment-node.tsx | 10 + ui/src/components/comment-nodes.tsx | 2 + ui/src/components/community.tsx | 165 ++++++++++--- ui/src/components/data-type-select.tsx | 65 ++++++ ui/src/components/main.tsx | 209 ++++++++++++----- ui/src/components/user.tsx | 30 +-- ui/src/index.tsx | 4 +- ui/src/interfaces.ts | 22 ++ ui/src/services/WebSocketService.ts | 6 + ui/src/translations/en.ts | 1 + ui/src/utils.ts | 32 +++ 21 files changed, 1048 insertions(+), 154 deletions(-) create mode 100644 server/migrations/2020-02-07-210055_add_comment_subscribed/down.sql create mode 100644 server/migrations/2020-02-07-210055_add_comment_subscribed/up.sql create mode 100644 ui/src/components/data-type-select.tsx diff --git a/README.md b/README.md index fcb07e72d..47290953c 100644 --- a/README.md +++ b/README.md @@ -130,19 +130,19 @@ If you'd like to add translations, take a look at the [English translation file] lang | done | missing ---- | ---- | ------- -ca | 97% | cross_posted_to,old,support_on_liberapay,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,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,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 | 74% | 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,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,post_title_too_long -fi | 97% | cross_posted_to,old,support_on_liberapay,post_title_too_long,time,action -fr | 82% | 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,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,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,post_title_too_long,time,action -pt-br | 100% | 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,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,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,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action +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 If you'd like to update this report, run: diff --git a/server/migrations/2020-02-07-210055_add_comment_subscribed/down.sql b/server/migrations/2020-02-07-210055_add_comment_subscribed/down.sql new file mode 100644 index 000000000..b6120d151 --- /dev/null +++ b/server/migrations/2020-02-07-210055_add_comment_subscribed/down.sql @@ -0,0 +1,206 @@ + +drop view reply_view; +drop view user_mention_view; +drop view user_mention_mview; +drop view comment_view; +drop view comment_mview; +drop materialized view comment_aggregates_mview; +drop view comment_aggregates_view; + +-- reply and comment view +create view comment_aggregates_view as +select +c.*, +(select community_id from post p where p.id = c.post_id), +(select u.banned from user_ u where c.creator_id = u.id) as banned, +(select cb.id::bool from community_user_ban cb, post p where c.creator_id = cb.user_id and p.id = c.post_id and p.community_id = cb.community_id) as banned_from_community, +(select name from user_ where c.creator_id = user_.id) as creator_name, +(select avatar from user_ where c.creator_id = user_.id) as creator_avatar, +coalesce(sum(cl.score), 0) as score, +count (case when cl.score = 1 then 1 else null end) as upvotes, +count (case when cl.score = -1 then 1 else null end) as downvotes +from comment c +left join comment_like cl on c.id = cl.comment_id +group by c.id; + +create materialized view comment_aggregates_mview as select * from comment_aggregates_view; + +create unique index idx_comment_aggregates_mview_id on comment_aggregates_mview (id); + +create view comment_view as +with all_comment as +( + select + ca.* + from comment_aggregates_view ca +) + +select +ac.*, +u.id as user_id, +coalesce(cl.score, 0) as my_vote, +(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved +from user_ u +cross join all_comment ac +left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id + +union all + +select + ac.*, + null as user_id, + null as my_vote, + null as saved +from all_comment ac +; + +create view comment_mview as +with all_comment as +( + select + ca.* + from comment_aggregates_mview ca +) + +select +ac.*, +u.id as user_id, +coalesce(cl.score, 0) as my_vote, +(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved +from user_ u +cross join all_comment ac +left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id + +union all + +select + ac.*, + null as user_id, + null as my_vote, + null as saved +from all_comment ac +; + + +-- Do the reply_view referencing the comment_mview +create view reply_view as +with closereply as ( + select + c2.id, + c2.creator_id as sender_id, + c.creator_id as recipient_id + from comment c + inner join comment c2 on c.id = c2.parent_id + where c2.creator_id != c.creator_id + -- Do union where post is null + union + select + c.id, + c.creator_id as sender_id, + p.creator_id as recipient_id + from comment c, post p + where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id +) +select cv.*, +closereply.recipient_id +from comment_mview cv, closereply +where closereply.id = cv.id +; + +-- user mention +create view user_mention_view as +select + c.id, + um.id as user_mention_id, + c.creator_id, + c.post_id, + c.parent_id, + c.content, + c.removed, + um.read, + c.published, + c.updated, + c.deleted, + c.community_id, + c.banned, + c.banned_from_community, + c.creator_name, + c.creator_avatar, + c.score, + c.upvotes, + c.downvotes, + c.user_id, + c.my_vote, + c.saved, + um.recipient_id +from user_mention um, comment_view c +where um.comment_id = c.id; + + +create view user_mention_mview as +with all_comment as +( + select + ca.* + from comment_aggregates_mview ca +) + +select + ac.id, + um.id as user_mention_id, + ac.creator_id, + ac.post_id, + ac.parent_id, + ac.content, + ac.removed, + um.read, + ac.published, + ac.updated, + ac.deleted, + ac.community_id, + ac.banned, + ac.banned_from_community, + ac.creator_name, + ac.creator_avatar, + ac.score, + ac.upvotes, + ac.downvotes, + u.id as user_id, + coalesce(cl.score, 0) as my_vote, + (select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved, + um.recipient_id +from user_ u +cross join all_comment ac +left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id +left join user_mention um on um.comment_id = ac.id + +union all + +select + ac.id, + um.id as user_mention_id, + ac.creator_id, + ac.post_id, + ac.parent_id, + ac.content, + ac.removed, + um.read, + ac.published, + ac.updated, + ac.deleted, + ac.community_id, + ac.banned, + ac.banned_from_community, + ac.creator_name, + ac.creator_avatar, + ac.score, + ac.upvotes, + ac.downvotes, + null as user_id, + null as my_vote, + null as saved, + um.recipient_id +from all_comment ac +left join user_mention um on um.comment_id = ac.id +; + diff --git a/server/migrations/2020-02-07-210055_add_comment_subscribed/up.sql b/server/migrations/2020-02-07-210055_add_comment_subscribed/up.sql new file mode 100644 index 000000000..8836a571a --- /dev/null +++ b/server/migrations/2020-02-07-210055_add_comment_subscribed/up.sql @@ -0,0 +1,220 @@ + +-- Adding community name, hot_rank, to comment_view, user_mention_view, and subscribed to comment_view + +-- Rebuild the comment view +drop view reply_view; +drop view user_mention_view; +drop view user_mention_mview; +drop view comment_view; +drop view comment_mview; +drop materialized view comment_aggregates_mview; +drop view comment_aggregates_view; + +-- reply and comment view +create view comment_aggregates_view as +select +c.*, +(select community_id from post p where p.id = c.post_id), +(select co.name from post p, community co where p.id = c.post_id and p.community_id = co.id) as community_name, +(select u.banned from user_ u where c.creator_id = u.id) as banned, +(select cb.id::bool from community_user_ban cb, post p where c.creator_id = cb.user_id and p.id = c.post_id and p.community_id = cb.community_id) as banned_from_community, +(select name from user_ where c.creator_id = user_.id) as creator_name, +(select avatar from user_ where c.creator_id = user_.id) as creator_avatar, +coalesce(sum(cl.score), 0) as score, +count (case when cl.score = 1 then 1 else null end) as upvotes, +count (case when cl.score = -1 then 1 else null end) as downvotes, +hot_rank(coalesce(sum(cl.score) , 0), c.published) as hot_rank +from comment c +left join comment_like cl on c.id = cl.comment_id +group by c.id; + +create materialized view comment_aggregates_mview as select * from comment_aggregates_view; + +create unique index idx_comment_aggregates_mview_id on comment_aggregates_mview (id); + +create view comment_view as +with all_comment as +( + select + ca.* + from comment_aggregates_view ca +) + +select +ac.*, +u.id as user_id, +coalesce(cl.score, 0) as my_vote, +(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.community_id = cf.community_id) as subscribed, +(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved +from user_ u +cross join all_comment ac +left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id + +union all + +select + ac.*, + null as user_id, + null as my_vote, + null as subscribed, + null as saved +from all_comment ac +; + +create view comment_mview as +with all_comment as +( + select + ca.* + from comment_aggregates_mview ca +) + +select +ac.*, +u.id as user_id, +coalesce(cl.score, 0) as my_vote, +(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.community_id = cf.community_id) as subscribed, +(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved +from user_ u +cross join all_comment ac +left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id + +union all + +select + ac.*, + null as user_id, + null as my_vote, + null as subscribed, + null as saved +from all_comment ac +; + +-- Do the reply_view referencing the comment_mview +create view reply_view as +with closereply as ( + select + c2.id, + c2.creator_id as sender_id, + c.creator_id as recipient_id + from comment c + inner join comment c2 on c.id = c2.parent_id + where c2.creator_id != c.creator_id + -- Do union where post is null + union + select + c.id, + c.creator_id as sender_id, + p.creator_id as recipient_id + from comment c, post p + where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id +) +select cv.*, +closereply.recipient_id +from comment_mview cv, closereply +where closereply.id = cv.id +; + +-- user mention +create view user_mention_view as +select + c.id, + um.id as user_mention_id, + c.creator_id, + c.post_id, + c.parent_id, + c.content, + c.removed, + um.read, + c.published, + c.updated, + c.deleted, + c.community_id, + c.community_name, + c.banned, + c.banned_from_community, + c.creator_name, + c.creator_avatar, + c.score, + c.upvotes, + c.downvotes, + c.hot_rank, + c.user_id, + c.my_vote, + c.saved, + um.recipient_id +from user_mention um, comment_view c +where um.comment_id = c.id; + + +create view user_mention_mview as +with all_comment as +( + select + ca.* + from comment_aggregates_mview ca +) + +select + ac.id, + um.id as user_mention_id, + ac.creator_id, + ac.post_id, + ac.parent_id, + ac.content, + ac.removed, + um.read, + ac.published, + ac.updated, + ac.deleted, + ac.community_id, + ac.community_name, + ac.banned, + ac.banned_from_community, + ac.creator_name, + ac.creator_avatar, + ac.score, + ac.upvotes, + ac.downvotes, + ac.hot_rank, + u.id as user_id, + coalesce(cl.score, 0) as my_vote, + (select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved, + um.recipient_id +from user_ u +cross join all_comment ac +left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id +left join user_mention um on um.comment_id = ac.id + +union all + +select + ac.id, + um.id as user_mention_id, + ac.creator_id, + ac.post_id, + ac.parent_id, + ac.content, + ac.removed, + um.read, + ac.published, + ac.updated, + ac.deleted, + ac.community_id, + ac.community_name, + ac.banned, + ac.banned_from_community, + ac.creator_name, + ac.creator_avatar, + ac.score, + ac.upvotes, + ac.downvotes, + ac.hot_rank, + null as user_id, + null as my_vote, + null as saved, + um.recipient_id +from all_comment ac +left join user_mention um on um.comment_id = ac.id +; + diff --git a/server/src/api/comment.rs b/server/src/api/comment.rs index 775085e93..5c6149666 100644 --- a/server/src/api/comment.rs +++ b/server/src/api/comment.rs @@ -2,6 +2,7 @@ use super::*; use crate::send_email; use crate::settings::Settings; use diesel::PgConnection; +use std::str::FromStr; #[derive(Serialize, Deserialize)] pub struct CreateComment { @@ -47,6 +48,21 @@ pub struct CreateCommentLike { auth: String, } +#[derive(Serialize, Deserialize)] +pub struct GetComments { + type_: String, + sort: String, + page: Option, + limit: Option, + pub community_id: Option, + auth: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct GetCommentsResponse { + comments: Vec, +} + impl Perform for Oper { fn perform(&self, conn: &PgConnection) -> Result { let data: &CreateComment = &self.data; @@ -456,3 +472,40 @@ impl Perform for Oper { }) } } + +impl Perform for Oper { + fn perform(&self, conn: &PgConnection) -> Result { + let data: &GetComments = &self.data; + + let user_claims: Option = match &data.auth { + Some(auth) => match Claims::decode(&auth) { + Ok(claims) => Some(claims.claims), + Err(_e) => None, + }, + None => None, + }; + + let user_id = match &user_claims { + Some(claims) => Some(claims.id), + None => None, + }; + + let type_ = ListingType::from_str(&data.type_)?; + let sort = SortType::from_str(&data.sort)?; + + let comments = match CommentQueryBuilder::create(&conn) + .listing_type(type_) + .sort(&sort) + .for_community_id(data.community_id) + .my_user_id(user_id) + .page(data.page) + .limit(data.limit) + .list() + { + Ok(comments) => comments, + Err(_e) => return Err(APIError::err("couldnt_get_comments").into()), + }; + + Ok(GetCommentsResponse { comments }) + } +} diff --git a/server/src/db/comment_view.rs b/server/src/db/comment_view.rs index febf18b78..01e1bbaf5 100644 --- a/server/src/db/comment_view.rs +++ b/server/src/db/comment_view.rs @@ -15,6 +15,7 @@ table! { updated -> Nullable, deleted -> Bool, community_id -> Int4, + community_name -> Varchar, banned -> Bool, banned_from_community -> Bool, creator_name -> Varchar, @@ -22,8 +23,10 @@ table! { score -> BigInt, upvotes -> BigInt, downvotes -> BigInt, + hot_rank -> Int4, user_id -> Nullable, my_vote -> Nullable, + subscribed -> Nullable, saved -> Nullable, } } @@ -41,6 +44,7 @@ table! { updated -> Nullable, deleted -> Bool, community_id -> Int4, + community_name -> Varchar, banned -> Bool, banned_from_community -> Bool, creator_name -> Varchar, @@ -48,8 +52,10 @@ table! { score -> BigInt, upvotes -> BigInt, downvotes -> BigInt, + hot_rank -> Int4, user_id -> Nullable, my_vote -> Nullable, + subscribed -> Nullable, saved -> Nullable, } } @@ -70,6 +76,7 @@ pub struct CommentView { pub updated: Option, pub deleted: bool, pub community_id: i32, + pub community_name: String, pub banned: bool, pub banned_from_community: bool, pub creator_name: String, @@ -77,15 +84,19 @@ pub struct CommentView { pub score: i64, pub upvotes: i64, pub downvotes: i64, + pub hot_rank: i32, pub user_id: Option, pub my_vote: Option, + pub subscribed: Option, pub saved: Option, } pub struct CommentQueryBuilder<'a> { conn: &'a PgConnection, query: super::comment_view::comment_mview::BoxedQuery<'a, Pg>, + listing_type: ListingType, sort: &'a SortType, + for_community_id: Option, for_post_id: Option, for_creator_id: Option, search_term: Option, @@ -104,7 +115,9 @@ impl<'a> CommentQueryBuilder<'a> { CommentQueryBuilder { conn, query, + listing_type: ListingType::All, sort: &SortType::New, + for_community_id: None, for_post_id: None, for_creator_id: None, search_term: None, @@ -115,6 +128,11 @@ impl<'a> CommentQueryBuilder<'a> { } } + pub fn listing_type(mut self, listing_type: ListingType) -> Self { + self.listing_type = listing_type; + self + } + pub fn sort(mut self, sort: &'a SortType) -> Self { self.sort = sort; self @@ -130,6 +148,11 @@ impl<'a> CommentQueryBuilder<'a> { self } + pub fn for_community_id>(mut self, for_community_id: T) -> Self { + self.for_community_id = for_community_id.get_optional(); + self + } + pub fn search_term>(mut self, search_term: T) -> Self { self.search_term = search_term.get_optional(); self @@ -171,6 +194,10 @@ impl<'a> CommentQueryBuilder<'a> { query = query.filter(creator_id.eq(for_creator_id)); }; + if let Some(for_community_id) = self.for_community_id { + query = query.filter(community_id.eq(for_community_id)); + } + if let Some(for_post_id) = self.for_post_id { query = query.filter(post_id.eq(for_post_id)); }; @@ -179,12 +206,18 @@ impl<'a> CommentQueryBuilder<'a> { query = query.filter(content.ilike(fuzzy_search(&search_term))); }; + if let ListingType::Subscribed = self.listing_type { + query = query.filter(subscribed.eq(true)); + } + if self.saved_only { query = query.filter(saved.eq(true)); } query = match self.sort { - // SortType::Hot => query.order(hot_rank.desc(), published.desc()), + SortType::Hot => query + .order_by(hot_rank.desc()) + .then_order_by(published.desc()), SortType::New => query.order_by(published.desc()), SortType::TopAll => query.order_by(score.desc()), SortType::TopYear => query @@ -199,7 +232,7 @@ impl<'a> CommentQueryBuilder<'a> { SortType::TopDay => query .filter(published.gt(now - 1.days())) .order_by(score.desc()), - _ => query.order_by(published.desc()), + // _ => query.order_by(published.desc()), }; let (limit, offset) = limit_and_offset(self.page, self.limit); @@ -251,6 +284,7 @@ table! { updated -> Nullable, deleted -> Bool, community_id -> Int4, + community_name -> Varchar, banned -> Bool, banned_from_community -> Bool, creator_name -> Varchar, @@ -258,8 +292,10 @@ table! { score -> BigInt, upvotes -> BigInt, downvotes -> BigInt, + hot_rank -> Int4, user_id -> Nullable, my_vote -> Nullable, + subscribed -> Nullable, saved -> Nullable, recipient_id -> Int4, } @@ -281,6 +317,7 @@ pub struct ReplyView { pub updated: Option, pub deleted: bool, pub community_id: i32, + pub community_name: String, pub banned: bool, pub banned_from_community: bool, pub creator_name: String, @@ -288,8 +325,10 @@ pub struct ReplyView { pub score: i64, pub upvotes: i64, pub downvotes: i64, + pub hot_rank: i32, pub user_id: Option, pub my_vote: Option, + pub subscribed: Option, pub saved: Option, pub recipient_id: i32, } @@ -474,6 +513,7 @@ mod tests { creator_id: inserted_user.id, post_id: inserted_post.id, community_id: inserted_community.id, + community_name: inserted_community.name.to_owned(), parent_id: None, removed: false, deleted: false, @@ -486,9 +526,11 @@ mod tests { creator_avatar: None, score: 1, downvotes: 0, + hot_rank: 0, upvotes: 1, user_id: None, my_vote: None, + subscribed: None, saved: None, }; @@ -498,6 +540,7 @@ mod tests { creator_id: inserted_user.id, post_id: inserted_post.id, community_id: inserted_community.id, + community_name: inserted_community.name.to_owned(), parent_id: None, removed: false, deleted: false, @@ -510,21 +553,26 @@ mod tests { creator_avatar: None, score: 1, downvotes: 0, + hot_rank: 0, upvotes: 1, user_id: Some(inserted_user.id), my_vote: Some(1), + subscribed: None, saved: None, }; - let read_comment_views_no_user = CommentQueryBuilder::create(&conn) + let mut read_comment_views_no_user = CommentQueryBuilder::create(&conn) .for_post_id(inserted_post.id) .list() .unwrap(); - let read_comment_views_with_user = CommentQueryBuilder::create(&conn) + read_comment_views_no_user[0].hot_rank = 0; + + let mut read_comment_views_with_user = CommentQueryBuilder::create(&conn) .for_post_id(inserted_post.id) .my_user_id(inserted_user.id) .list() .unwrap(); + read_comment_views_with_user[0].hot_rank = 0; let like_removed = CommentLike::remove(&conn, &comment_like_form).unwrap(); let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap(); diff --git a/server/src/db/post_view.rs b/server/src/db/post_view.rs index 4d09308d8..d6a1d1917 100644 --- a/server/src/db/post_view.rs +++ b/server/src/db/post_view.rs @@ -121,6 +121,9 @@ pub struct PostQueryBuilder<'a> { sort: &'a SortType, my_user_id: Option, for_creator_id: Option, + for_community_id: Option, + search_term: Option, + url_search: Option, show_nsfw: bool, saved_only: bool, unread_only: bool, @@ -137,10 +140,13 @@ impl<'a> PostQueryBuilder<'a> { PostQueryBuilder { conn, query, - my_user_id: None, - for_creator_id: None, listing_type: ListingType::All, sort: &SortType::Hot, + my_user_id: None, + for_creator_id: None, + for_community_id: None, + search_term: None, + url_search: None, show_nsfw: true, saved_only: false, unread_only: false, @@ -160,38 +166,22 @@ impl<'a> PostQueryBuilder<'a> { } pub fn for_community_id>(mut self, for_community_id: T) -> Self { - use super::post_view::post_mview::dsl::*; - if let Some(for_community_id) = for_community_id.get_optional() { - self.query = self.query.filter(community_id.eq(for_community_id)); - self.query = self.query.then_order_by(stickied.desc()); - } + self.for_community_id = for_community_id.get_optional(); self } pub fn for_creator_id>(mut self, for_creator_id: T) -> Self { - if let Some(for_creator_id) = for_creator_id.get_optional() { - self.for_creator_id = Some(for_creator_id); - } + self.for_creator_id = for_creator_id.get_optional(); self } pub fn search_term>(mut self, search_term: T) -> Self { - use super::post_view::post_mview::dsl::*; - if let Some(search_term) = search_term.get_optional() { - let searcher = fuzzy_search(&search_term); - self.query = self - .query - .filter(name.ilike(searcher.to_owned())) - .or_filter(body.ilike(searcher)); - } + self.search_term = search_term.get_optional(); self } pub fn url_search>(mut self, url_search: T) -> Self { - use super::post_view::post_mview::dsl::*; - if let Some(url_search) = url_search.get_optional() { - self.query = self.query.filter(url.eq(url_search)); - } + self.url_search = url_search.get_optional(); self } @@ -234,6 +224,22 @@ impl<'a> PostQueryBuilder<'a> { query = query.filter(subscribed.eq(true)); } + if let Some(for_community_id) = self.for_community_id { + query = query.filter(community_id.eq(for_community_id)); + query = query.then_order_by(stickied.desc()); + } + + if let Some(url_search) = self.url_search { + query = query.filter(url.eq(url_search)); + } + + if let Some(search_term) = self.search_term { + let searcher = fuzzy_search(&search_term); + query = query + .filter(name.ilike(searcher.to_owned())) + .or_filter(body.ilike(searcher)); + } + query = match self.sort { SortType::Hot => query .then_order_by(hot_rank.desc()) diff --git a/server/src/db/user_mention_view.rs b/server/src/db/user_mention_view.rs index 1cf43984a..8046747e6 100644 --- a/server/src/db/user_mention_view.rs +++ b/server/src/db/user_mention_view.rs @@ -16,6 +16,7 @@ table! { updated -> Nullable, deleted -> Bool, community_id -> Int4, + community_name -> Varchar, banned -> Bool, banned_from_community -> Bool, creator_name -> Varchar, @@ -23,6 +24,7 @@ table! { score -> BigInt, upvotes -> BigInt, downvotes -> BigInt, + hot_rank -> Int4, user_id -> Nullable, my_vote -> Nullable, saved -> Nullable, @@ -44,6 +46,7 @@ table! { updated -> Nullable, deleted -> Bool, community_id -> Int4, + community_name -> Varchar, banned -> Bool, banned_from_community -> Bool, creator_name -> Varchar, @@ -51,6 +54,7 @@ table! { score -> BigInt, upvotes -> BigInt, downvotes -> BigInt, + hot_rank -> Int4, user_id -> Nullable, my_vote -> Nullable, saved -> Nullable, @@ -75,6 +79,7 @@ pub struct UserMentionView { pub updated: Option, pub deleted: bool, pub community_id: i32, + pub community_name: String, pub banned: bool, pub banned_from_community: bool, pub creator_name: String, @@ -82,6 +87,7 @@ pub struct UserMentionView { pub score: i64, pub upvotes: i64, pub downvotes: i64, + pub hot_rank: i32, pub user_id: Option, pub my_vote: Option, pub saved: Option, @@ -149,7 +155,9 @@ impl<'a> UserMentionQueryBuilder<'a> { .filter(recipient_id.eq(self.for_user_id)); query = match self.sort { - // SortType::Hot => query.order_by(hot_rank.desc()), + SortType::Hot => query + .order_by(hot_rank.desc()) + .then_order_by(published.desc()), SortType::New => query.order_by(published.desc()), SortType::TopAll => query.order_by(score.desc()), SortType::TopYear => query @@ -164,7 +172,7 @@ impl<'a> UserMentionQueryBuilder<'a> { SortType::TopDay => query .filter(published.gt(now - 1.days())) .order_by(score.desc()), - _ => query.order_by(published.desc()), + // _ => query.order_by(published.desc()), }; let (limit, offset) = limit_and_offset(self.page, self.limit); diff --git a/server/src/routes/index.rs b/server/src/routes/index.rs index b044833ef..c1c363c98 100644 --- a/server/src/routes/index.rs +++ b/server/src/routes/index.rs @@ -6,7 +6,7 @@ pub fn config(cfg: &mut web::ServiceConfig) { cfg .route("/", web::get().to(index)) .route( - "/home/type/{type}/sort/{sort}/page/{page}", + "/home/data_type/{data_type}/listing_type/{listing_type}/sort/{sort}/page/{page}", web::get().to(index), ) .route("/login", web::get().to(index)) @@ -17,7 +17,10 @@ pub fn config(cfg: &mut web::ServiceConfig) { .route("/communities", web::get().to(index)) .route("/post/{id}/comment/{id2}", web::get().to(index)) .route("/post/{id}", web::get().to(index)) - .route("/c/{name}/sort/{sort}/page/{page}", web::get().to(index)) + .route( + "/c/{name}/data_type/{data_type}/sort/{sort}/page/{page}", + web::get().to(index), + ) .route("/c/{name}", web::get().to(index)) .route("/community/{id}", web::get().to(index)) .route( diff --git a/server/src/websocket/mod.rs b/server/src/websocket/mod.rs index c9a41a1fc..a1feede25 100644 --- a/server/src/websocket/mod.rs +++ b/server/src/websocket/mod.rs @@ -45,4 +45,5 @@ pub enum UserOperation { EditPrivateMessage, GetPrivateMessages, UserJoin, + GetComments, } diff --git a/server/src/websocket/server.rs b/server/src/websocket/server.rs index bf3249e1d..003b886e1 100644 --- a/server/src/websocket/server.rs +++ b/server/src/websocket/server.rs @@ -121,6 +121,12 @@ impl ChatServer { sessions.remove(&id); } + // Also leave all post rooms + // This avoids double messages + for sessions in self.post_rooms.values_mut() { + sessions.remove(&id); + } + // If the room doesn't exist yet if self.community_rooms.get_mut(&community_id).is_none() { self.community_rooms.insert(community_id, HashSet::new()); @@ -139,6 +145,12 @@ impl ChatServer { sessions.remove(&id); } + // Also leave all communities + // This avoids double messages + for sessions in self.community_rooms.values_mut() { + sessions.remove(&id); + } + // If the room doesn't exist yet if self.post_rooms.get_mut(&post_id).is_none() { self.post_rooms.insert(post_id, HashSet::new()); @@ -243,6 +255,10 @@ impl ChatServer { self.send_user_room_message(recipient_id, &comment_reply_sent_str, id); } + // Send it to the community too + self.send_community_room_message(0, &comment_post_sent_str, id); + self.send_community_room_message(comment.comment.community_id, &comment_post_sent_str, id); + Ok(comment_user_sent_str) } @@ -613,6 +629,15 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result { + let get_comments: GetComments = serde_json::from_str(data)?; + if get_comments.community_id.is_none() { + // 0 is the "all" community + chat.join_community_room(0, msg.id); + } + let res = Oper::new(get_comments).perform(&conn)?; + to_json_string(&user_operation, &res) + } UserOperation::CreatePost => { chat.check_rate_limit_post(msg.id, true)?; let create_post: CreatePost = serde_json::from_str(data)?; diff --git a/ui/src/components/comment-node.tsx b/ui/src/components/comment-node.tsx index 3296a5c8e..a67b1c351 100644 --- a/ui/src/components/comment-node.tsx +++ b/ui/src/components/comment-node.tsx @@ -58,7 +58,9 @@ interface CommentNodeProps { markable?: boolean; moderators: Array; admins: Array; + // TODO is this necessary, can't I get it from the node itself? postCreatorId?: number; + showCommunity?: boolean; } export class CommentNode extends Component { @@ -205,6 +207,14 @@ export class CommentNode extends Component { ) + {this.props.showCommunity && ( +
  • + {i18n.t('to')} + + {node.comment.community_name} + +
  • + )}
  • diff --git a/ui/src/components/comment-nodes.tsx b/ui/src/components/comment-nodes.tsx index 18faf1ac4..fb700cc40 100644 --- a/ui/src/components/comment-nodes.tsx +++ b/ui/src/components/comment-nodes.tsx @@ -17,6 +17,7 @@ interface CommentNodesProps { viewOnly?: boolean; locked?: boolean; markable?: boolean; + showCommunity?: boolean; } export class CommentNodes extends Component< @@ -40,6 +41,7 @@ export class CommentNodes extends Component< admins={this.props.admins} postCreatorId={this.props.postCreatorId} markable={this.props.markable} + showCommunity={this.props.showCommunity} /> ))} diff --git a/ui/src/components/community.tsx b/ui/src/components/community.tsx index 069f9158d..3e04a8bfb 100644 --- a/ui/src/components/community.tsx +++ b/ui/src/components/community.tsx @@ -13,17 +13,31 @@ import { GetPostsForm, GetCommunityForm, ListingType, + DataType, GetPostsResponse, PostResponse, AddModToCommunityResponse, BanFromCommunityResponse, + Comment, + GetCommentsForm, + GetCommentsResponse, + CommentResponse, WebSocketJsonResponse, } from '../interfaces'; -import { WebSocketService, UserService } from '../services'; +import { WebSocketService } from '../services'; import { PostListings } from './post-listings'; +import { CommentNodes } from './comment-nodes'; import { SortSelect } from './sort-select'; +import { DataTypeSelect } from './data-type-select'; import { Sidebar } from './sidebar'; -import { wsJsonToRes, routeSortTypeToEnum, fetchLimit, toast } from '../utils'; +import { + wsJsonToRes, + fetchLimit, + toast, + getPageFromProps, + getSortTypeFromProps, + getDataTypeFromProps, +} from '../utils'; import { i18n } from '../i18next'; interface State { @@ -35,6 +49,8 @@ interface State { online: number; loading: boolean; posts: Array; + comments: Array; + dataType: DataType; sort: SortType; page: number; } @@ -65,27 +81,18 @@ export class Community extends Component { online: null, loading: true, posts: [], - sort: this.getSortTypeFromProps(this.props), - page: this.getPageFromProps(this.props), + comments: [], + dataType: getDataTypeFromProps(this.props), + sort: getSortTypeFromProps(this.props), + page: getPageFromProps(this.props), }; - getSortTypeFromProps(props: any): SortType { - return props.match.params.sort - ? routeSortTypeToEnum(props.match.params.sort) - : UserService.Instance.user - ? UserService.Instance.user.default_sort_type - : SortType.Hot; - } - - getPageFromProps(props: any): number { - return props.match.params.page ? Number(props.match.params.page) : 1; - } - constructor(props: any, context: any) { super(props, context); this.state = this.emptyState; this.handleSortChange = this.handleSortChange.bind(this); + this.handleDataTypeChange = this.handleDataTypeChange.bind(this); this.subscription = WebSocketService.Instance.subject .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) @@ -112,10 +119,11 @@ export class Community extends Component { nextProps.history.action == 'POP' || nextProps.history.action == 'PUSH' ) { - this.state.sort = this.getSortTypeFromProps(nextProps); - this.state.page = this.getPageFromProps(nextProps); + this.state.dataType = getDataTypeFromProps(nextProps); + this.state.sort = getSortTypeFromProps(nextProps); + this.state.page = getPageFromProps(nextProps); this.setState(this.state); - this.fetchPosts(); + this.fetchData(); } } @@ -145,7 +153,7 @@ export class Community extends Component { )} {this.selects()} - + {this.listings()} {this.paginator()}
    @@ -162,10 +170,31 @@ export class Community extends Component { ); } + listings() { + return this.state.dataType == DataType.Post ? ( + + ) : ( + this.state.comments.map(comment => ( +
    +
    + +
    +
    + )) + ); + } + selects() { return (
    - + + + + + { i.state.page++; i.setState(i.state); i.updateUrl(); - i.fetchPosts(); + i.fetchData(); window.scrollTo(0, 0); } @@ -215,7 +244,7 @@ export class Community extends Component { i.state.page--; i.setState(i.state); i.updateUrl(); - i.fetchPosts(); + i.fetchData(); window.scrollTo(0, 0); } @@ -225,26 +254,48 @@ export class Community extends Component { this.state.loading = true; this.setState(this.state); this.updateUrl(); - this.fetchPosts(); + this.fetchData(); + window.scrollTo(0, 0); + } + + handleDataTypeChange(val: DataType) { + this.state.dataType = val; + this.state.page = 1; + this.state.loading = true; + this.setState(this.state); + this.updateUrl(); + this.fetchData(); window.scrollTo(0, 0); } updateUrl() { + let dataTypeStr = DataType[this.state.dataType].toLowerCase(); let sortStr = SortType[this.state.sort].toLowerCase(); this.props.history.push( - `/c/${this.state.community.name}/sort/${sortStr}/page/${this.state.page}` + `/c/${this.state.community.name}/data_type/${dataTypeStr}/sort/${sortStr}/page/${this.state.page}` ); } - fetchPosts() { - let getPostsForm: GetPostsForm = { - page: this.state.page, - limit: fetchLimit, - sort: SortType[this.state.sort], - type_: ListingType[ListingType.Community], - community_id: this.state.community.id, - }; - WebSocketService.Instance.getPosts(getPostsForm); + fetchData() { + if (this.state.dataType == DataType.Post) { + let getPostsForm: GetPostsForm = { + page: this.state.page, + limit: fetchLimit, + sort: SortType[this.state.sort], + type_: ListingType[ListingType.Community], + community_id: this.state.community.id, + }; + WebSocketService.Instance.getPosts(getPostsForm); + } else { + let getCommentsForm: GetCommentsForm = { + page: this.state.page, + limit: fetchLimit, + sort: SortType[this.state.sort], + type_: ListingType[ListingType.Community], + community_id: this.state.community.id, + }; + WebSocketService.Instance.getComments(getCommentsForm); + } } parseMessage(msg: WebSocketJsonResponse) { @@ -255,7 +306,7 @@ export class Community extends Component { this.context.router.history.push('/'); return; } else if (msg.reconnect) { - this.fetchPosts(); + this.fetchData(); } else if (res.op == UserOperation.GetCommunity) { let data = res.data as GetCommunityResponse; this.state.community = data.community; @@ -264,7 +315,7 @@ export class Community extends Component { this.state.online = data.online; document.title = `/c/${this.state.community.name} - ${WebSocketService.Instance.site.name}`; this.setState(this.state); - this.fetchPosts(); + this.fetchData(); } else if (res.op == UserOperation.EditCommunity) { let data = res.data as CommunityResponse; this.state.community = data.community; @@ -319,6 +370,48 @@ export class Community extends Component { .forEach(p => (p.banned = data.banned)); this.setState(this.state); + } else if (res.op == UserOperation.GetComments) { + let data = res.data as GetCommentsResponse; + this.state.comments = data.comments; + this.state.loading = false; + this.setState(this.state); + } else if (res.op == UserOperation.EditComment) { + let data = res.data as CommentResponse; + + let found = this.state.comments.find(c => c.id == data.comment.id); + if (found) { + found.content = data.comment.content; + found.updated = data.comment.updated; + found.removed = data.comment.removed; + found.deleted = data.comment.deleted; + found.upvotes = data.comment.upvotes; + found.downvotes = data.comment.downvotes; + found.score = data.comment.score; + this.setState(this.state); + } + } else if (res.op == UserOperation.CreateComment) { + let data = res.data as CommentResponse; + + // Necessary since it might be a user reply + if (data.recipient_ids.length == 0) { + this.state.comments.unshift(data.comment); + this.setState(this.state); + } + } else if (res.op == UserOperation.SaveComment) { + let data = res.data as CommentResponse; + let found = this.state.comments.find(c => c.id == data.comment.id); + found.saved = data.comment.saved; + this.setState(this.state); + } else if (res.op == UserOperation.CreateCommentLike) { + let data = res.data as CommentResponse; + let found: Comment = this.state.comments.find( + c => c.id === data.comment.id + ); + found.score = data.comment.score; + found.upvotes = data.comment.upvotes; + found.downvotes = data.comment.downvotes; + if (data.comment.my_vote !== null) found.my_vote = data.comment.my_vote; + this.setState(this.state); } } } diff --git a/ui/src/components/data-type-select.tsx b/ui/src/components/data-type-select.tsx new file mode 100644 index 000000000..f2539c810 --- /dev/null +++ b/ui/src/components/data-type-select.tsx @@ -0,0 +1,65 @@ +import { Component, linkEvent } from 'inferno'; +import { DataType } from '../interfaces'; + +import { i18n } from '../i18next'; + +interface DataTypeSelectProps { + type_: DataType; + onChange?(val: DataType): any; +} + +interface DataTypeSelectState { + type_: DataType; +} + +export class DataTypeSelect extends Component< + DataTypeSelectProps, + DataTypeSelectState +> { + private emptyState: DataTypeSelectState = { + type_: this.props.type_, + }; + + constructor(props: any, context: any) { + super(props, context); + this.state = this.emptyState; + } + + render() { + return ( +
    + + +
    + ); + } + + handleTypeChange(i: DataTypeSelect, event: any) { + i.state.type_ = Number(event.target.value); + i.setState(i.state); + i.props.onChange(i.state.type_); + } +} diff --git a/ui/src/components/main.tsx b/ui/src/components/main.tsx index 161f5df45..92434360b 100644 --- a/ui/src/components/main.tsx +++ b/ui/src/components/main.tsx @@ -12,30 +12,39 @@ import { SortType, GetSiteResponse, ListingType, + DataType, SiteResponse, GetPostsResponse, PostResponse, Post, GetPostsForm, + Comment, + GetCommentsForm, + GetCommentsResponse, + CommentResponse, AddAdminResponse, BanUserResponse, WebSocketJsonResponse, } from '../interfaces'; import { WebSocketService, UserService } from '../services'; import { PostListings } from './post-listings'; +import { CommentNodes } from './comment-nodes'; import { SortSelect } from './sort-select'; import { ListingTypeSelect } from './listing-type-select'; +import { DataTypeSelect } from './data-type-select'; import { SiteForm } from './site-form'; import { wsJsonToRes, repoUrl, mdToHtml, fetchLimit, - routeSortTypeToEnum, - routeListingTypeToEnum, pictshareAvatarThumbnail, showAvatars, toast, + getListingTypeFromProps, + getPageFromProps, + getSortTypeFromProps, + getDataTypeFromProps, } from '../utils'; import { i18n } from '../i18next'; import { T } from 'inferno-i18next'; @@ -47,7 +56,9 @@ interface MainState { showEditSite: boolean; loading: boolean; posts: Array; - type_: ListingType; + comments: Array; + listingType: ListingType; + dataType: DataType; sort: SortType; page: number; } @@ -79,38 +90,21 @@ export class Main extends Component { showEditSite: false, loading: true, posts: [], - type_: this.getListingTypeFromProps(this.props), - sort: this.getSortTypeFromProps(this.props), - page: this.getPageFromProps(this.props), + comments: [], + listingType: getListingTypeFromProps(this.props), + dataType: getDataTypeFromProps(this.props), + sort: getSortTypeFromProps(this.props), + page: getPageFromProps(this.props), }; - getListingTypeFromProps(props: any): ListingType { - return props.match.params.type - ? routeListingTypeToEnum(props.match.params.type) - : UserService.Instance.user - ? UserService.Instance.user.default_listing_type - : ListingType.All; - } - - getSortTypeFromProps(props: any): SortType { - return props.match.params.sort - ? routeSortTypeToEnum(props.match.params.sort) - : UserService.Instance.user - ? UserService.Instance.user.default_sort_type - : SortType.Hot; - } - - getPageFromProps(props: any): number { - return props.match.params.page ? Number(props.match.params.page) : 1; - } - constructor(props: any, context: any) { super(props, context); this.state = this.emptyState; this.handleEditCancel = this.handleEditCancel.bind(this); this.handleSortChange = this.handleSortChange.bind(this); - this.handleTypeChange = this.handleTypeChange.bind(this); + this.handleListingTypeChange = this.handleListingTypeChange.bind(this); + this.handleDataTypeChange = this.handleDataTypeChange.bind(this); this.subscription = WebSocketService.Instance.subject .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) @@ -133,7 +127,7 @@ export class Main extends Component { WebSocketService.Instance.listCommunities(listCommunitiesForm); - this.fetchPosts(); + this.fetchData(); } componentWillUnmount() { @@ -146,11 +140,12 @@ export class Main extends Component { nextProps.history.action == 'POP' || nextProps.history.action == 'PUSH' ) { - this.state.type_ = this.getListingTypeFromProps(nextProps); - this.state.sort = this.getSortTypeFromProps(nextProps); - this.state.page = this.getPageFromProps(nextProps); + this.state.listingType = getListingTypeFromProps(nextProps); + this.state.dataType = getDataTypeFromProps(nextProps); + this.state.sort = getSortTypeFromProps(nextProps); + this.state.page = getPageFromProps(nextProps); this.setState(this.state); - this.fetchPosts(); + this.fetchData(); } } @@ -251,10 +246,11 @@ export class Main extends Component { } updateUrl() { - let typeStr = ListingType[this.state.type_].toLowerCase(); + let listingTypeStr = ListingType[this.state.listingType].toLowerCase(); + let dataTypeStr = DataType[this.state.dataType].toLowerCase(); let sortStr = SortType[this.state.sort].toLowerCase(); this.props.history.push( - `/home/type/${typeStr}/sort/${sortStr}/page/${this.state.page}` + `/home/data_type/${dataTypeStr}/listing_type/${listingTypeStr}/sort/${sortStr}/page/${this.state.page}` ); } @@ -392,11 +388,7 @@ export class Main extends Component { ) : (
    {this.selects()} - + {this.listings()} {this.paginator()}
    )} @@ -404,17 +396,41 @@ export class Main extends Component { ); } + listings() { + return this.state.dataType == DataType.Post ? ( + + ) : ( + this.state.comments.map(comment => ( +
    +
    + +
    +
    + )) + ); + } + selects() { return (
    - + + + - {this.state.type_ == ListingType.All && ( + {this.state.listingType == ListingType.All && ( { )} {UserService.Instance.user && - this.state.type_ == ListingType.Subscribed && ( + this.state.listingType == ListingType.Subscribed && ( { i.state.loading = true; i.setState(i.state); i.updateUrl(); - i.fetchPosts(); + i.fetchData(); window.scrollTo(0, 0); } @@ -497,7 +513,7 @@ export class Main extends Component { i.state.loading = true; i.setState(i.state); i.updateUrl(); - i.fetchPosts(); + i.fetchData(); window.scrollTo(0, 0); } @@ -507,28 +523,48 @@ export class Main extends Component { this.state.loading = true; this.setState(this.state); this.updateUrl(); - this.fetchPosts(); + this.fetchData(); window.scrollTo(0, 0); } - handleTypeChange(val: ListingType) { - this.state.type_ = val; + handleListingTypeChange(val: ListingType) { + this.state.listingType = val; this.state.page = 1; this.state.loading = true; this.setState(this.state); this.updateUrl(); - this.fetchPosts(); + this.fetchData(); window.scrollTo(0, 0); } - fetchPosts() { - let getPostsForm: GetPostsForm = { - page: this.state.page, - limit: fetchLimit, - sort: SortType[this.state.sort], - type_: ListingType[this.state.type_], - }; - WebSocketService.Instance.getPosts(getPostsForm); + handleDataTypeChange(val: DataType) { + this.state.dataType = val; + this.state.page = 1; + this.state.loading = true; + this.setState(this.state); + this.updateUrl(); + this.fetchData(); + window.scrollTo(0, 0); + } + + fetchData() { + if (this.state.dataType == DataType.Post) { + let getPostsForm: GetPostsForm = { + page: this.state.page, + limit: fetchLimit, + sort: SortType[this.state.sort], + type_: ListingType[this.state.listingType], + }; + WebSocketService.Instance.getPosts(getPostsForm); + } else { + let getCommentsForm: GetCommentsForm = { + page: this.state.page, + limit: fetchLimit, + sort: SortType[this.state.sort], + type_: ListingType[this.state.listingType], + }; + WebSocketService.Instance.getComments(getCommentsForm); + } } parseMessage(msg: WebSocketJsonResponse) { @@ -538,7 +574,7 @@ export class Main extends Component { toast(i18n.t(msg.error), 'danger'); return; } else if (msg.reconnect) { - this.fetchPosts(); + this.fetchData(); } else if (res.op == UserOperation.GetFollowedCommunities) { let data = res.data as GetFollowedCommunitiesResponse; this.state.subscribedCommunities = data.communities; @@ -574,7 +610,7 @@ export class Main extends Component { let data = res.data as PostResponse; // If you're on subscribed, only push it if you're subscribed. - if (this.state.type_ == ListingType.Subscribed) { + if (this.state.listingType == ListingType.Subscribed) { if ( this.state.subscribedCommunities .map(c => c.community_id) @@ -633,6 +669,59 @@ export class Main extends Component { .forEach(p => (p.banned = data.banned)); this.setState(this.state); + } else if (res.op == UserOperation.GetComments) { + let data = res.data as GetCommentsResponse; + this.state.comments = data.comments; + this.state.loading = false; + this.setState(this.state); + } else if (res.op == UserOperation.EditComment) { + let data = res.data as CommentResponse; + + let found = this.state.comments.find(c => c.id == data.comment.id); + if (found) { + found.content = data.comment.content; + found.updated = data.comment.updated; + found.removed = data.comment.removed; + found.deleted = data.comment.deleted; + found.upvotes = data.comment.upvotes; + found.downvotes = data.comment.downvotes; + found.score = data.comment.score; + this.setState(this.state); + } + } else if (res.op == UserOperation.CreateComment) { + let data = res.data as CommentResponse; + + // Necessary since it might be a user reply + if (data.recipient_ids.length == 0) { + // If you're on subscribed, only push it if you're subscribed. + if (this.state.listingType == ListingType.Subscribed) { + if ( + this.state.subscribedCommunities + .map(c => c.community_id) + .includes(data.comment.community_id) + ) { + this.state.comments.unshift(data.comment); + } + } else { + this.state.comments.unshift(data.comment); + } + this.setState(this.state); + } + } else if (res.op == UserOperation.SaveComment) { + let data = res.data as CommentResponse; + let found = this.state.comments.find(c => c.id == data.comment.id); + found.saved = data.comment.saved; + this.setState(this.state); + } else if (res.op == UserOperation.CreateCommentLike) { + let data = res.data as CommentResponse; + let found: Comment = this.state.comments.find( + c => c.id === data.comment.id + ); + found.score = data.comment.score; + found.upvotes = data.comment.upvotes; + found.downvotes = data.comment.downvotes; + if (data.comment.my_vote !== null) found.my_vote = data.comment.my_vote; + this.setState(this.state); } } } diff --git a/ui/src/components/user.tsx b/ui/src/components/user.tsx index da6aa8cee..effc9e1dc 100644 --- a/ui/src/components/user.tsx +++ b/ui/src/components/user.tsx @@ -1034,20 +1034,24 @@ export class User extends Component { let data = res.data as CommentResponse; let found = this.state.comments.find(c => c.id == data.comment.id); - found.content = data.comment.content; - found.updated = data.comment.updated; - found.removed = data.comment.removed; - found.deleted = data.comment.deleted; - found.upvotes = data.comment.upvotes; - found.downvotes = data.comment.downvotes; - found.score = data.comment.score; - - this.setState(this.state); + if (found) { + found.content = data.comment.content; + found.updated = data.comment.updated; + found.removed = data.comment.removed; + found.deleted = data.comment.deleted; + found.upvotes = data.comment.upvotes; + found.downvotes = data.comment.downvotes; + found.score = data.comment.score; + this.setState(this.state); + } } else if (res.op == UserOperation.CreateComment) { - // let res: CommentResponse = msg; - toast(i18n.t('reply_sent')); - // this.state.comments.unshift(res.comment); // TODO do this right - // this.setState(this.state); + let data = res.data as CommentResponse; + if ( + UserService.Instance.user && + data.comment.creator_id == UserService.Instance.user.id + ) { + toast(i18n.t('reply_sent')); + } } else if (res.op == UserOperation.SaveComment) { let data = res.data as CommentResponse; let found = this.state.comments.find(c => c.id == data.comment.id); diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 8a9aa3c38..c56f6c4ea 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -41,7 +41,7 @@ class Index extends Component { @@ -56,7 +56,7 @@ class Index extends Component { diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts index 02c108aa5..5736d375a 100644 --- a/ui/src/interfaces.ts +++ b/ui/src/interfaces.ts @@ -42,6 +42,7 @@ export enum UserOperation { EditPrivateMessage, GetPrivateMessages, UserJoin, + GetComments, } export enum CommentSortType { @@ -57,6 +58,11 @@ export enum ListingType { Community, } +export enum DataType { + Post, + Comment, +} + export enum SortType { Hot, New, @@ -187,6 +193,7 @@ export interface Comment { published: string; updated?: string; community_id: number; + community_name: string; banned: boolean; banned_from_community: boolean; creator_name: string; @@ -194,8 +201,10 @@ export interface Comment { score: number; upvotes: number; downvotes: number; + hot_rank: number; user_id?: number; my_vote?: number; + subscribed?: number; saved?: boolean; user_mention_id?: number; // For mention type recipient_id?: number; @@ -659,6 +668,19 @@ export interface GetPostsResponse { posts: Array; } +export interface GetCommentsForm { + type_: string; + sort: string; + page?: number; + limit: number; + community_id?: number; + auth?: string; +} + +export interface GetCommentsResponse { + comments: Array; +} + export interface CreatePostLikeForm { post_id: number; score: number; diff --git a/ui/src/services/WebSocketService.ts b/ui/src/services/WebSocketService.ts index 6d951618a..3df69457a 100644 --- a/ui/src/services/WebSocketService.ts +++ b/ui/src/services/WebSocketService.ts @@ -38,6 +38,7 @@ import { PrivateMessageForm, EditPrivateMessageForm, GetPrivateMessagesForm, + GetCommentsForm, UserJoinForm, MessageType, WebSocketJsonResponse, @@ -172,6 +173,11 @@ export class WebSocketService { this.ws.send(this.wsSendWrapper(UserOperation.GetPosts, form)); } + public getComments(form: GetCommentsForm) { + this.setAuth(form, false); + this.ws.send(this.wsSendWrapper(UserOperation.GetComments, form)); + } + public likePost(form: CreatePostLikeForm) { this.setAuth(form); this.ws.send(this.wsSendWrapper(UserOperation.CreatePostLike, form)); diff --git a/ui/src/translations/en.ts b/ui/src/translations/en.ts index f71c203bc..788bce798 100644 --- a/ui/src/translations/en.ts +++ b/ui/src/translations/en.ts @@ -201,6 +201,7 @@ export const en = { 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.', diff --git a/ui/src/utils.ts b/ui/src/utils.ts index 9ad0920f4..51836c6f0 100644 --- a/ui/src/utils.ts +++ b/ui/src/utils.ts @@ -19,6 +19,7 @@ import { User, SortType, ListingType, + DataType, SearchType, WebSocketResponse, WebSocketJsonResponse, @@ -198,6 +199,10 @@ export function routeListingTypeToEnum(type: string): ListingType { return ListingType[capitalizeFirstLetter(type)]; } +export function routeDataTypeToEnum(type: string): DataType { + return DataType[capitalizeFirstLetter(type)]; +} + export function routeSearchTypeToEnum(type: string): SearchType { return SearchType[capitalizeFirstLetter(type)]; } @@ -519,3 +524,30 @@ function communitySearch(text: string, cb: any) { cb([]); } } + +export function getListingTypeFromProps(props: any): ListingType { + return props.match.params.listing_type + ? routeListingTypeToEnum(props.match.params.listing_type) + : UserService.Instance.user + ? UserService.Instance.user.default_listing_type + : ListingType.All; +} + +// TODO might need to add a user setting for this too +export function getDataTypeFromProps(props: any): DataType { + return props.match.params.data_type + ? routeDataTypeToEnum(props.match.params.data_type) + : DataType.Post; +} + +export function getSortTypeFromProps(props: any): SortType { + return props.match.params.sort + ? routeSortTypeToEnum(props.match.params.sort) + : UserService.Instance.user + ? UserService.Instance.user.default_sort_type + : SortType.Hot; +} + +export function getPageFromProps(props: any): number { + return props.match.params.page ? Number(props.match.params.page) : 1; +} From 8c5a510cfc316d92e09ce821104e0b2d66ccb041 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Fri, 7 Feb 2020 23:50:33 -0500 Subject: [PATCH 30/77] Version v0.6.15 --- ansible/VERSION | 2 +- docker/prod/docker-compose.yml | 2 +- server/src/version.rs | 2 +- ui/src/version.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ansible/VERSION b/ansible/VERSION index dae7cf429..f9b38caa3 100644 --- a/ansible/VERSION +++ b/ansible/VERSION @@ -1 +1 @@ -v0.6.14 +v0.6.15 diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml index 7b04dc260..b4973d1f4 100644 --- a/docker/prod/docker-compose.yml +++ b/docker/prod/docker-compose.yml @@ -11,7 +11,7 @@ services: - lemmy_db:/var/lib/postgresql/data restart: always lemmy: - image: dessalines/lemmy:v0.6.14 + image: dessalines/lemmy:v0.6.15 ports: - "127.0.0.1:8536:8536" restart: always diff --git a/server/src/version.rs b/server/src/version.rs index 4efa3b59f..e65234bbc 100644 --- a/server/src/version.rs +++ b/server/src/version.rs @@ -1 +1 @@ -pub const VERSION: &str = "v0.6.14"; +pub const VERSION: &str = "v0.6.15"; diff --git a/ui/src/version.ts b/ui/src/version.ts index f31c87acd..87e24bad7 100644 --- a/ui/src/version.ts +++ b/ui/src/version.ts @@ -1 +1 @@ -export const version: string = 'v0.6.14'; +export const version: string = 'v0.6.15'; From 74aa161ff6a68d1cc5f857a1b9d7cbb91d1ebae3 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sat, 8 Feb 2020 11:16:58 -0500 Subject: [PATCH 31/77] Change post sorting hot rank to use newest comment time. Fixes #517 --- .../down.sql | 88 +++++++++++++++ .../up.sql | 106 ++++++++++++++++++ server/src/db/post_view.rs | 5 + ui/src/interfaces.ts | 1 + 4 files changed, 200 insertions(+) create mode 100644 server/migrations/2020-02-08-145624_add_post_newest_activity_time/down.sql create mode 100644 server/migrations/2020-02-08-145624_add_post_newest_activity_time/up.sql diff --git a/server/migrations/2020-02-08-145624_add_post_newest_activity_time/down.sql b/server/migrations/2020-02-08-145624_add_post_newest_activity_time/down.sql new file mode 100644 index 000000000..8b912fa36 --- /dev/null +++ b/server/migrations/2020-02-08-145624_add_post_newest_activity_time/down.sql @@ -0,0 +1,88 @@ +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), p.published) as hot_rank +from post p +left join post_like pl on p.id = pl.post_id +group by p.id; + +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 +; + diff --git a/server/migrations/2020-02-08-145624_add_post_newest_activity_time/up.sql b/server/migrations/2020-02-08-145624_add_post_newest_activity_time/up.sql new file mode 100644 index 000000000..e15412771 --- /dev/null +++ b/server/migrations/2020-02-08-145624_add_post_newest_activity_time/up.sql @@ -0,0 +1,106 @@ +-- 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 +; + diff --git a/server/src/db/post_view.rs b/server/src/db/post_view.rs index d6a1d1917..a831b87d0 100644 --- a/server/src/db/post_view.rs +++ b/server/src/db/post_view.rs @@ -31,6 +31,7 @@ table! { upvotes -> BigInt, downvotes -> BigInt, hot_rank -> Int4, + newest_activity_time -> Timestamp, user_id -> Nullable, my_vote -> Nullable, subscribed -> Nullable, @@ -70,6 +71,7 @@ pub struct PostView { pub upvotes: i64, pub downvotes: i64, pub hot_rank: i32, + pub newest_activity_time: chrono::NaiveDateTime, pub user_id: Option, pub my_vote: Option, pub subscribed: Option, @@ -106,6 +108,7 @@ table! { upvotes -> BigInt, downvotes -> BigInt, hot_rank -> Int4, + newest_activity_time -> Timestamp, user_id -> Nullable, my_vote -> Nullable, subscribed -> Nullable, @@ -445,6 +448,7 @@ mod tests { downvotes: 0, hot_rank: 1728, published: inserted_post.published, + newest_activity_time: inserted_post.published, updated: None, subscribed: None, read: None, @@ -479,6 +483,7 @@ mod tests { downvotes: 0, hot_rank: 1728, published: inserted_post.published, + newest_activity_time: inserted_post.published, updated: None, subscribed: None, read: None, diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts index 5736d375a..185056f3f 100644 --- a/ui/src/interfaces.ts +++ b/ui/src/interfaces.ts @@ -171,6 +171,7 @@ export interface Post { upvotes: number; downvotes: number; hot_rank: number; + newest_activity_time: string; user_id?: number; my_vote?: number; subscribed?: boolean; From 841a86a6663425d489711625b319d205610f3800 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sat, 8 Feb 2020 11:44:16 -0500 Subject: [PATCH 32/77] Version v0.6.16 --- ansible/VERSION | 2 +- docker/prod/docker-compose.yml | 2 +- server/src/version.rs | 2 +- ui/src/version.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ansible/VERSION b/ansible/VERSION index f9b38caa3..a77ea9dc1 100644 --- a/ansible/VERSION +++ b/ansible/VERSION @@ -1 +1 @@ -v0.6.15 +v0.6.16 diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml index b4973d1f4..aa8fce8bd 100644 --- a/docker/prod/docker-compose.yml +++ b/docker/prod/docker-compose.yml @@ -11,7 +11,7 @@ services: - lemmy_db:/var/lib/postgresql/data restart: always lemmy: - image: dessalines/lemmy:v0.6.15 + image: dessalines/lemmy:v0.6.16 ports: - "127.0.0.1:8536:8536" restart: always diff --git a/server/src/version.rs b/server/src/version.rs index e65234bbc..fda837d29 100644 --- a/server/src/version.rs +++ b/server/src/version.rs @@ -1 +1 @@ -pub const VERSION: &str = "v0.6.15"; +pub const VERSION: &str = "v0.6.16"; diff --git a/ui/src/version.ts b/ui/src/version.ts index 87e24bad7..f2dcbfaa1 100644 --- a/ui/src/version.ts +++ b/ui/src/version.ts @@ -1 +1 @@ -export const version: string = 'v0.6.15'; +export const version: string = 'v0.6.16'; From 52b65bda6900870bced638624f24831c90391a45 Mon Sep 17 00:00:00 2001 From: Richie Zhang <12566991+StaticallyTypedRice@users.noreply.github.com> Date: Sat, 8 Feb 2020 12:53:46 -0800 Subject: [PATCH 33/77] Add an option to initialize the database from install.sh --- install.sh | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 168a1f6b0..b1db69a70 100755 --- a/install.sh +++ b/install.sh @@ -1,13 +1,39 @@ -#!/bin/sh +#!/bin/bash set -e +# Set the database variable to the default first. +# Don't forget to change this string to your actual database parameters +# if you don't plan to initialize the database in this script. export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy + +# Set other environment variables export JWT_SECRET=changeme export HOSTNAME=rrr +# Optionally initialize the database +init_db_valid=0 +init_db_final=0 +while [ "$init_db_valid" == 0 ] +do + read -p "Initialize database (y/n)? " init_db + case "${init_db,,}" in + y|yes ) init_db_valid=1; init_db_final=1;; + n|no ) init_db_valid=1 init_db_final=0;; + * ) echo "Invalid input" 1>&2;; + esac +done +if [ "$init_db_final" = 1 ] +then + source ./server/db-init.sh + read -n 1 -s -r -p "Press ENTER to continue execution of this script, press CTRL+C to quit..." +fi + +# Build the web client cd ui yarn yarn build + +# Build and run the backend cd ../server cargo run From 7f92d82b1b016fa9655ad77a968a9f43767bc1fc Mon Sep 17 00:00:00 2001 From: Richie Zhang <12566991+StaticallyTypedRice@users.noreply.github.com> Date: Sat, 8 Feb 2020 12:54:41 -0800 Subject: [PATCH 34/77] Fix a user prompt in install.sh --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index b1db69a70..60bf67345 100755 --- a/install.sh +++ b/install.sh @@ -25,7 +25,7 @@ done if [ "$init_db_final" = 1 ] then source ./server/db-init.sh - read -n 1 -s -r -p "Press ENTER to continue execution of this script, press CTRL+C to quit..." + read -n 1 -s -r -p "Press ANY KEY to continue execution of this script, press CTRL+C to quit..." fi # Build the web client From 69c22b17a025593450a9982e07bec0e18446758c Mon Sep 17 00:00:00 2001 From: Richie Zhang <12566991+StaticallyTypedRice@users.noreply.github.com> Date: Sat, 8 Feb 2020 12:56:13 -0800 Subject: [PATCH 35/77] Add a semicolon. --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 60bf67345..9bed6e1d5 100755 --- a/install.sh +++ b/install.sh @@ -18,7 +18,7 @@ do read -p "Initialize database (y/n)? " init_db case "${init_db,,}" in y|yes ) init_db_valid=1; init_db_final=1;; - n|no ) init_db_valid=1 init_db_final=0;; + n|no ) init_db_valid=1; init_db_final=0;; * ) echo "Invalid input" 1>&2;; esac done From 49a35b12713f47598f2a6b3763383924fd3312ea Mon Sep 17 00:00:00 2001 From: Richie Zhang <12566991+StaticallyTypedRice@users.noreply.github.com> Date: Sat, 8 Feb 2020 13:02:40 -0800 Subject: [PATCH 36/77] Add line breaks after user prompts in install.sh --- install.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/install.sh b/install.sh index 9bed6e1d5..b368891cf 100755 --- a/install.sh +++ b/install.sh @@ -21,11 +21,13 @@ do n|no ) init_db_valid=1; init_db_final=0;; * ) echo "Invalid input" 1>&2;; esac + echo done if [ "$init_db_final" = 1 ] then source ./server/db-init.sh read -n 1 -s -r -p "Press ANY KEY to continue execution of this script, press CTRL+C to quit..." + echo fi # Build the web client From 469594fac8f0513215b1e7e87a663a33f79ca78c Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sat, 8 Feb 2020 23:20:11 -0500 Subject: [PATCH 37/77] Fixing some technical debt. Fixes #524 --- server/src/db/comment_view.rs | 5 +- server/src/db/community_view.rs | 4 +- server/src/db/post_view.rs | 4 +- server/src/db/user_view.rs | 5 +- ui/package.json | 6 +-- ui/src/components/community.tsx | 60 ++++++---------------- ui/src/components/inbox.tsx | 44 +++++++---------- ui/src/components/main.tsx | 71 +++++++------------------- ui/src/components/post.tsx | 76 +++++++++------------------- ui/src/components/search.tsx | 37 ++++---------- ui/src/components/user.tsx | 48 ++++++------------ ui/src/utils.ts | 88 +++++++++++++++++++++++++++++++++ ui/yarn.lock | 16 +++--- 13 files changed, 207 insertions(+), 257 deletions(-) diff --git a/server/src/db/comment_view.rs b/server/src/db/comment_view.rs index 01e1bbaf5..ff915d5e5 100644 --- a/server/src/db/comment_view.rs +++ b/server/src/db/comment_view.rs @@ -251,9 +251,8 @@ impl CommentView { from_comment_id: i32, my_user_id: Option, ) -> Result { - use super::comment_view::comment_view::dsl::*; - - let mut query = comment_view.into_boxed(); + use super::comment_view::comment_mview::dsl::*; + let mut query = comment_mview.into_boxed(); // The view lets you pass a null user_id, if you're not logged in if let Some(my_user_id) = my_user_id { diff --git a/server/src/db/community_view.rs b/server/src/db/community_view.rs index 95e00c65c..18ff67a8a 100644 --- a/server/src/db/community_view.rs +++ b/server/src/db/community_view.rs @@ -227,9 +227,9 @@ impl CommunityView { from_community_id: i32, from_user_id: Option, ) -> Result { - use super::community_view::community_view::dsl::*; + use super::community_view::community_mview::dsl::*; - let mut query = community_view.into_boxed(); + let mut query = community_mview.into_boxed(); query = query.filter(id.eq(from_community_id)); diff --git a/server/src/db/post_view.rs b/server/src/db/post_view.rs index a831b87d0..3f385077f 100644 --- a/server/src/db/post_view.rs +++ b/server/src/db/post_view.rs @@ -315,10 +315,10 @@ impl PostView { from_post_id: i32, my_user_id: Option, ) -> Result { - use super::post_view::post_view::dsl::*; + use super::post_view::post_mview::dsl::*; use diesel::prelude::*; - let mut query = post_view.into_boxed(); + let mut query = post_mview.into_boxed(); query = query.filter(id.eq(from_post_id)); diff --git a/server/src/db/user_view.rs b/server/src/db/user_view.rs index 3ea506e7f..2274ecbdf 100644 --- a/server/src/db/user_view.rs +++ b/server/src/db/user_view.rs @@ -144,9 +144,8 @@ impl<'a> UserQueryBuilder<'a> { impl UserView { pub fn read(conn: &PgConnection, from_user_id: i32) -> Result { - use super::user_view::user_view::dsl::*; - - user_view.find(from_user_id).first::(conn) + use super::user_view::user_mview::dsl::*; + user_mview.find(from_user_id).first::(conn) } pub fn admins(conn: &PgConnection) -> Result, Error> { diff --git a/ui/package.json b/ui/package.json index 31d91bb4c..9cf349108 100644 --- a/ui/package.json +++ b/ui/package.json @@ -7,7 +7,7 @@ "main": "index.js", "scripts": { "build": "node fuse prod", - "lint": "eslint --report-unused-disable-directives --ext .js,.ts,.tsx src", + "lint": "tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src", "start": "node fuse dev" }, "keywords": [], @@ -22,7 +22,7 @@ "bootswatch": "^4.3.1", "classcat": "^1.1.3", "dotenv": "^8.2.0", - "emoji-short-name": "^0.1.0", + "emoji-short-name": "^1.0.0", "husky": "^4.2.1", "i18next": "^19.0.3", "inferno": "^7.0.1", @@ -35,7 +35,7 @@ "markdown-it-emoji": "^1.4.0", "moment": "^2.24.0", "prettier": "^1.18.2", - "reconnecting-websocket": "^4.3.0", + "reconnecting-websocket": "^4.4.0", "rxjs": "^6.4.0", "terser": "^4.6.3", "toastify-js": "^1.6.2", diff --git a/ui/src/components/community.tsx b/ui/src/components/community.tsx index 3e04a8bfb..32392ca1d 100644 --- a/ui/src/components/community.tsx +++ b/ui/src/components/community.tsx @@ -37,6 +37,12 @@ import { getPageFromProps, getSortTypeFromProps, getDataTypeFromProps, + editCommentRes, + saveCommentRes, + createCommentLikeRes, + createPostLikeFindRes, + editPostFindRes, + commentsToFlatNodes, } from '../utils'; import { i18n } from '../i18next'; @@ -174,13 +180,7 @@ export class Community extends Component { return this.state.dataType == DataType.Post ? ( ) : ( - this.state.comments.map(comment => ( -
    -
    - -
    -
    - )) + ); } @@ -333,30 +333,15 @@ export class Community extends Component { this.setState(this.state); } else if (res.op == UserOperation.EditPost) { let data = res.data as PostResponse; - let found = this.state.posts.find(c => c.id == data.post.id); - if (found) { - found.url = data.post.url; - found.name = data.post.name; - found.nsfw = data.post.nsfw; - this.setState(this.state); - } + editPostFindRes(data, this.state.posts); + this.setState(this.state); } else if (res.op == UserOperation.CreatePost) { let data = res.data as PostResponse; this.state.posts.unshift(data.post); this.setState(this.state); } else if (res.op == UserOperation.CreatePostLike) { let data = res.data as PostResponse; - let found = this.state.posts.find(c => c.id == data.post.id); - if (found) { - found.score = data.post.score; - found.upvotes = data.post.upvotes; - found.downvotes = data.post.downvotes; - if (data.post.my_vote !== null) { - found.my_vote = data.post.my_vote; - found.upvoteLoading = false; - found.downvoteLoading = false; - } - } + createPostLikeFindRes(data, this.state.posts); this.setState(this.state); } else if (res.op == UserOperation.AddModToCommunity) { let data = res.data as AddModToCommunityResponse; @@ -377,18 +362,8 @@ export class Community extends Component { this.setState(this.state); } else if (res.op == UserOperation.EditComment) { let data = res.data as CommentResponse; - - let found = this.state.comments.find(c => c.id == data.comment.id); - if (found) { - found.content = data.comment.content; - found.updated = data.comment.updated; - found.removed = data.comment.removed; - found.deleted = data.comment.deleted; - found.upvotes = data.comment.upvotes; - found.downvotes = data.comment.downvotes; - found.score = data.comment.score; - this.setState(this.state); - } + editCommentRes(data, this.state.comments); + this.setState(this.state); } else if (res.op == UserOperation.CreateComment) { let data = res.data as CommentResponse; @@ -399,18 +374,11 @@ export class Community extends Component { } } else if (res.op == UserOperation.SaveComment) { let data = res.data as CommentResponse; - let found = this.state.comments.find(c => c.id == data.comment.id); - found.saved = data.comment.saved; + saveCommentRes(data, this.state.comments); this.setState(this.state); } else if (res.op == UserOperation.CreateCommentLike) { let data = res.data as CommentResponse; - let found: Comment = this.state.comments.find( - c => c.id === data.comment.id - ); - found.score = data.comment.score; - found.upvotes = data.comment.upvotes; - found.downvotes = data.comment.downvotes; - if (data.comment.my_vote !== null) found.my_vote = data.comment.my_vote; + createCommentLikeRes(data, this.state.comments); this.setState(this.state); } } diff --git a/ui/src/components/inbox.tsx b/ui/src/components/inbox.tsx index 6849b37d2..027a1db04 100644 --- a/ui/src/components/inbox.tsx +++ b/ui/src/components/inbox.tsx @@ -19,7 +19,16 @@ import { PrivateMessageResponse, } from '../interfaces'; import { WebSocketService, UserService } from '../services'; -import { wsJsonToRes, fetchLimit, isCommentType, toast } from '../utils'; +import { + wsJsonToRes, + fetchLimit, + isCommentType, + toast, + editCommentRes, + saveCommentRes, + createCommentLikeRes, + commentsToFlatNodes, +} from '../utils'; import { CommentNodes } from './comment-nodes'; import { PrivateMessage } from './private-message'; import { SortSelect } from './sort-select'; @@ -197,9 +206,11 @@ export class Inbox extends Component { replies() { return (
    - {this.state.replies.map(reply => ( - - ))} +
    ); } @@ -362,15 +373,7 @@ export class Inbox extends Component { this.setState(this.state); } else if (res.op == UserOperation.EditComment) { let data = res.data as CommentResponse; - - let found = this.state.replies.find(c => c.id == data.comment.id); - found.content = data.comment.content; - found.updated = data.comment.updated; - found.removed = data.comment.removed; - found.deleted = data.comment.deleted; - found.upvotes = data.comment.upvotes; - found.downvotes = data.comment.downvotes; - found.score = data.comment.score; + editCommentRes(data, this.state.replies); // If youre in the unread view, just remove it from the list if (this.state.unreadOrAll == UnreadOrAll.Unread && data.comment.read) { @@ -418,28 +421,17 @@ export class Inbox extends Component { this.setState(this.state); } else if (res.op == UserOperation.CreatePrivateMessage) { let data = res.data as PrivateMessageResponse; - if (data.message.recipient_id == UserService.Instance.user.id) { this.state.messages.unshift(data.message); this.setState(this.state); - } else if (data.message.creator_id == UserService.Instance.user.id) { - toast(i18n.t('message_sent')); } - this.setState(this.state); } else if (res.op == UserOperation.SaveComment) { let data = res.data as CommentResponse; - let found = this.state.replies.find(c => c.id == data.comment.id); - found.saved = data.comment.saved; + saveCommentRes(data, this.state.replies); this.setState(this.state); } else if (res.op == UserOperation.CreateCommentLike) { let data = res.data as CommentResponse; - let found: Comment = this.state.replies.find( - c => c.id === data.comment.id - ); - found.score = data.comment.score; - found.upvotes = data.comment.upvotes; - found.downvotes = data.comment.downvotes; - if (data.comment.my_vote !== null) found.my_vote = data.comment.my_vote; + createCommentLikeRes(data, this.state.replies); this.setState(this.state); } } diff --git a/ui/src/components/main.tsx b/ui/src/components/main.tsx index 92434360b..99b1618de 100644 --- a/ui/src/components/main.tsx +++ b/ui/src/components/main.tsx @@ -45,6 +45,12 @@ import { getPageFromProps, getSortTypeFromProps, getDataTypeFromProps, + editCommentRes, + saveCommentRes, + createCommentLikeRes, + createPostLikeFindRes, + editPostFindRes, + commentsToFlatNodes, } from '../utils'; import { i18n } from '../i18next'; import { T } from 'inferno-i18next'; @@ -400,17 +406,11 @@ export class Main extends Component { return this.state.dataType == DataType.Post ? ( ) : ( - this.state.comments.map(comment => ( -
    -
    - -
    -
    - )) + ); } @@ -625,28 +625,12 @@ export class Main extends Component { this.setState(this.state); } else if (res.op == UserOperation.EditPost) { let data = res.data as PostResponse; - let found = this.state.posts.find(c => c.id == data.post.id); - if (found) { - found.url = data.post.url; - found.name = data.post.name; - found.nsfw = data.post.nsfw; - - this.setState(this.state); - } + editPostFindRes(data, this.state.posts); + this.setState(this.state); } else if (res.op == UserOperation.CreatePostLike) { let data = res.data as PostResponse; - let found = this.state.posts.find(c => c.id == data.post.id); - if (found) { - found.score = data.post.score; - found.upvotes = data.post.upvotes; - found.downvotes = data.post.downvotes; - if (data.post.my_vote !== null) { - found.my_vote = data.post.my_vote; - found.upvoteLoading = false; - found.downvoteLoading = false; - } - this.setState(this.state); - } + createPostLikeFindRes(data, this.state.posts); + this.setState(this.state); } else if (res.op == UserOperation.AddAdmin) { let data = res.data as AddAdminResponse; this.state.siteRes.admins = data.admins; @@ -676,18 +660,8 @@ export class Main extends Component { this.setState(this.state); } else if (res.op == UserOperation.EditComment) { let data = res.data as CommentResponse; - - let found = this.state.comments.find(c => c.id == data.comment.id); - if (found) { - found.content = data.comment.content; - found.updated = data.comment.updated; - found.removed = data.comment.removed; - found.deleted = data.comment.deleted; - found.upvotes = data.comment.upvotes; - found.downvotes = data.comment.downvotes; - found.score = data.comment.score; - this.setState(this.state); - } + editCommentRes(data, this.state.comments); + this.setState(this.state); } else if (res.op == UserOperation.CreateComment) { let data = res.data as CommentResponse; @@ -709,18 +683,11 @@ export class Main extends Component { } } else if (res.op == UserOperation.SaveComment) { let data = res.data as CommentResponse; - let found = this.state.comments.find(c => c.id == data.comment.id); - found.saved = data.comment.saved; + saveCommentRes(data, this.state.comments); this.setState(this.state); } else if (res.op == UserOperation.CreateCommentLike) { let data = res.data as CommentResponse; - let found: Comment = this.state.comments.find( - c => c.id === data.comment.id - ); - found.score = data.comment.score; - found.upvotes = data.comment.upvotes; - found.downvotes = data.comment.downvotes; - if (data.comment.my_vote !== null) found.my_vote = data.comment.my_vote; + createCommentLikeRes(data, this.state.comments); this.setState(this.state); } } diff --git a/ui/src/components/post.tsx b/ui/src/components/post.tsx index 922fc01ea..1f2e40ba6 100644 --- a/ui/src/components/post.tsx +++ b/ui/src/components/post.tsx @@ -29,7 +29,16 @@ import { WebSocketJsonResponse, } from '../interfaces'; import { WebSocketService, UserService } from '../services'; -import { wsJsonToRes, hotRank, toast } from '../utils'; +import { + wsJsonToRes, + hotRank, + toast, + editCommentRes, + saveCommentRes, + createCommentLikeRes, + createPostLikeRes, + commentsToFlatNodes, +} from '../utils'; import { PostListing } from './post-listing'; import { PostListings } from './post-listings'; import { Sidebar } from './sidebar'; @@ -256,16 +265,14 @@ export class Post extends Component {
    {i18n.t('recent_comments')}
    - {this.state.comments.map(comment => ( - - ))} +
    ); @@ -408,53 +415,19 @@ export class Post extends Component { } } else if (res.op == UserOperation.EditComment) { let data = res.data as CommentResponse; - let found = this.state.comments.find(c => c.id == data.comment.id); - if (found) { - found.content = data.comment.content; - found.updated = data.comment.updated; - found.removed = data.comment.removed; - found.deleted = data.comment.deleted; - found.upvotes = data.comment.upvotes; - found.downvotes = data.comment.downvotes; - found.score = data.comment.score; - found.read = data.comment.read; - - this.setState(this.state); - } + editCommentRes(data, this.state.comments); + this.setState(this.state); } else if (res.op == UserOperation.SaveComment) { let data = res.data as CommentResponse; - let found = this.state.comments.find(c => c.id == data.comment.id); - if (found) { - found.saved = data.comment.saved; - this.setState(this.state); - } + saveCommentRes(data, this.state.comments); + this.setState(this.state); } else if (res.op == UserOperation.CreateCommentLike) { let data = res.data as CommentResponse; - let found: Comment = this.state.comments.find( - c => c.id === data.comment.id - ); - if (found) { - found.score = data.comment.score; - found.upvotes = data.comment.upvotes; - found.downvotes = data.comment.downvotes; - if (data.comment.my_vote !== null) { - found.my_vote = data.comment.my_vote; - found.upvoteLoading = false; - found.downvoteLoading = false; - } - } + createCommentLikeRes(data, this.state.comments); this.setState(this.state); } else if (res.op == UserOperation.CreatePostLike) { let data = res.data as PostResponse; - this.state.post.score = data.post.score; - this.state.post.upvotes = data.post.upvotes; - this.state.post.downvotes = data.post.downvotes; - if (data.post.my_vote !== null) { - this.state.post.my_vote = data.post.my_vote; - this.state.post.upvoteLoading = false; - this.state.post.downvoteLoading = false; - } - + createPostLikeRes(data, this.state.post); this.setState(this.state); } else if (res.op == UserOperation.EditPost) { let data = res.data as PostResponse; @@ -510,7 +483,6 @@ export class Post extends Component { this.setState(this.state); } else if (res.op == UserOperation.TransferSite) { let data = res.data as GetSiteResponse; - this.state.admins = data.admins; this.setState(this.state); } else if (res.op == UserOperation.TransferCommunity) { diff --git a/ui/src/components/search.tsx b/ui/src/components/search.tsx index 3acb71672..3fd2f4677 100644 --- a/ui/src/components/search.tsx +++ b/ui/src/components/search.tsx @@ -25,6 +25,9 @@ import { pictshareAvatarThumbnail, showAvatars, toast, + createCommentLikeRes, + createPostLikeFindRes, + commentsToFlatNodes, } from '../utils'; import { PostListing } from './post-listing'; import { SortSelect } from './sort-select'; @@ -294,15 +297,11 @@ export class Search extends Component { comments() { return ( - <> - {this.state.searchResponse.comments.map(comment => ( -
    -
    - -
    -
    - ))} - + ); } @@ -474,27 +473,11 @@ export class Search extends Component { this.setState(this.state); } else if (res.op == UserOperation.CreateCommentLike) { let data = res.data as CommentResponse; - let found: Comment = this.state.searchResponse.comments.find( - c => c.id === data.comment.id - ); - found.score = data.comment.score; - found.upvotes = data.comment.upvotes; - found.downvotes = data.comment.downvotes; - if (data.comment.my_vote !== null) { - found.my_vote = data.comment.my_vote; - found.upvoteLoading = false; - found.downvoteLoading = false; - } + createCommentLikeRes(data, this.state.searchResponse.comments); this.setState(this.state); } else if (res.op == UserOperation.CreatePostLike) { let data = res.data as PostResponse; - let found = this.state.searchResponse.posts.find( - c => c.id == data.post.id - ); - found.my_vote = data.post.my_vote; - found.score = data.post.score; - found.upvotes = data.post.upvotes; - found.downvotes = data.post.downvotes; + createPostLikeFindRes(data, this.state.searchResponse.posts); this.setState(this.state); } } diff --git a/ui/src/components/user.tsx b/ui/src/components/user.tsx index effc9e1dc..e2df15e14 100644 --- a/ui/src/components/user.tsx +++ b/ui/src/components/user.tsx @@ -32,6 +32,11 @@ import { languages, showAvatars, toast, + editCommentRes, + saveCommentRes, + createCommentLikeRes, + createPostLikeFindRes, + commentsToFlatNodes, } from '../utils'; import { PostListing } from './post-listing'; import { SortSelect } from './sort-select'; @@ -316,13 +321,11 @@ export class User extends Component { comments() { return (
    - {this.state.comments.map(comment => ( - - ))} +
    ); } @@ -1032,18 +1035,8 @@ export class User extends Component { this.setState(this.state); } else if (res.op == UserOperation.EditComment) { let data = res.data as CommentResponse; - - let found = this.state.comments.find(c => c.id == data.comment.id); - if (found) { - found.content = data.comment.content; - found.updated = data.comment.updated; - found.removed = data.comment.removed; - found.deleted = data.comment.deleted; - found.upvotes = data.comment.upvotes; - found.downvotes = data.comment.downvotes; - found.score = data.comment.score; - this.setState(this.state); - } + editCommentRes(data, this.state.comments); + this.setState(this.state); } else if (res.op == UserOperation.CreateComment) { let data = res.data as CommentResponse; if ( @@ -1054,26 +1047,15 @@ export class User extends Component { } } else if (res.op == UserOperation.SaveComment) { let data = res.data as CommentResponse; - let found = this.state.comments.find(c => c.id == data.comment.id); - found.saved = data.comment.saved; + saveCommentRes(data, this.state.comments); this.setState(this.state); } else if (res.op == UserOperation.CreateCommentLike) { let data = res.data as CommentResponse; - let found: Comment = this.state.comments.find( - c => c.id === data.comment.id - ); - found.score = data.comment.score; - found.upvotes = data.comment.upvotes; - found.downvotes = data.comment.downvotes; - if (data.comment.my_vote !== null) found.my_vote = data.comment.my_vote; + createCommentLikeRes(data, this.state.comments); this.setState(this.state); } else if (res.op == UserOperation.CreatePostLike) { let data = res.data as PostResponse; - let found = this.state.posts.find(c => c.id == data.post.id); - found.my_vote = data.post.my_vote; - found.score = data.post.score; - found.upvotes = data.post.upvotes; - found.downvotes = data.post.downvotes; + createPostLikeFindRes(data, this.state.posts); this.setState(this.state); } else if (res.op == UserOperation.BanUser) { let data = res.data as BanUserResponse; diff --git a/ui/src/utils.ts b/ui/src/utils.ts index 51836c6f0..8cdc02f04 100644 --- a/ui/src/utils.ts +++ b/ui/src/utils.ts @@ -15,6 +15,8 @@ import 'moment/locale/pt-br'; import { UserOperation, Comment, + CommentNode, + Post, PrivateMessage, User, SortType, @@ -25,6 +27,8 @@ import { WebSocketJsonResponse, SearchForm, SearchResponse, + CommentResponse, + PostResponse, } from './interfaces'; import { UserService, WebSocketService } from './services'; @@ -551,3 +555,87 @@ export function getSortTypeFromProps(props: any): SortType { export function getPageFromProps(props: any): number { return props.match.params.page ? Number(props.match.params.page) : 1; } + +export function editCommentRes( + data: CommentResponse, + comments: Array +) { + let found = comments.find(c => c.id == data.comment.id); + if (found) { + found.content = data.comment.content; + found.updated = data.comment.updated; + found.removed = data.comment.removed; + found.deleted = data.comment.deleted; + found.upvotes = data.comment.upvotes; + found.downvotes = data.comment.downvotes; + found.score = data.comment.score; + } +} + +export function saveCommentRes( + data: CommentResponse, + comments: Array +) { + let found = comments.find(c => c.id == data.comment.id); + if (found) { + found.saved = data.comment.saved; + } +} + +export function createCommentLikeRes( + data: CommentResponse, + comments: Array +) { + let found: Comment = comments.find(c => c.id === data.comment.id); + if (found) { + found.score = data.comment.score; + found.upvotes = data.comment.upvotes; + found.downvotes = data.comment.downvotes; + if (data.comment.my_vote !== null) { + found.my_vote = data.comment.my_vote; + found.upvoteLoading = false; + found.downvoteLoading = false; + } + } +} + +export function createPostLikeFindRes(data: PostResponse, posts: Array) { + let found = posts.find(c => c.id == data.post.id); + if (found) { + createPostLikeRes(data, found); + } +} + +export function createPostLikeRes(data: PostResponse, post: Post) { + post.score = data.post.score; + post.upvotes = data.post.upvotes; + post.downvotes = data.post.downvotes; + if (data.post.my_vote !== null) { + post.my_vote = data.post.my_vote; + post.upvoteLoading = false; + post.downvoteLoading = false; + } +} + +export function editPostFindRes(data: PostResponse, posts: Array) { + let found = posts.find(c => c.id == data.post.id); + if (found) { + editPostRes(data, found); + } +} + +export function editPostRes(data: PostResponse, post: Post) { + post.url = data.post.url; + post.name = data.post.name; + post.nsfw = data.post.nsfw; +} + +export function commentsToFlatNodes( + comments: Array +): Array { + let nodes: Array = []; + for (let comment of comments) { + nodes.push({ comment: comment }); + } + return nodes; +} diff --git a/ui/yarn.lock b/ui/yarn.lock index 64493e39f..5c9ad637a 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -1079,10 +1079,10 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -emoji-short-name@^0.1.0: - version "0.1.4" - resolved "https://registry.yarnpkg.com/emoji-short-name/-/emoji-short-name-0.1.4.tgz#125a452adc22a399b089f802f9d8d46ecb6e5b08" - integrity sha512-VTjEKkhN1UARtHLqlK70N5K3SwxuZAkmdm5sXvSjkV677kr0jt/O7mvB5eQqM+3rKCa+w3Qb5G7wwU/fezonKQ== +emoji-short-name@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/emoji-short-name/-/emoji-short-name-1.0.0.tgz#82e6f543b6c68984d69bdc80eac735104fdd4af8" + integrity sha512-+tiniHvgRR7XMI1jAaGveumWg5LALE/nWkFD6CcOn6M5IDM9w4PkMs8UwzLTMoZtDLdTdQmzxGvLOxHVIjPzjg== encodeurl@~1.0.2: version "1.0.2" @@ -3731,10 +3731,10 @@ realm-utils@^1.0.9: app-root-path "^1.3.0" mkdirp "^0.5.1" -reconnecting-websocket@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.3.0.tgz#aaefbc7629a89450aa45324b89aec2276e728cc5" - integrity sha512-3eaHIEVYB9Zb0GfYy1xdEHKJLA2JaawAegByZ1AZ8Npb3AiRgUN5l89cvE2H+pHTsFcoC88t32ky9qET6DJ75Q== +reconnecting-websocket@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783" + integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng== regenerate-unicode-properties@^8.1.0: version "8.1.0" From 54b215a5871b4680b6cc43d10606e474e1bc324b Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sun, 9 Feb 2020 11:44:24 -0500 Subject: [PATCH 38/77] Live post and comment resorting. Fixes #522 - Moving sorting to utils. --- ui/src/components/comment-node.tsx | 6 ++ ui/src/components/comment-nodes.tsx | 19 ++++- ui/src/components/community.tsx | 12 +++- ui/src/components/main.tsx | 9 ++- ui/src/components/post-listings.tsx | 22 ++++-- ui/src/components/post.tsx | 41 +---------- ui/src/utils.ts | 108 ++++++++++++++++++++++++++-- 7 files changed, 164 insertions(+), 53 deletions(-) diff --git a/ui/src/components/comment-node.tsx b/ui/src/components/comment-node.tsx index a67b1c351..0e5110639 100644 --- a/ui/src/components/comment-node.tsx +++ b/ui/src/components/comment-node.tsx @@ -15,6 +15,8 @@ import { TransferCommunityForm, TransferSiteForm, BanType, + CommentSortType, + SortType, } from '../interfaces'; import { WebSocketService, UserService } from '../services'; import { @@ -61,6 +63,8 @@ interface CommentNodeProps { // TODO is this necessary, can't I get it from the node itself? postCreatorId?: number; showCommunity?: boolean; + sort?: CommentSortType; + sortType?: SortType; } export class CommentNode extends Component { @@ -630,6 +634,8 @@ export class CommentNode extends Component { moderators={this.props.moderators} admins={this.props.admins} postCreatorId={this.props.postCreatorId} + sort={this.props.sort} + sortType={this.props.sortType} /> )} {/* A collapsed clearfix */} diff --git a/ui/src/components/comment-nodes.tsx b/ui/src/components/comment-nodes.tsx index fb700cc40..b15da5208 100644 --- a/ui/src/components/comment-nodes.tsx +++ b/ui/src/components/comment-nodes.tsx @@ -3,7 +3,10 @@ import { CommentNode as CommentNodeI, CommunityUser, UserView, + CommentSortType, + SortType, } from '../interfaces'; +import { commentSort, commentSortSortType } from '../utils'; import { CommentNode } from './comment-node'; interface CommentNodesState {} @@ -18,6 +21,8 @@ interface CommentNodesProps { locked?: boolean; markable?: boolean; showCommunity?: boolean; + sort?: CommentSortType; + sortType?: SortType; } export class CommentNodes extends Component< @@ -31,7 +36,7 @@ export class CommentNodes extends Component< render() { return (
    - {this.props.nodes.map(node => ( + {this.sorter().map(node => ( ))}
    ); } + + sorter(): Array { + if (this.props.sort !== undefined) { + commentSort(this.props.nodes, this.props.sort); + } else if (this.props.sortType !== undefined) { + commentSortSortType(this.props.nodes, this.props.sortType); + } + + return this.props.nodes; + } } diff --git a/ui/src/components/community.tsx b/ui/src/components/community.tsx index 32392ca1d..e28c99bc7 100644 --- a/ui/src/components/community.tsx +++ b/ui/src/components/community.tsx @@ -178,9 +178,17 @@ export class Community extends Component { listings() { return this.state.dataType == DataType.Post ? ( - + ) : ( - + ); } diff --git a/ui/src/components/main.tsx b/ui/src/components/main.tsx index 99b1618de..c8e132f7a 100644 --- a/ui/src/components/main.tsx +++ b/ui/src/components/main.tsx @@ -51,6 +51,7 @@ import { createPostLikeFindRes, editPostFindRes, commentsToFlatNodes, + commentSortSortType, } from '../utils'; import { i18n } from '../i18next'; import { T } from 'inferno-i18next'; @@ -404,12 +405,18 @@ export class Main extends Component { listings() { return this.state.dataType == DataType.Post ? ( - + ) : ( ); } diff --git a/ui/src/components/post-listings.tsx b/ui/src/components/post-listings.tsx index 005c4fe03..d61f624d4 100644 --- a/ui/src/components/post-listings.tsx +++ b/ui/src/components/post-listings.tsx @@ -1,6 +1,7 @@ import { Component } from 'inferno'; import { Link } from 'inferno-router'; -import { Post } from '../interfaces'; +import { Post, SortType } from '../interfaces'; +import { postSort } from '../utils'; import { PostListing } from './post-listing'; import { i18n } from '../i18next'; import { T } from 'inferno-i18next'; @@ -9,6 +10,7 @@ interface PostListingsProps { posts: Array; showCommunity?: boolean; removeDuplicates?: boolean; + sort?: SortType; } export class PostListings extends Component { @@ -20,10 +22,7 @@ export class PostListings extends Component { return (
    {this.props.posts.length > 0 ? ( - (this.props.removeDuplicates - ? this.removeDuplicates(this.props.posts) - : this.props.posts - ).map(post => ( + this.outer().map(post => ( <> { ); } + outer(): Array { + let out = this.props.posts; + if (this.props.removeDuplicates) { + out = this.removeDuplicates(out); + } + + if (this.props.sort !== undefined) { + postSort(out, this.props.sort); + } + + return out; + } + removeDuplicates(posts: Array): Array { // A map from post url to list of posts (dupes) let urlMap = new Map>(); diff --git a/ui/src/components/post.tsx b/ui/src/components/post.tsx index 1f2e40ba6..b5b1fce36 100644 --- a/ui/src/components/post.tsx +++ b/ui/src/components/post.tsx @@ -31,7 +31,6 @@ import { import { WebSocketService, UserService } from '../services'; import { wsJsonToRes, - hotRank, toast, editCommentRes, saveCommentRes, @@ -314,48 +313,9 @@ export class Post extends Component { } } - this.sortTree(tree); - return tree; } - sortTree(tree: Array) { - // First, put removed and deleted comments at the bottom, then do your other sorts - if (this.state.commentSort == CommentSortType.Top) { - tree.sort( - (a, b) => - +a.comment.removed - +b.comment.removed || - +a.comment.deleted - +b.comment.deleted || - b.comment.score - a.comment.score - ); - } else if (this.state.commentSort == CommentSortType.New) { - tree.sort( - (a, b) => - +a.comment.removed - +b.comment.removed || - +a.comment.deleted - +b.comment.deleted || - b.comment.published.localeCompare(a.comment.published) - ); - } else if (this.state.commentSort == CommentSortType.Old) { - tree.sort( - (a, b) => - +a.comment.removed - +b.comment.removed || - +a.comment.deleted - +b.comment.deleted || - a.comment.published.localeCompare(b.comment.published) - ); - } else if (this.state.commentSort == CommentSortType.Hot) { - tree.sort( - (a, b) => - +a.comment.removed - +b.comment.removed || - +a.comment.deleted - +b.comment.deleted || - hotRank(b.comment) - hotRank(a.comment) - ); - } - - for (let node of tree) { - this.sortTree(node.children); - } - } - commentsTree() { let nodes = this.buildCommentsTree(); return ( @@ -366,6 +326,7 @@ export class Post extends Component { moderators={this.state.moderators} admins={this.state.admins} postCreatorId={this.state.post.creator_id} + sort={this.state.commentSort} />
    ); diff --git a/ui/src/utils.ts b/ui/src/utils.ts index 8cdc02f04..929877fc7 100644 --- a/ui/src/utils.ts +++ b/ui/src/utils.ts @@ -20,6 +20,7 @@ import { PrivateMessage, User, SortType, + CommentSortType, ListingType, DataType, SearchType, @@ -93,15 +94,22 @@ md.renderer.rules.emoji = function(token, idx) { return twemoji.parse(token[idx].content); }; -export function hotRank(comment: Comment): number { - // Rank = ScaleFactor * sign(Score) * log(1 + abs(Score)) / (Time + 2)^Gravity +export function hotRankComment(comment: Comment): number { + return hotRank(comment.score, comment.published); +} - let date: Date = new Date(comment.published + 'Z'); // Add Z to convert from UTC date +export function hotRankPost(post: Post): number { + return hotRank(post.score, post.newest_activity_time); +} + +export function hotRank(score: number, timeStr: string): number { + // Rank = ScaleFactor * sign(Score) * log(1 + abs(Score)) / (Time + 2)^Gravity + let date: Date = new Date(timeStr + 'Z'); // Add Z to convert from UTC date let now: Date = new Date(); let hoursElapsed: number = (now.getTime() - date.getTime()) / 36e5; let rank = - (10000 * Math.log10(Math.max(1, 3 + comment.score))) / + (10000 * Math.log10(Math.max(1, 3 + score))) / Math.pow(hoursElapsed + 2, 1.8); // console.log(`Comment: ${comment.content}\nRank: ${rank}\nScore: ${comment.score}\nHours: ${hoursElapsed}`); @@ -639,3 +647,95 @@ export function commentsToFlatNodes( } return nodes; } + +export function commentSort(tree: Array, sort: CommentSortType) { + // First, put removed and deleted comments at the bottom, then do your other sorts + if (sort == CommentSortType.Top) { + tree.sort( + (a, b) => + +a.comment.removed - +b.comment.removed || + +a.comment.deleted - +b.comment.deleted || + b.comment.score - a.comment.score + ); + } else if (sort == CommentSortType.New) { + tree.sort( + (a, b) => + +a.comment.removed - +b.comment.removed || + +a.comment.deleted - +b.comment.deleted || + b.comment.published.localeCompare(a.comment.published) + ); + } else if (sort == CommentSortType.Old) { + tree.sort( + (a, b) => + +a.comment.removed - +b.comment.removed || + +a.comment.deleted - +b.comment.deleted || + a.comment.published.localeCompare(b.comment.published) + ); + } else if (sort == CommentSortType.Hot) { + tree.sort( + (a, b) => + +a.comment.removed - +b.comment.removed || + +a.comment.deleted - +b.comment.deleted || + hotRankComment(b.comment) - hotRankComment(a.comment) + ); + } + + // Go through the children recursively + for (let node of tree) { + if (node.children) { + commentSort(node.children, sort); + } + } +} + +export function commentSortSortType(tree: Array, sort: SortType) { + commentSort(tree, convertCommentSortType(sort)); +} + +function convertCommentSortType(sort: SortType): CommentSortType { + if ( + sort == SortType.TopAll || + sort == SortType.TopDay || + sort == SortType.TopWeek || + sort == SortType.TopMonth || + sort == SortType.TopYear + ) { + return CommentSortType.Top; + } else if (sort == SortType.New) { + return CommentSortType.New; + } else if (sort == SortType.Hot) { + return CommentSortType.Hot; + } else { + return CommentSortType.Hot; + } +} + +export function postSort(posts: Array, sort: SortType) { + // First, put removed and deleted comments at the bottom, then do your other sorts + if ( + sort == SortType.TopAll || + sort == SortType.TopDay || + sort == SortType.TopWeek || + sort == SortType.TopMonth || + sort == SortType.TopYear + ) { + posts.sort( + (a, b) => + +a.removed - +b.removed || +a.deleted - +b.deleted || b.score - a.score + ); + } else if (sort == SortType.New) { + posts.sort( + (a, b) => + +a.removed - +b.removed || + +a.deleted - +b.deleted || + b.published.localeCompare(a.published) + ); + } else if (sort == SortType.Hot) { + posts.sort( + (a, b) => + +a.removed - +b.removed || + +a.deleted - +b.deleted || + hotRankPost(b) - hotRankPost(a) + ); + } +} From 036d6260bb7da3952c6862617e5438d105483645 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sun, 9 Feb 2020 15:04:41 -0500 Subject: [PATCH 39/77] Adding instant voting / vote animations. Fixes #526 --- server/src/websocket/server.rs | 3 + ui/assets/css/main.css | 6 ++ ui/src/components/comment-node.tsx | 112 ++++++++++++++++------------- ui/src/components/post-listing.tsx | 103 ++++++++++++++------------ ui/src/interfaces.ts | 4 -- ui/src/utils.ts | 4 -- 6 files changed, 130 insertions(+), 102 deletions(-) diff --git a/server/src/websocket/server.rs b/server/src/websocket/server.rs index 003b886e1..1cbcb34fb 100644 --- a/server/src/websocket/server.rs +++ b/server/src/websocket/server.rs @@ -280,6 +280,9 @@ impl ChatServer { self.send_community_room_message(0, &post_sent_str, id); self.send_community_room_message(community_id, &post_sent_str, id); + // Send it to the post room + self.send_post_room_message(post_sent.post.id, &post_sent_str, id); + to_json_string(&user_operation, post) } diff --git a/ui/assets/css/main.css b/ui/assets/css/main.css index b1ad884a5..df75ec319 100644 --- a/ui/assets/css/main.css +++ b/ui/assets/css/main.css @@ -175,3 +175,9 @@ hr { .img-expanded { max-height: 90vh; } + +.vote-animate:active { + transform: scale(1.2); + -webkit-transform: scale(1.2); + -ms-transform: scale(1.2); +} diff --git a/ui/src/components/comment-node.tsx b/ui/src/components/comment-node.tsx index 0e5110639..1d0b12cad 100644 --- a/ui/src/components/comment-node.tsx +++ b/ui/src/components/comment-node.tsx @@ -48,8 +48,10 @@ interface CommentNodeState { showConfirmAppointAsAdmin: boolean; collapsed: boolean; viewSource: boolean; - upvoteLoading: boolean; - downvoteLoading: boolean; + my_vote: number; + score: number; + upvotes: number; + downvotes: number; } interface CommentNodeProps { @@ -83,8 +85,10 @@ export class CommentNode extends Component { showConfirmTransferCommunity: false, showConfirmAppointAsMod: false, showConfirmAppointAsAdmin: false, - upvoteLoading: this.props.node.comment.upvoteLoading, - downvoteLoading: this.props.node.comment.downvoteLoading, + my_vote: this.props.node.comment.my_vote, + score: this.props.node.comment.score, + upvotes: this.props.node.comment.upvotes, + downvotes: this.props.node.comment.downvotes, }; constructor(props: any, context: any) { @@ -97,15 +101,11 @@ export class CommentNode extends Component { } componentWillReceiveProps(nextProps: CommentNodeProps) { - if ( - nextProps.node.comment.upvoteLoading !== this.state.upvoteLoading || - nextProps.node.comment.downvoteLoading !== this.state.downvoteLoading - ) { - this.setState({ - upvoteLoading: false, - downvoteLoading: false, - }); - } + this.state.my_vote = nextProps.node.comment.my_vote; + this.state.upvotes = nextProps.node.comment.upvotes; + this.state.downvotes = nextProps.node.comment.downvotes; + this.state.score = nextProps.node.comment.score; + this.setState(this.state); } render() { @@ -122,40 +122,26 @@ export class CommentNode extends Component { .viewOnly && 'no-click'}`} > -
    - {node.comment.score} -
    +
    {this.state.score}
    {WebSocketService.Instance.site.enable_downvotes && ( )}
    @@ -205,9 +191,9 @@ export class CommentNode extends Component { )}
  • - (+{node.comment.upvotes} + (+{this.state.upvotes} | - -{node.comment.downvotes} + -{this.state.downvotes} )
  • @@ -772,31 +758,57 @@ export class CommentNode extends Component { } handleCommentUpvote(i: CommentNodeI) { - if (UserService.Instance.user) { - this.setState({ - upvoteLoading: true, - }); + let new_vote = this.state.my_vote == 1 ? 0 : 1; + + if (this.state.my_vote == 1) { + this.state.score--; + this.state.upvotes--; + } else if (this.state.my_vote == -1) { + this.state.downvotes--; + this.state.upvotes++; + this.state.score += 2; + } else { + this.state.upvotes++; + this.state.score++; } + + this.state.my_vote = new_vote; + let form: CommentLikeForm = { comment_id: i.comment.id, post_id: i.comment.post_id, - score: i.comment.my_vote == 1 ? 0 : 1, + score: this.state.my_vote, }; + WebSocketService.Instance.likeComment(form); + this.setState(this.state); } handleCommentDownvote(i: CommentNodeI) { - if (UserService.Instance.user) { - this.setState({ - downvoteLoading: true, - }); + let new_vote = this.state.my_vote == -1 ? 0 : -1; + + if (this.state.my_vote == 1) { + this.state.score -= 2; + this.state.upvotes--; + this.state.downvotes++; + } else if (this.state.my_vote == -1) { + this.state.downvotes--; + this.state.score++; + } else { + this.state.downvotes++; + this.state.score--; } + + this.state.my_vote = new_vote; + let form: CommentLikeForm = { comment_id: i.comment.id, post_id: i.comment.post_id, - score: i.comment.my_vote == -1 ? 0 : -1, + score: this.state.my_vote, }; + WebSocketService.Instance.likeComment(form); + this.setState(this.state); } handleModRemoveShow(i: CommentNode) { diff --git a/ui/src/components/post-listing.tsx b/ui/src/components/post-listing.tsx index f11d9e144..1e7289013 100644 --- a/ui/src/components/post-listing.tsx +++ b/ui/src/components/post-listing.tsx @@ -43,8 +43,10 @@ interface PostListingState { showConfirmTransferCommunity: boolean; imageExpanded: boolean; viewSource: boolean; - upvoteLoading: boolean; - downvoteLoading: boolean; + my_vote: number; + score: number; + upvotes: number; + downvotes: number; } interface PostListingProps { @@ -68,8 +70,10 @@ export class PostListing extends Component { showConfirmTransferCommunity: false, imageExpanded: false, viewSource: false, - upvoteLoading: this.props.post.upvoteLoading, - downvoteLoading: this.props.post.downvoteLoading, + my_vote: this.props.post.my_vote, + score: this.props.post.score, + upvotes: this.props.post.upvotes, + downvotes: this.props.post.downvotes, }; constructor(props: any, context: any) { @@ -83,15 +87,11 @@ export class PostListing extends Component { } componentWillReceiveProps(nextProps: PostListingProps) { - if ( - nextProps.post.upvoteLoading !== this.state.upvoteLoading || - nextProps.post.downvoteLoading !== this.state.downvoteLoading - ) { - this.setState({ - upvoteLoading: false, - downvoteLoading: false, - }); - } + this.state.my_vote = nextProps.post.my_vote; + this.state.upvotes = nextProps.post.upvotes; + this.state.downvotes = nextProps.post.downvotes; + this.state.score = nextProps.post.score; + this.setState(this.state); } render() { @@ -118,38 +118,26 @@ export class PostListing extends Component {
    -
    {post.score}
    +
    {this.state.score}
    {WebSocketService.Instance.site.enable_downvotes && ( )}
    @@ -315,9 +303,9 @@ export class PostListing extends Component {
  • - (+{post.upvotes} + (+{this.state.upvotes} | - -{post.downvotes} + -{this.state.downvotes} )
  • @@ -747,28 +735,55 @@ export class PostListing extends Component { } handlePostLike(i: PostListing) { - if (UserService.Instance.user) { - i.setState({ upvoteLoading: true }); + let new_vote = i.state.my_vote == 1 ? 0 : 1; + + if (i.state.my_vote == 1) { + i.state.score--; + i.state.upvotes--; + } else if (i.state.my_vote == -1) { + i.state.downvotes--; + i.state.upvotes++; + i.state.score += 2; + } else { + i.state.upvotes++; + i.state.score++; } + i.state.my_vote = new_vote; + let form: CreatePostLikeForm = { post_id: i.props.post.id, - score: i.props.post.my_vote == 1 ? 0 : 1, + score: i.state.my_vote, }; WebSocketService.Instance.likePost(form); + i.setState(i.state); } handlePostDisLike(i: PostListing) { - if (UserService.Instance.user) { - i.setState({ downvoteLoading: true }); + let new_vote = i.state.my_vote == -1 ? 0 : -1; + + if (i.state.my_vote == 1) { + i.state.score -= 2; + i.state.upvotes--; + i.state.downvotes++; + } else if (i.state.my_vote == -1) { + i.state.downvotes--; + i.state.score++; + } else { + i.state.downvotes++; + i.state.score--; } + i.state.my_vote = new_vote; + let form: CreatePostLikeForm = { post_id: i.props.post.id, - score: i.props.post.my_vote == -1 ? 0 : -1, + score: i.state.my_vote, }; + WebSocketService.Instance.likePost(form); + i.setState(i.state); } handleEditClick(i: PostListing) { diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts index 185056f3f..23551b595 100644 --- a/ui/src/interfaces.ts +++ b/ui/src/interfaces.ts @@ -177,8 +177,6 @@ export interface Post { subscribed?: boolean; read?: boolean; saved?: boolean; - upvoteLoading?: boolean; - downvoteLoading?: boolean; duplicates?: Array; } @@ -209,8 +207,6 @@ export interface Comment { saved?: boolean; user_mention_id?: number; // For mention type recipient_id?: number; - upvoteLoading?: boolean; - downvoteLoading?: boolean; } export interface Category { diff --git a/ui/src/utils.ts b/ui/src/utils.ts index 929877fc7..384d5c1d5 100644 --- a/ui/src/utils.ts +++ b/ui/src/utils.ts @@ -601,8 +601,6 @@ export function createCommentLikeRes( found.downvotes = data.comment.downvotes; if (data.comment.my_vote !== null) { found.my_vote = data.comment.my_vote; - found.upvoteLoading = false; - found.downvoteLoading = false; } } } @@ -620,8 +618,6 @@ export function createPostLikeRes(data: PostResponse, post: Post) { post.downvotes = data.post.downvotes; if (data.post.my_vote !== null) { post.my_vote = data.post.my_vote; - post.upvoteLoading = false; - post.downvoteLoading = false; } } From 9237226585c82f3a015d545e226205a08b294e97 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sun, 9 Feb 2020 15:08:51 -0500 Subject: [PATCH 40/77] Version v0.6.17 --- ansible/VERSION | 2 +- docker/prod/docker-compose.yml | 2 +- server/src/version.rs | 2 +- ui/src/version.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ansible/VERSION b/ansible/VERSION index a77ea9dc1..c32d86a50 100644 --- a/ansible/VERSION +++ b/ansible/VERSION @@ -1 +1 @@ -v0.6.16 +v0.6.17 diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml index aa8fce8bd..4baceb3ed 100644 --- a/docker/prod/docker-compose.yml +++ b/docker/prod/docker-compose.yml @@ -11,7 +11,7 @@ services: - lemmy_db:/var/lib/postgresql/data restart: always lemmy: - image: dessalines/lemmy:v0.6.16 + image: dessalines/lemmy:v0.6.17 ports: - "127.0.0.1:8536:8536" restart: always diff --git a/server/src/version.rs b/server/src/version.rs index fda837d29..92eb4e7f3 100644 --- a/server/src/version.rs +++ b/server/src/version.rs @@ -1 +1 @@ -pub const VERSION: &str = "v0.6.16"; +pub const VERSION: &str = "v0.6.17"; diff --git a/ui/src/version.ts b/ui/src/version.ts index f2dcbfaa1..8eab99577 100644 --- a/ui/src/version.ts +++ b/ui/src/version.ts @@ -1 +1 @@ -export const version: string = 'v0.6.16'; +export const version: string = 'v0.6.17'; From d6867fa791c91f5a0eccad548878bd861cc02b15 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 10 Feb 2020 14:45:50 -0500 Subject: [PATCH 41/77] Fixing ansible certbot renew. --- ansible/lemmy.yml | 2 +- ansible/lemmy_dev.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ansible/lemmy.yml b/ansible/lemmy.yml index 87f71699d..c415abef5 100644 --- a/ansible/lemmy.yml +++ b/ansible/lemmy.yml @@ -63,4 +63,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'" diff --git a/ansible/lemmy_dev.yml b/ansible/lemmy_dev.yml index c15569faf..c150714ca 100644 --- a/ansible/lemmy_dev.yml +++ b/ansible/lemmy_dev.yml @@ -97,4 +97,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'" From 6bddeecdb621272a056cf742247335e1fb96682d Mon Sep 17 00:00:00 2001 From: Dessalines Date: Tue, 11 Feb 2020 10:14:09 -0500 Subject: [PATCH 42/77] Fixing unread indicator on link click. Fixes #527 --- ui/src/components/inbox.tsx | 2 +- ui/src/components/navbar.tsx | 8 +++++--- ui/src/components/post.tsx | 4 ++++ ui/src/interfaces.ts | 1 + ui/src/services/UserService.ts | 7 +++---- 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/ui/src/components/inbox.tsx b/ui/src/components/inbox.tsx index 027a1db04..56bf15785 100644 --- a/ui/src/components/inbox.tsx +++ b/ui/src/components/inbox.tsx @@ -443,9 +443,9 @@ export class Inbox extends Component { 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, }); } } diff --git a/ui/src/components/navbar.tsx b/ui/src/components/navbar.tsx index c675cfe7b..75cdd5549 100644 --- a/ui/src/components/navbar.tsx +++ b/ui/src/components/navbar.tsx @@ -60,8 +60,10 @@ export class Navbar extends Component { // Subscribe to user changes this.userSub = UserService.Instance.sub.subscribe(user => { this.state.isLoggedIn = user.user !== undefined; - this.state.unreadCount = user.unreadCount; - this.requestNotificationPermission(); + if (this.state.isLoggedIn) { + this.state.unreadCount = user.user.unreadCount; + this.requestNotificationPermission(); + } this.setState(this.state); }); @@ -304,9 +306,9 @@ export class Navbar extends Component { } sendUnreadCount() { + UserService.Instance.user.unreadCount = this.state.unreadCount; UserService.Instance.sub.next({ user: UserService.Instance.user, - unreadCount: this.state.unreadCount, }); } diff --git a/ui/src/components/post.tsx b/ui/src/components/post.tsx index b5b1fce36..d8f662cf7 100644 --- a/ui/src/components/post.tsx +++ b/ui/src/components/post.tsx @@ -156,6 +156,10 @@ export class Post extends Component { auth: null, }; WebSocketService.Instance.editComment(form); + UserService.Instance.user.unreadCount--; + UserService.Instance.sub.next({ + user: UserService.Instance.user, + }); } } diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts index 23551b595..5846b548c 100644 --- a/ui/src/interfaces.ts +++ b/ui/src/interfaces.ts @@ -93,6 +93,7 @@ export interface User { lang: string; avatar?: string; show_avatars: boolean; + unreadCount?: number; } export interface UserView { diff --git a/ui/src/services/UserService.ts b/ui/src/services/UserService.ts index 03380e593..47e28c73e 100644 --- a/ui/src/services/UserService.ts +++ b/ui/src/services/UserService.ts @@ -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); } From d3f8e5512435e0e3c52cdc9a154cbaf5d1120b3a Mon Sep 17 00:00:00 2001 From: Dessalines Date: Wed, 12 Feb 2020 12:12:19 -0500 Subject: [PATCH 43/77] Add community refine by searching on new post creation. Fixes #521 --- ui/assets/css/main.css | 6 ++++++ ui/assets/css/selectr.min.css | 7 +++++++ ui/package.json | 1 + ui/src/components/post-form.tsx | 8 ++++++++ ui/src/index.html | 1 + ui/yarn.lock | 5 +++++ 6 files changed, 28 insertions(+) create mode 100644 ui/assets/css/selectr.min.css diff --git a/ui/assets/css/main.css b/ui/assets/css/main.css index df75ec319..e7784877b 100644 --- a/ui/assets/css/main.css +++ b/ui/assets/css/main.css @@ -181,3 +181,9 @@ 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; +} diff --git a/ui/assets/css/selectr.min.css b/ui/assets/css/selectr.min.css new file mode 100644 index 000000000..78bab83fc --- /dev/null +++ b/ui/assets/css/selectr.min.css @@ -0,0 +1,7 @@ +/*! + * Selectr 2.4.13 + * http://mobius.ovh/docs/selectr + * + * Released under the MIT license + */ +.selectr-container li,.selectr-option,.selectr-tag{list-style:none}.selectr-container{position:relative}.selectr-hidden{position:absolute;overflow:hidden;clip:rect(0,0,0,0);width:1px;height:1px;margin:-1px;padding:0;border:0}.selectr-visible{position:absolute;left:0;top:0;width:100%;height:100%;opacity:0;z-index:11}.selectr-desktop.multiple .selectr-visible{display:none}.selectr-desktop.multiple.native-open .selectr-visible{top:100%;min-height:200px!important;height:auto;opacity:1;display:block}.selectr-container.multiple.selectr-mobile .selectr-selected{z-index:0}.selectr-selected{position:relative;z-index:1;box-sizing:border-box;width:100%;padding:7px 28px 7px 14px;cursor:pointer;border:1px solid #999;border-radius:3px;}.selectr-selected::before{position:absolute;top:50%;right:10px;width:0;height:0;content:'';-o-transform:rotate(0) translate3d(0,-50%,0);-ms-transform:rotate(0) translate3d(0,-50%,0);-moz-transform:rotate(0) translate3d(0,-50%,0);-webkit-transform:rotate(0) translate3d(0,-50%,0);transform:rotate(0) translate3d(0,-50%,0);border-width:4px 4px 0;border-style:solid;border-color:#6c7a86 transparent transparent}.selectr-container.native-open .selectr-selected::before,.selectr-container.open .selectr-selected::before{border-width:0 4px 4px;border-style:solid;border-color:transparent transparent #6c7a86}.selectr-label{display:none;overflow:hidden;width:100%;white-space:nowrap;text-overflow:ellipsis}.selectr-placeholder{color:#6c7a86}.selectr-tags{margin:0;padding:0;white-space:normal}.has-selected .selectr-tags{margin:0 0 -2px}.selectr-tag{position:relative;float:left;padding:2px 25px 2px 8px;margin:0 2px 2px 0;cursor:default;color:#fff;border:none;border-radius:10px;background:#acb7bf}.selectr-container.multiple.has-selected .selectr-selected{padding:5px 28px 5px 5px}.selectr-options-container{position:absolute;z-index:10000;top:calc(100% - 1px);left:0;display:none;box-sizing:border-box;width:100%;border-width:0 1px 1px;border-style:solid;border-color:transparent #999 #999;border-radius:0 0 3px 3px;}.selectr-container.open .selectr-options-container{display:block}.selectr-input-container{position:relative;display:none}.selectr-clear,.selectr-input-clear,.selectr-tag-remove{position:absolute;top:50%;right:22px;width:20px;height:20px;padding:0;cursor:pointer;-o-transform:translate3d(0,-50%,0);-ms-transform:translate3d(0,-50%,0);-moz-transform:translate3d(0,-50%,0);-webkit-transform:translate3d(0,-50%,0);transform:translate3d(0,-50%,0);border:none;background-color:transparent;z-index:11}.selectr-clear,.selectr-input-clear{display:none}.selectr-container.has-selected .selectr-clear,.selectr-input-container.active,.selectr-input-container.active .selectr-clear,.selectr-input-container.active .selectr-input-clear{display:block}.selectr-selected .selectr-tag-remove{right:2px}.selectr-clear::after,.selectr-clear::before,.selectr-input-clear::after,.selectr-input-clear::before,.selectr-tag-remove::after,.selectr-tag-remove::before{position:absolute;top:5px;left:9px;width:2px;height:10px;content:' ';background-color:#6c7a86}.selectr-tag-remove::after,.selectr-tag-remove::before{top:4px;width:3px;height:12px;}.selectr-clear:before,.selectr-input-clear::before,.selectr-tag-remove::before{-o-transform:rotate(45deg);-ms-transform:rotate(45deg);-moz-transform:rotate(45deg);-webkit-transform:rotate(45deg);transform:rotate(45deg)}.selectr-clear:after,.selectr-input-clear::after,.selectr-tag-remove::after{-o-transform:rotate(-45deg);-ms-transform:rotate(-45deg);-moz-transform:rotate(-45deg);-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}.selectr-input{top:5px;left:5px;box-sizing:border-box;width:calc(100% - 30px);margin:10px 15px;padding:7px 30px 7px 9px;border:1px solid #999;border-radius:3px}.selectr-notice{display:none;box-sizing:border-box;width:100%;padding:8px 16px;border-top:1px solid #999;border-radius:0 0 3px 3px;}.input-tag,.taggable .selectr-label{width:auto}.selectr-container.notice .selectr-notice{display:block}.selectr-container.notice .selectr-selected{border-radius:3px 3px 0 0}.selectr-options{position:relative;top:calc(100% + 2px);display:none;overflow-x:auto;overflow-y:scroll;max-height:200px;margin:0;padding:0}.selectr-container.notice .selectr-options-container,.selectr-container.open .selectr-input-container,.selectr-container.open .selectr-options{display:block}.selectr-option{position:relative;display:block;padding:5px 20px;cursor:pointer;font-weight:400}.has-selected .selectr-placeholder,.selectr-empty,.selectr-option.excluded{display:none}.selectr-options.optgroups>.selectr-option{padding-left:25px}.selectr-optgroup{font-weight:700;padding:0}.selectr-optgroup--label{font-weight:700;margin-top:10px;padding:5px 15px}.selectr-match{text-decoration:underline}.selectr-option.selected{background-color:#ddd}.selectr-option.active{color:#fff;background-color:#5897fb}.selectr-option.disabled{opacity:.4}.selectr-container.open .selectr-selected{border-color:#999 #999 transparent;border-radius:3px 3px 0 0}.selectr-container.open .selectr-selected::after{-o-transform:rotate(180deg) translate3d(0,50%,0);-ms-transform:rotate(180deg) translate3d(0,50%,0);-moz-transform:rotate(180deg) translate3d(0,50%,0);-webkit-transform:rotate(180deg) translate3d(0,50%,0);transform:rotate(180deg) translate3d(0,50%,0)}.selectr-disabled{opacity:.6}.has-selected .selectr-label{display:block}.taggable .selectr-selected{padding:4px 28px 4px 4px}.taggable .selectr-selected::after{display:table;content:" ";clear:both}.taggable .selectr-tags{float:left;display:block}.taggable .selectr-placeholder{display:none}.input-tag{float:left;min-width:90px}.selectr-tag-input{border:none;padding:3px 10px;width:100%;font-family:inherit;font-weight:inherit;font-size:inherit}.selectr-input-container.loading::after{position:absolute;top:50%;right:20px;width:20px;height:20px;content:'';-o-transform:translate3d(0,-50%,0);-ms-transform:translate3d(0,-50%,0);-moz-transform:translate3d(0,-50%,0);-webkit-transform:translate3d(0,-50%,0);transform:translate3d(0,-50%,0);-o-transform-origin:50% 0 0;-ms-transform-origin:50% 0 0;-moz-transform-origin:50% 0 0;-webkit-transform-origin:50% 0 0;transform-origin:50% 0 0;-moz-animation:.5s linear 0s normal forwards infinite running spin;-webkit-animation:.5s linear 0s normal forwards infinite running spin;animation:.5s linear 0s normal forwards infinite running spin;border-width:3px;border-style:solid;border-color:#aaa #ddd #ddd;border-radius:50%}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0) translate3d(0,-50%,0);transform:rotate(0) translate3d(0,-50%,0)}100%{-webkit-transform:rotate(360deg) translate3d(0,-50%,0);transform:rotate(360deg) translate3d(0,-50%,0)}}@keyframes spin{0%{-webkit-transform:rotate(0) translate3d(0,-50%,0);transform:rotate(0) translate3d(0,-50%,0)}100%{-webkit-transform:rotate(360deg) translate3d(0,-50%,0);transform:rotate(360deg) translate3d(0,-50%,0)}}.selectr-container.open.inverted .selectr-selected{border-color:transparent #999 #999;border-radius:0 0 3px 3px}.selectr-container.inverted .selectr-options-container{border-width:1px 1px 0;border-color:#999 #999 transparent;border-radius:3px 3px 0 0;top:auto;bottom:calc(100% - 1px)}.selectr-container ::-webkit-input-placeholder{color:#6c7a86;opacity:1}.selectr-container ::-moz-placeholder{color:#6c7a86;opacity:1}.selectr-container :-ms-input-placeholder{color:#6c7a86;opacity:1}.selectr-container ::placeholder{color:#6c7a86;opacity:1} diff --git a/ui/package.json b/ui/package.json index 9cf349108..6d7ad7758 100644 --- a/ui/package.json +++ b/ui/package.json @@ -33,6 +33,7 @@ "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", diff --git a/ui/src/components/post-form.tsx b/ui/src/components/post-form.tsx index 7984c2a85..8abdf26d1 100644 --- a/ui/src/components/post-form.tsx +++ b/ui/src/components/post-form.tsx @@ -35,6 +35,7 @@ import { } 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; @@ -514,6 +515,13 @@ export class PostForm extends Component { this.state.postForm.community_id = data.communities[0].id; } this.setState(this.state); + + // Set up select searching + let selectId: any = document.getElementById('post-community'); + let selector = new Selectr(selectId, {}); + 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) { diff --git a/ui/src/index.html b/ui/src/index.html index 09f13097d..24bed7247 100644 --- a/ui/src/index.html +++ b/ui/src/index.html @@ -13,6 +13,7 @@ + diff --git a/ui/yarn.lock b/ui/yarn.lock index 5c9ad637a..4cbc90d95 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -3076,6 +3076,11 @@ mkdirp@^0.5.1: dependencies: minimist "0.0.8" +mobius1-selectr@^2.4.13: + version "2.4.13" + resolved "https://registry.yarnpkg.com/mobius1-selectr/-/mobius1-selectr-2.4.13.tgz#0019dfd9f984840d6e40f70683ab3ec78ce3b5df" + integrity sha512-Mk9qDrvU44UUL0EBhbAA1phfQZ7aMZPjwtL7wkpiBzGh8dETGqfsh50mWoX9EkjDlkONlErWXArHCKfoxVg0Bw== + moment@^2.24.0: version "2.24.0" resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" From deae2b6adedc7d030a5be09ba4c1c556510e0667 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Thu, 13 Feb 2020 12:35:22 -0500 Subject: [PATCH 44/77] Fix minor loading bug. --- ui/src/utils.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/ui/src/utils.ts b/ui/src/utils.ts index 384d5c1d5..329168d26 100644 --- a/ui/src/utils.ts +++ b/ui/src/utils.ts @@ -613,11 +613,13 @@ export function createPostLikeFindRes(data: PostResponse, posts: Array) { } export function createPostLikeRes(data: PostResponse, post: Post) { - post.score = data.post.score; - post.upvotes = data.post.upvotes; - post.downvotes = data.post.downvotes; - if (data.post.my_vote !== null) { - post.my_vote = data.post.my_vote; + if (post) { + post.score = data.post.score; + post.upvotes = data.post.upvotes; + post.downvotes = data.post.downvotes; + if (data.post.my_vote !== null) { + post.my_vote = data.post.my_vote; + } } } @@ -629,9 +631,11 @@ export function editPostFindRes(data: PostResponse, posts: Array) { } export function editPostRes(data: PostResponse, post: Post) { - post.url = data.post.url; - post.name = data.post.name; - post.nsfw = data.post.nsfw; + if (post) { + post.url = data.post.url; + post.name = data.post.name; + post.nsfw = data.post.nsfw; + } } export function commentsToFlatNodes( From a95f2829364cbabdb29dfb2f271b924e8d6d6853 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Fri, 14 Feb 2020 10:02:37 -0500 Subject: [PATCH 45/77] Making links go to post page. --- ui/src/components/post-listing.tsx | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/ui/src/components/post-listing.tsx b/ui/src/components/post-listing.tsx index 1e7289013..4dea4d86c 100644 --- a/ui/src/components/post-listing.tsx +++ b/ui/src/components/post-listing.tsx @@ -171,24 +171,13 @@ export class PostListing extends Component {
    - {post.url ? ( - - {post.name} - - ) : ( - - {post.name} - - )} + + {post.name} +
    {post.url && ( From ee2c6e085088276d743e611975628854513dbd9a Mon Sep 17 00:00:00 2001 From: Dessalines Date: Fri, 14 Feb 2020 10:12:40 -0500 Subject: [PATCH 46/77] Version v0.6.18 --- ansible/VERSION | 2 +- docker/prod/docker-compose.yml | 2 +- server/src/version.rs | 2 +- ui/src/version.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ansible/VERSION b/ansible/VERSION index c32d86a50..5425cbcce 100644 --- a/ansible/VERSION +++ b/ansible/VERSION @@ -1 +1 @@ -v0.6.17 +v0.6.18 diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml index 4baceb3ed..876c82c14 100644 --- a/docker/prod/docker-compose.yml +++ b/docker/prod/docker-compose.yml @@ -11,7 +11,7 @@ services: - lemmy_db:/var/lib/postgresql/data restart: always lemmy: - image: dessalines/lemmy:v0.6.17 + image: dessalines/lemmy:v0.6.18 ports: - "127.0.0.1:8536:8536" restart: always diff --git a/server/src/version.rs b/server/src/version.rs index 92eb4e7f3..6ec29de8e 100644 --- a/server/src/version.rs +++ b/server/src/version.rs @@ -1 +1 @@ -pub const VERSION: &str = "v0.6.17"; +pub const VERSION: &str = "v0.6.18"; diff --git a/ui/src/version.ts b/ui/src/version.ts index 8eab99577..262956349 100644 --- a/ui/src/version.ts +++ b/ui/src/version.ts @@ -1 +1 @@ -export const version: string = 'v0.6.17'; +export const version: string = 'v0.6.18'; From a04b360d91e2f0d61d6d046c1e7c0950f14faa4e Mon Sep 17 00:00:00 2001 From: Richard Date: Fri, 14 Feb 2020 21:30:06 +0100 Subject: [PATCH 47/77] changed permissions from db-init.sh to be able to run --- server/db-init.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 server/db-init.sh diff --git a/server/db-init.sh b/server/db-init.sh old mode 100644 new mode 100755 From f98b2bd4ba280feb4411e4e687f0727ff7f85f24 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Fri, 14 Feb 2020 22:28:29 -0500 Subject: [PATCH 48/77] Adding an external link icon. #530 --- ui/src/components/post-listing.tsx | 3 +++ ui/src/components/symbols.tsx | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/ui/src/components/post-listing.tsx b/ui/src/components/post-listing.tsx index 4dea4d86c..1b8042f8a 100644 --- a/ui/src/components/post-listing.tsx +++ b/ui/src/components/post-listing.tsx @@ -188,6 +188,9 @@ export class PostListing extends Component { title={post.url} > {new URL(post.url).hostname} + + + )} diff --git a/ui/src/components/symbols.tsx b/ui/src/components/symbols.tsx index 829201e4c..0870efd71 100644 --- a/ui/src/components/symbols.tsx +++ b/ui/src/components/symbols.tsx @@ -15,6 +15,10 @@ export class Symbols extends Component { xmlnsXlink="http://www.w3.org/1999/xlink" > + + external-link + + coffee1 From 10223d5aef157ae2e59edc85b568edd9ec3af8dc Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sat, 15 Feb 2020 16:09:53 -0500 Subject: [PATCH 49/77] Fix minor issue with selector. Fix issue with truncate wrapping. --- ui/src/components/post-form.tsx | 10 ++++++---- ui/src/components/post-listing.tsx | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/ui/src/components/post-form.tsx b/ui/src/components/post-form.tsx index 8abdf26d1..d502330b4 100644 --- a/ui/src/components/post-form.tsx +++ b/ui/src/components/post-form.tsx @@ -518,10 +518,12 @@ export class PostForm extends Component { // Set up select searching let selectId: any = document.getElementById('post-community'); - let selector = new Selectr(selectId, {}); - selector.on('selectr.select', option => { - this.state.postForm.community_id = Number(option.value); - }); + 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) { diff --git a/ui/src/components/post-listing.tsx b/ui/src/components/post-listing.tsx index 1b8042f8a..41cd6675a 100644 --- a/ui/src/components/post-listing.tsx +++ b/ui/src/components/post-listing.tsx @@ -169,7 +169,7 @@ export class PostListing extends Component { )}
    -
    +
    {
    {post.url && ( - + Date: Sat, 15 Feb 2020 20:29:57 -0500 Subject: [PATCH 50/77] Some front end fixes. --- ui/src/components/community.tsx | 6 +++--- ui/src/components/main.tsx | 4 ++-- ui/src/components/post-listing.tsx | 25 ++++++++++++++++++------- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/ui/src/components/community.tsx b/ui/src/components/community.tsx index e28c99bc7..67386469e 100644 --- a/ui/src/components/community.tsx +++ b/ui/src/components/community.tsx @@ -136,6 +136,7 @@ export class Community extends Component { render() { return (
    + {this.selects()} {this.state.loading ? (
    @@ -158,7 +159,6 @@ export class Community extends Component { )}
    - {this.selects()} {this.listings()} {this.paginator()}
    @@ -200,7 +200,7 @@ export class Community extends Component { onChange={this.handleDataTypeChange} /> - +
    { }`} target="_blank" > - + # diff --git a/ui/src/components/main.tsx b/ui/src/components/main.tsx index c8e132f7a..87a2fb66b 100644 --- a/ui/src/components/main.tsx +++ b/ui/src/components/main.tsx @@ -386,6 +386,7 @@ export class Main extends Component { posts() { return (
    + {this.selects()} {this.state.loading ? (
    @@ -394,7 +395,6 @@ export class Main extends Component {
    ) : (
    - {this.selects()} {this.listings()} {this.paginator()}
    @@ -428,7 +428,7 @@ export class Main extends Component { type_={this.state.dataType} onChange={this.handleDataTypeChange} /> - + {
    - - {post.name} - + {this.props.showBody && post.url ? ( + + {post.name} + + ) : ( + + {post.name} + + )}
    {post.url && ( From be83e9933431965e356b8976152824571564a49d Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sun, 16 Feb 2020 14:26:39 -0500 Subject: [PATCH 51/77] Version v0.6.19 --- ansible/VERSION | 2 +- docker/prod/docker-compose.yml | 2 +- server/src/version.rs | 2 +- ui/src/version.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ansible/VERSION b/ansible/VERSION index 5425cbcce..6139a0061 100644 --- a/ansible/VERSION +++ b/ansible/VERSION @@ -1 +1 @@ -v0.6.18 +v0.6.19 diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml index 876c82c14..3472be5d6 100644 --- a/docker/prod/docker-compose.yml +++ b/docker/prod/docker-compose.yml @@ -11,7 +11,7 @@ services: - lemmy_db:/var/lib/postgresql/data restart: always lemmy: - image: dessalines/lemmy:v0.6.18 + image: dessalines/lemmy:v0.6.19 ports: - "127.0.0.1:8536:8536" restart: always diff --git a/server/src/version.rs b/server/src/version.rs index 6ec29de8e..cd7310c47 100644 --- a/server/src/version.rs +++ b/server/src/version.rs @@ -1 +1 @@ -pub const VERSION: &str = "v0.6.18"; +pub const VERSION: &str = "v0.6.19"; diff --git a/ui/src/version.ts b/ui/src/version.ts index 262956349..c58b46d3f 100644 --- a/ui/src/version.ts +++ b/ui/src/version.ts @@ -1 +1 @@ -export const version: string = 'v0.6.18'; +export const version: string = 'v0.6.19'; From 4b4e84b309567f9e9aa1d18a37c429b475ef249a Mon Sep 17 00:00:00 2001 From: Andre Vallestero Date: Sun, 16 Feb 2020 16:04:00 -0500 Subject: [PATCH 52/77] Added french translation for sponsor message --- ui/src/translations/fr.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ui/src/translations/fr.ts b/ui/src/translations/fr.ts index 663b7dcb9..f16af7bfc 100644 --- a/ui/src/translations/fr.ts +++ b/ui/src/translations/fr.ts @@ -140,8 +140,7 @@ export const fr = { "Lemmy est gratuit et <1>open-source, 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.', + general_sponsors: 'Les sponsors généraux sont ceux garantissant de 10 à 39$.', crypto: 'Cryptomonnaies', bitcoin: 'Bitcoin', ethereum: 'Ethereum', From 9e5cf8f27291c0d7c7e9e513b9d0ce7a4c53fbf8 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 17 Feb 2020 11:18:01 -0500 Subject: [PATCH 53/77] First pass at adding oembeds / iframely. --- ansible/lemmy.yml | 1 + ansible/lemmy_dev.yml | 1 + ansible/templates/docker-compose.yml | 9 + ansible/templates/nginx.conf | 7 + docker/dev/docker-compose.yml | 8 + docker/iframely.config.local.js | 283 ++++++++++++++++++++++ docker/prod/docker-compose.yml | 8 + docs/src/administration_install_docker.md | 1 + ui/src/components/iframely-card.tsx | 100 ++++++++ ui/src/components/post-listing.tsx | 53 +++- ui/src/interfaces.ts | 15 ++ 11 files changed, 482 insertions(+), 4 deletions(-) create mode 100644 docker/iframely.config.local.js create mode 100644 ui/src/components/iframely-card.tsx diff --git a/ansible/lemmy.yml b/ansible/lemmy.yml index c415abef5..8d5e22641 100644 --- a/ansible/lemmy.yml +++ b/ansible/lemmy.yml @@ -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' diff --git a/ansible/lemmy_dev.yml b/ansible/lemmy_dev.yml index c150714ca..e9b8364f3 100644 --- a/ansible/lemmy_dev.yml +++ b/ansible/lemmy_dev.yml @@ -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' diff --git a/ansible/templates/docker-compose.yml b/ansible/templates/docker-compose.yml index 2693d7ad2..bf9aeeb5a 100644 --- a/ansible/templates/docker-compose.yml +++ b/ansible/templates/docker-compose.yml @@ -30,6 +30,14 @@ services: - lemmy_pictshare:/usr/share/nginx/html/data restart: always + lemmy_iframely: + image: dogbin/iframely:latest + ports: + - "127.0.0.1:8061:8061" + volumes: + - ./iframely.config.local.js:/iframely/config.local.js:ro + restart: always + postfix: image: mwader/postfix-relay environment: @@ -38,3 +46,4 @@ services: volumes: lemmy_db: lemmy_pictshare: + lemmy_iframely: diff --git a/ansible/templates/nginx.conf b/ansible/templates/nginx.conf index 9f31140b2..04e5a6436 100644 --- a/ansible/templates/nginx.conf +++ b/ansible/templates/nginx.conf @@ -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 diff --git a/docker/dev/docker-compose.yml b/docker/dev/docker-compose.yml index eabd334d5..987be4d5b 100644 --- a/docker/dev/docker-compose.yml +++ b/docker/dev/docker-compose.yml @@ -28,6 +28,14 @@ services: volumes: - lemmy_pictshare:/usr/share/nginx/html/data restart: always + lemmy_iframely: + image: dogbin/iframely:latest + ports: + - "127.0.0.1:8061:8061" + volumes: + - ../iframely.config.local.js:/iframely/config.local.js:ro + restart: always volumes: lemmy_db: lemmy_pictshare: + lemmy_iframely: diff --git a/docker/iframely.config.local.js b/docker/iframely.config.local.js new file mode 100644 index 000000000..5c00cb143 --- /dev/null +++ b/docker/iframely.config.local.js @@ -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: 8061, //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; +})(); diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml index 3472be5d6..8469a1e7b 100644 --- a/docker/prod/docker-compose.yml +++ b/docker/prod/docker-compose.yml @@ -26,6 +26,14 @@ services: volumes: - lemmy_pictshare:/usr/share/nginx/html/data restart: always + lemmy_iframely: + image: dogbin/iframely:latest + ports: + - "127.0.0.1:8061:8061" + volumes: + - ./iframely.config.local.js:/iframely/config.local.js:ro + restart: always volumes: lemmy_db: lemmy_pictshare: + lemmy_iframely: diff --git a/docs/src/administration_install_docker.md b/docs/src/administration_install_docker.md index f92cbd5be..992049839 100644 --- a/docs/src/administration_install_docker.md +++ b/docs/src/administration_install_docker.md @@ -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 ``` diff --git a/ui/src/components/iframely-card.tsx b/ui/src/components/iframely-card.tsx new file mode 100644 index 000000000..73f3cef72 --- /dev/null +++ b/ui/src/components/iframely-card.tsx @@ -0,0 +1,100 @@ +import { Component, linkEvent } from 'inferno'; +import { FramelyData } from '../interfaces'; +import { mdToHtml } from '../utils'; + +interface FramelyCardProps { + iframely: FramelyData; +} + +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 iframely = this.props.iframely; + return ( + <> +
    +
    + {iframely.thumbnail_url && ( +
    + {iframely.html ? ( + + + + ) : ( + + )} +
    + )} +
    +
    +
    + + + {iframely.title} + + +
    + + + {new URL(iframely.url).hostname} + + + + + {iframely.html && ( + + {this.state.expanded ? '[-]' : '[+]'} + + )} + + {iframely.description && ( +
    + )} +
    +
    +
    +
    + {this.state.expanded && ( +
    +
    +
    + )} + + ); + } + + handleIframeExpand(i: IFramelyCard) { + i.state.expanded = !i.state.expanded; + i.setState(i.state); + } +} diff --git a/ui/src/components/post-listing.tsx b/ui/src/components/post-listing.tsx index d37725440..5cc632517 100644 --- a/ui/src/components/post-listing.tsx +++ b/ui/src/components/post-listing.tsx @@ -15,9 +15,11 @@ import { AddAdminForm, TransferSiteForm, TransferCommunityForm, + FramelyData, } from '../interfaces'; import { MomentTime } from './moment-time'; import { PostForm } from './post-form'; +import { IFramelyCard } from './iframely-card'; import { mdToHtml, canMod, @@ -47,6 +49,7 @@ interface PostListingState { score: number; upvotes: number; downvotes: number; + iframely: FramelyData; } interface PostListingProps { @@ -74,6 +77,7 @@ export class PostListing extends Component { score: this.props.post.score, upvotes: this.props.post.upvotes, downvotes: this.props.post.downvotes, + iframely: null, }; constructor(props: any, context: any) { @@ -84,6 +88,10 @@ export class PostListing extends Component { this.handlePostDisLike = this.handlePostDisLike.bind(this); this.handleEditPost = this.handleEditPost.bind(this); this.handleEditCancel = this.handleEditCancel.bind(this); + + if (this.props.post.url) { + this.fetchIframely(); + } } componentWillReceiveProps(nextProps: PostListingProps) { @@ -141,7 +149,7 @@ export class PostListing extends Component { )}
    - {post.url && isImage(post.url) && !this.state.imageExpanded && ( + {this.hasImage() && !this.state.imageExpanded && ( { className={`mx-2 mt-1 float-left img-fluid thumbnail rounded ${(post.nsfw || post.community_nsfw) && 'img-blur'}`} - src={imageThumbnailer(post.url)} + src={imageThumbnailer(this.getImage())} /> )} @@ -205,7 +213,7 @@ export class PostListing extends Component {
    )} - {post.url && isImage(post.url) && ( + {this.hasImage() && ( <> {!this.state.imageExpanded ? ( { class="pointer" onClick={linkEvent(this, this.handleImageExpandClick)} > - +
    @@ -587,6 +598,9 @@ export class PostListing extends Component { )} + {post.url && this.props.showBody && this.state.iframely && ( + + )} {this.state.showRemoveDialog && (
    { ); } + fetchIframely() { + fetch(`/iframely/oembed?url=${this.props.post.url}`) + .then(res => res.json()) + .then(res => { + this.state.iframely = res; + this.setState(this.state); + }) + .catch(error => { + console.error(`Iframely service not set up properly. ${error}`); + }); + } + + hasImage(): boolean { + return ( + (this.props.post.url && isImage(this.props.post.url)) || + (this.state.iframely && this.state.iframely.thumbnail_url !== undefined) + ); + } + + getImage(): string { + let simpleImg = isImage(this.props.post.url); + if (simpleImg) { + return this.props.post.url; + } else if (this.state.iframely) { + let iframelyThumbnail = this.state.iframely.thumbnail_url; + if (iframelyThumbnail) { + return iframelyThumbnail; + } + } + } + handlePostLike(i: PostListing) { let new_vote = i.state.my_vote == 1 ? 0 : 1; diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts index 5846b548c..5baadb170 100644 --- a/ui/src/interfaces.ts +++ b/ui/src/interfaces.ts @@ -876,3 +876,18 @@ export interface WebSocketJsonResponse { error?: string; reconnect?: boolean; } + +export interface FramelyData { + url: string; + type: string; + version?: string; + title: string; + author?: string; + author_url?: string; + provider_name?: string; + thumbnail_url?: string; + thumbnail_width?: number; + thumbnail_height?: number; + description?: string; + html?: string; +} From 97fccb561515101ad770453182dcacf29b344821 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 17 Feb 2020 12:45:08 -0500 Subject: [PATCH 54/77] Only show it if it has a title. --- ui/src/components/iframely-card.tsx | 94 +++++++++++++++-------------- 1 file changed, 48 insertions(+), 46 deletions(-) diff --git a/ui/src/components/iframely-card.tsx b/ui/src/components/iframely-card.tsx index 73f3cef72..a9193528f 100644 --- a/ui/src/components/iframely-card.tsx +++ b/ui/src/components/iframely-card.tsx @@ -27,60 +27,62 @@ export class IFramelyCard extends Component< let iframely = this.props.iframely; return ( <> -
    -
    - {iframely.thumbnail_url && ( -
    - {iframely.html ? ( - - - - ) : ( - - )} -
    - )} -
    -
    -
    - - - {iframely.title} - - -
    - - - {new URL(iframely.url).hostname} - - - - - {iframely.html && ( + {iframely.title && ( +
    +
    + {iframely.thumbnail_url && ( +
    + {iframely.html ? ( - {this.state.expanded ? '[-]' : '[+]'} + + ) : ( + )} - - {iframely.description && ( -
    - )} +
    + )} +
    +
    +
    + + + {iframely.title} + + +
    + + + {new URL(iframely.url).hostname} + + + + + {iframely.html && ( + + {this.state.expanded ? '[-]' : '[+]'} + + )} + + {iframely.description && ( +
    + )} +
    -
    + )} {this.state.expanded && (
    Date: Mon, 17 Feb 2020 12:55:43 -0500 Subject: [PATCH 55/77] Remove the responsive bootstrap utils. --- ui/src/components/iframely-card.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/ui/src/components/iframely-card.tsx b/ui/src/components/iframely-card.tsx index a9193528f..ea237edb3 100644 --- a/ui/src/components/iframely-card.tsx +++ b/ui/src/components/iframely-card.tsx @@ -84,12 +84,10 @@ export class IFramelyCard extends Component<
    )} {this.state.expanded && ( -
    -
    -
    +
    )} ); From d6eeabd73b9d35ae22e0596187cdbaea8214a791 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 17 Feb 2020 13:04:53 -0500 Subject: [PATCH 56/77] Version v0.6.20 --- ansible/VERSION | 2 +- docker/prod/docker-compose.yml | 2 +- server/src/version.rs | 2 +- ui/src/version.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ansible/VERSION b/ansible/VERSION index 6139a0061..672c01a45 100644 --- a/ansible/VERSION +++ b/ansible/VERSION @@ -1 +1 @@ -v0.6.19 +v0.6.20 diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml index 8469a1e7b..4ffe00867 100644 --- a/docker/prod/docker-compose.yml +++ b/docker/prod/docker-compose.yml @@ -11,7 +11,7 @@ services: - lemmy_db:/var/lib/postgresql/data restart: always lemmy: - image: dessalines/lemmy:v0.6.19 + image: dessalines/lemmy:v0.6.20 ports: - "127.0.0.1:8536:8536" restart: always diff --git a/server/src/version.rs b/server/src/version.rs index cd7310c47..220cd9ba5 100644 --- a/server/src/version.rs +++ b/server/src/version.rs @@ -1 +1 @@ -pub const VERSION: &str = "v0.6.19"; +pub const VERSION: &str = "v0.6.20"; diff --git a/ui/src/version.ts b/ui/src/version.ts index c58b46d3f..90de0134d 100644 --- a/ui/src/version.ts +++ b/ui/src/version.ts @@ -1 +1 @@ -export const version: string = 'v0.6.19'; +export const version: string = 'v0.6.20'; From ae473c2b0dfe41ab1c4e23bf673fe41cad939b76 Mon Sep 17 00:00:00 2001 From: Andre Vallestero Date: Mon, 17 Feb 2020 13:54:22 -0500 Subject: [PATCH 57/77] Completed french translations, verified syntax --- ui/src/translations/fr.ts | 46 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/ui/src/translations/fr.ts b/ui/src/translations/fr.ts index f16af7bfc..7e8a6ba9c 100644 --- a/ui/src/translations/fr.ts +++ b/ui/src/translations/fr.ts @@ -9,7 +9,8 @@ export const fr = { posts: 'Publications', related_posts: 'Ces sujets peuvent être corrélés', cross_posts: 'Ce sujet a également été posté sur :', - cross_post: 'crosspost', + cross_post: 'publication croisée', + cross_posted_to: 'publication croisée à', comments: 'Commentaires', number_of_comments: '{{count}} Commentaires', remove_comment: 'Supprimer le commentaire', @@ -23,11 +24,18 @@ export const fr = { list_of_communities: 'Liste des communautés', number_of_communities: '{{count}} communautés', community_reqs: 'en minuscule, sans espace et avec tiret du bas.', + create_private_message: 'Créer un message privé', + send_secure_message: 'Envoyer le message sécurisé', + send_message: 'Enovyer le message', + message: 'Message', edit: 'éditer', reply: 'répondre', cancel: 'Annuler', preview: 'prévisualiser', upload_image: 'envoyer une image', + avatar: 'Avatar', + upload_avatar: 'Télécharger une avatar', + show_avatar: 'Afficher les avatars', formatting_help: 'aide au formattage', view_source: 'voir la source', unlock: 'débloquer', @@ -35,6 +43,7 @@ export const fr = { sticky: 'épingler', unsticky: 'décrocher', link: 'lien', + archive_link: 'archiver le lien', mod: 'modérateur', mods: 'modérateurs', moderates: 'Modérer', @@ -89,6 +98,7 @@ export const fr = { sort_type: 'Trier', hot: 'Tendances', new: 'Nouveaux', + old: 'Ancien', top_day: 'Top du jour', week: 'Semaine', month: 'Mois', @@ -96,12 +106,16 @@ export const fr = { all: 'Tout', top: 'Top', api: 'API', + docs: 'Documentations', inbox: 'Boîte de réception', inbox_for: 'Boîte de réception de <1>{{user}}', mark_all_as_read: 'Tout marquer comme lu', type: 'Type', unread: 'Non-lu', + replies: 'Réponses', + mentions: 'Mentions', reply_sent: 'Réponse envoyée', + message_sent: 'Message envoyé', search: 'Rechercher', overview: 'Général', view: 'Voir', @@ -112,11 +126,29 @@ export const fr = { notifications_error: 'Les notifications de bureau ne sont pas discponibles sur votre navigateur. Essayez Firefox ou Chrome.', unread_messages: 'Messages non-lu', + messages: 'Messages', password: 'Mot de passe', verify_password: 'Vérifiez le mot de passe', + old_password: 'Ancien mot de passe', + forgot_password: 'Mot de passe oublié', + reset_password_mail_sent: 'Un email a été envoyé pour réinitialiser votre mot de passe.', + password_change: '----------------------------------------------', + new_password: 'Nouveau mot de passe', + no_email_setup: "Ce serveur n'a pas correctement configuré la messagerie de email.", email: 'Email', + matrix_user_id: 'Utilisateur Matrix', + private_message_disclaimer: + "Attention: les messages privés en Matrix ne sont pas sécurisés. S'il vous plait, créer un compte de <1>Riot.im pour des messages sécurisés.", + send_notifications_to_email: 'Envoyer des notifications par email', optional: 'Optionnel', expires: 'Expire', + language: 'Langue', + browser_default: 'Défaut pour le navigateur', + downvotes_disabled: 'Votes négatifs désactivés', + enable_downvotes: 'Votes négatifs activés', + open_registration: 'Ouvrir la regestration', + registration_closed: 'Régestration fermée', + enable_nsfw: 'Activer NSFW', url: 'URL', body: 'Texte', copy_suggested_title: 'Ajouter le titre suggéré: {{title}}', @@ -140,6 +172,8 @@ export const fr = { "Lemmy est gratuit et <1>open-source, 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', + donate_to_lemmy: 'Faire un don à reddit', + donate: 'Faire un don', general_sponsors: 'Les sponsors généraux sont ceux garantissant de 10 à 39$.', crypto: 'Cryptomonnaies', bitcoin: 'Bitcoin', @@ -149,6 +183,7 @@ export const fr = { joined: 'Membre depuis', by: 'par', to: 'vers', + from: 'de', transfer_community: 'transférer la communauté', transfer_site: 'transférer le site', are_you_sure: 'Êtes-vous sûr ?', @@ -158,12 +193,14 @@ export const fr = { landing_0: 'Lemmy est un <1>aggrégateur de lien, similaire à reddit et conçu pour fonctionner sur le <2>fédiverse.<3>Il est auto-hébergeable, se met à jour en direct et est léger (<4>~80kB). La fédération via Activitypub est prévue. <5>Lemmy est une <6>version beta très précoce, et de nombreuses fonctionnalités sont manquantes ou non fonctionnelles. <7>Vous pouvez rapporter des bugs et suggérez de nouvelles fonctionnalités <8>ici.<9>Crée avec <10>Rust, <11>Actix, <12>Inferno, <13>Typescript.', not_logged_in: "Vous n'êtes pas connecté.", + logged_in: 'Vous êtes 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.', + couldnt_get_comments: 'Impossible de obtenir les commentaires.', no_comment_edit_allowed: "Vous n'êtes pas autorisé à éditer ce commentaire.", no_post_edit_allowed: "Vous n'êtes pas autorisé à éditer sujet.", @@ -176,6 +213,7 @@ export const fr = { 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.', + post_title_too_long: 'Sujet titre trop long.', couldnt_like_post: "Impossible d'aimer le sujet.", couldnt_find_post: 'Impossible de trouver le sujet.', couldnt_get_posts: "Impossible d'obtenir les sujets", @@ -191,8 +229,14 @@ export const fr = { 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à.", + email_already_exists: "l'email 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.', + couldnt_create_private_message: 'Impossible de créer un message privé.', + no_private_message_edit_allowed: 'Pas autorisé à modifier un message privé.', + couldnt_update_private_message: 'Impossible de modifier un message privé.', + time: 'Temps', + action: 'Action', }, }; From d629f26ea726425a2a9eeef71c80b84960997694 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 17 Feb 2020 14:06:19 -0500 Subject: [PATCH 58/77] Add oembed to readme. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 47290953c..be92c428f 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,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. @@ -136,7 +137,7 @@ fa | 71% | cross_post,cross_posted_to,subscribed_to_communities,trending_communi 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 +fr | 100% | show_avatars 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 From 3a598b7af2387552a2df96492bc52388e1f65c4b Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 17 Feb 2020 14:37:36 -0500 Subject: [PATCH 59/77] Removing images from iframely cards. --- ui/src/components/iframely-card.tsx | 31 +++++++++-------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/ui/src/components/iframely-card.tsx b/ui/src/components/iframely-card.tsx index ea237edb3..b0a0d34b3 100644 --- a/ui/src/components/iframely-card.tsx +++ b/ui/src/components/iframely-card.tsx @@ -27,27 +27,10 @@ export class IFramelyCard extends Component< let iframely = this.props.iframely; return ( <> - {iframely.title && ( -
    -
    - {iframely.thumbnail_url && ( -
    - {iframely.html ? ( - - - - ) : ( - - )} -
    - )} -
    + {iframely.title && !this.state.expanded && ( +
    +
    +
    @@ -57,7 +40,11 @@ export class IFramelyCard extends Component<
    - + {new URL(iframely.url).hostname} From a0e0c3edabca8d3a6b4566766ab469a563123216 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 17 Feb 2020 14:38:16 -0500 Subject: [PATCH 60/77] Version v0.6.21 --- ansible/VERSION | 2 +- docker/prod/docker-compose.yml | 2 +- server/src/version.rs | 2 +- ui/src/version.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ansible/VERSION b/ansible/VERSION index 672c01a45..a0387771c 100644 --- a/ansible/VERSION +++ b/ansible/VERSION @@ -1 +1 @@ -v0.6.20 +v0.6.21 diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml index 4ffe00867..e1cf6b250 100644 --- a/docker/prod/docker-compose.yml +++ b/docker/prod/docker-compose.yml @@ -11,7 +11,7 @@ services: - lemmy_db:/var/lib/postgresql/data restart: always lemmy: - image: dessalines/lemmy:v0.6.20 + image: dessalines/lemmy:v0.6.21 ports: - "127.0.0.1:8536:8536" restart: always diff --git a/server/src/version.rs b/server/src/version.rs index 220cd9ba5..798dce2e7 100644 --- a/server/src/version.rs +++ b/server/src/version.rs @@ -1 +1 @@ -pub const VERSION: &str = "v0.6.20"; +pub const VERSION: &str = "v0.6.21"; diff --git a/ui/src/version.ts b/ui/src/version.ts index 90de0134d..853731e0d 100644 --- a/ui/src/version.ts +++ b/ui/src/version.ts @@ -1 +1 @@ -export const version: string = 'v0.6.20'; +export const version: string = 'v0.6.21'; From 87969e70242f72beb1c46f5a6a0f1ede7723268b Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 17 Feb 2020 16:00:18 -0500 Subject: [PATCH 61/77] Fix dynamic post url changing issue. --- ui/src/components/iframely-card.tsx | 4 +-- ui/src/components/post-listing.tsx | 43 +++++++++++++++++------------ 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/ui/src/components/iframely-card.tsx b/ui/src/components/iframely-card.tsx index b0a0d34b3..4bae06d1c 100644 --- a/ui/src/components/iframely-card.tsx +++ b/ui/src/components/iframely-card.tsx @@ -52,7 +52,7 @@ export class IFramelyCard extends Component< {iframely.html && ( {this.state.expanded ? '[-]' : '[+]'} @@ -72,7 +72,7 @@ export class IFramelyCard extends Component< )} {this.state.expanded && (
    )} diff --git a/ui/src/components/post-listing.tsx b/ui/src/components/post-listing.tsx index 5cc632517..7b3d64798 100644 --- a/ui/src/components/post-listing.tsx +++ b/ui/src/components/post-listing.tsx @@ -49,6 +49,7 @@ interface PostListingState { score: number; upvotes: number; downvotes: number; + url: string; iframely: FramelyData; } @@ -77,6 +78,7 @@ export class PostListing extends Component { score: this.props.post.score, upvotes: this.props.post.upvotes, downvotes: this.props.post.downvotes, + url: this.props.post.url, iframely: null, }; @@ -89,7 +91,7 @@ export class PostListing extends Component { this.handleEditPost = this.handleEditPost.bind(this); this.handleEditCancel = this.handleEditCancel.bind(this); - if (this.props.post.url) { + if (this.state.url) { this.fetchIframely(); } } @@ -99,6 +101,13 @@ export class PostListing extends Component { this.state.upvotes = nextProps.post.upvotes; this.state.downvotes = nextProps.post.downvotes; this.state.score = nextProps.post.score; + this.state.url = nextProps.post.url; + this.state.iframely = null; + + if (nextProps.post.url) { + this.fetchIframely(); + } + this.setState(this.state); } @@ -163,7 +172,7 @@ export class PostListing extends Component { /> )} - {post.url && isVideo(post.url) && ( + {this.state.url && isVideo(this.state.url) && ( )}
    - {this.props.showBody && post.url ? ( + {this.props.showBody && this.state.url ? ( {post.name} @@ -198,15 +207,15 @@ export class PostListing extends Component { )}
    - {post.url && ( + {this.state.url && ( - {new URL(post.url).hostname} + {new URL(this.state.url).hostname} @@ -598,7 +607,7 @@ export class PostListing extends Component { )} - {post.url && this.props.showBody && this.state.iframely && ( + {this.state.url && this.props.showBody && this.state.iframely && ( )} {this.state.showRemoveDialog && ( @@ -752,7 +761,7 @@ export class PostListing extends Component { } fetchIframely() { - fetch(`/iframely/oembed?url=${this.props.post.url}`) + fetch(`/iframely/oembed?url=${this.state.url}`) .then(res => res.json()) .then(res => { this.state.iframely = res; @@ -765,15 +774,15 @@ export class PostListing extends Component { hasImage(): boolean { return ( - (this.props.post.url && isImage(this.props.post.url)) || + (this.state.url && isImage(this.state.url)) || (this.state.iframely && this.state.iframely.thumbnail_url !== undefined) ); } getImage(): string { - let simpleImg = isImage(this.props.post.url); + let simpleImg = isImage(this.state.url); if (simpleImg) { - return this.props.post.url; + return this.state.url; } else if (this.state.iframely) { let iframelyThumbnail = this.state.iframely.thumbnail_url; if (iframelyThumbnail) { @@ -877,8 +886,8 @@ export class PostListing extends Component { get crossPostParams(): string { let params = `?title=${this.props.post.name}`; - if (this.props.post.url) { - params += `&url=${this.props.post.url}`; + if (this.state.url) { + params += `&url=${this.state.url}`; } if (this.props.post.body) { params += `&body=${this.props.post.body}`; From b79ec0cc1be205bc359a2f93072cf87bae61aa0b Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 17 Feb 2020 16:11:49 -0500 Subject: [PATCH 62/77] Version v0.6.22 --- ansible/VERSION | 2 +- docker/prod/docker-compose.yml | 2 +- server/src/version.rs | 2 +- ui/src/version.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ansible/VERSION b/ansible/VERSION index a0387771c..635026fb8 100644 --- a/ansible/VERSION +++ b/ansible/VERSION @@ -1 +1 @@ -v0.6.21 +v0.6.22 diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml index e1cf6b250..2ada25123 100644 --- a/docker/prod/docker-compose.yml +++ b/docker/prod/docker-compose.yml @@ -11,7 +11,7 @@ services: - lemmy_db:/var/lib/postgresql/data restart: always lemmy: - image: dessalines/lemmy:v0.6.21 + image: dessalines/lemmy:v0.6.22 ports: - "127.0.0.1:8536:8536" restart: always diff --git a/server/src/version.rs b/server/src/version.rs index 798dce2e7..e4fc27d2a 100644 --- a/server/src/version.rs +++ b/server/src/version.rs @@ -1 +1 @@ -pub const VERSION: &str = "v0.6.21"; +pub const VERSION: &str = "v0.6.22"; diff --git a/ui/src/version.ts b/ui/src/version.ts index 853731e0d..0e9211a2b 100644 --- a/ui/src/version.ts +++ b/ui/src/version.ts @@ -1 +1 @@ -export const version: string = 'v0.6.21'; +export const version: string = 'v0.6.22'; From 6cb10d266a6faa7e28fd4bc645d976a407d04f9c Mon Sep 17 00:00:00 2001 From: Andre Vallestero Date: Mon, 17 Feb 2020 17:20:03 -0500 Subject: [PATCH 63/77] Corrected show_avatar to show_avatars --- ui/src/translations/fr.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/translations/fr.ts b/ui/src/translations/fr.ts index 7e8a6ba9c..de3d158ba 100644 --- a/ui/src/translations/fr.ts +++ b/ui/src/translations/fr.ts @@ -34,7 +34,7 @@ export const fr = { preview: 'prévisualiser', upload_image: 'envoyer une image', avatar: 'Avatar', - upload_avatar: 'Télécharger une avatar', + upload_avatars: 'Télécharger une avatar', show_avatar: 'Afficher les avatars', formatting_help: 'aide au formattage', view_source: 'voir la source', From e8735e4edcf964a1c1c535bda09bd8d2c0039ec5 Mon Sep 17 00:00:00 2001 From: Andre Vallestero Date: Mon, 17 Feb 2020 17:30:19 -0500 Subject: [PATCH 64/77] Corrections made, added text for password change --- ui/src/translations/fr.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/translations/fr.ts b/ui/src/translations/fr.ts index de3d158ba..abc3332dd 100644 --- a/ui/src/translations/fr.ts +++ b/ui/src/translations/fr.ts @@ -132,7 +132,7 @@ export const fr = { old_password: 'Ancien mot de passe', forgot_password: 'Mot de passe oublié', reset_password_mail_sent: 'Un email a été envoyé pour réinitialiser votre mot de passe.', - password_change: '----------------------------------------------', + password_change: 'Changement de mot de passe', new_password: 'Nouveau mot de passe', no_email_setup: "Ce serveur n'a pas correctement configuré la messagerie de email.", email: 'Email', @@ -229,7 +229,7 @@ export const fr = { 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à.", - email_already_exists: "l'email existe déjà", + email_already_exists: "L'email 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.', From 8a16497eef0dad072c138379688b118e9c5403d4 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 17 Feb 2020 18:26:39 -0500 Subject: [PATCH 65/77] Updating translation report. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index be92c428f..b55d678fc 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ fa | 71% | cross_post,cross_posted_to,subscribed_to_communities,trending_communi 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 | 100% | show_avatars +fr | 100% | upload_avatar,show_avatars 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 From 8c81dc04da15c41deed334916e6e179d583b40b2 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 17 Feb 2020 20:48:17 -0500 Subject: [PATCH 66/77] Add a user guide. Fixes #543 --- README.md | 1 + docs/src/SUMMARY.md | 1 + docs/src/about_guide.md | 28 ++++++++++++++++++++++++++++ ui/src/utils.ts | 2 +- 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 docs/src/about_guide.md diff --git a/README.md b/README.md index b55d678fc..a18b210b2 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,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. diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index c2df6223e..0514cbcda 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -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) diff --git a/docs/src/about_guide.md b/docs/src/about_guide.md new file mode 100644 index 000000000..426d659a0 --- /dev/null +++ b/docs/src/about_guide.md @@ -0,0 +1,28 @@ +# 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. + +## Markdown Guide + +Type | Or | … to Get +--- | --- | --- +\*Italic\* | \_Italic\_ | _Italic_ +\*\*Bold\*\* | \_\_Bold\_\_ | **Bold** +\# Heading 1 | Heading 1
    ========= | # Heading 1 +\## Heading 2 | Heading 2
    --------- | ## Heading 2 +\[Link\](http://a.com) | \[Link\]\[1\]

    \[1\]: http://b.org | [Link](https://commonmark.org/) +!\[Image\](http://url/a.png) | !\[Image\]\[1\]

    \[1\]: http://url/b.jpg | ![Markdown](https://commonmark.org/help/images/favicon.png) +\> Blockquote | | > Blockquote +\* List
    \* List
    \* List | \- List
    \- List
    \- List
    | * List
    * List
    * List
    +1\. One
    2\. Two
    3\. Three | 1) One
    2) Two
    3) Three | 1. One
    2. Two
    3. Three +Horizontal Rule
    \--- | Horizontal Rule
    \*\*\* | Horizontal Rule
    * * * +\`Inline code\` with backticks | |`Inline code` with backticks +\`\`\`
    \# code block
    print '3 backticks or'
    print 'indent 4 spaces'
    \`\`\` | ····\# code block
    ····print '3 backticks or'
    ····print 'indent 4 spaces' | \# code block
    print '3 backticks or'
    print 'indent 4 spaces' +::: spoiler hidden stuff
    *a bunch of spoilers here*
    ::: | |
    hidden stuff

    a bunch of spoilers here

    + +[CommonMark Tutorial](https://commonmark.org/help/tutorial/) + diff --git a/ui/src/utils.ts b/ui/src/utils.ts index 329168d26..1d5615abb 100644 --- a/ui/src/utils.ts +++ b/ui/src/utils.ts @@ -42,7 +42,7 @@ import emojiShortName from 'emoji-short-name'; import Toastify from 'toastify-js'; export const repoUrl = 'https://github.com/dessalines/lemmy'; -export const markdownHelpUrl = 'https://commonmark.org/help/'; +export const markdownHelpUrl = '/docs/about_guide.html'; export const archiveUrl = 'https://archive.is'; export const postRefetchSeconds: number = 60 * 1000; From 8505329889943cc18b736e1cc442a09d1d9824d7 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 17 Feb 2020 20:54:45 -0500 Subject: [PATCH 67/77] Fixing about guide. --- docs/src/about_guide.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/src/about_guide.md b/docs/src/about_guide.md index 426d659a0..f22e201be 100644 --- a/docs/src/about_guide.md +++ b/docs/src/about_guide.md @@ -12,17 +12,17 @@ Type | Or | … to Get --- | --- | --- \*Italic\* | \_Italic\_ | _Italic_ \*\*Bold\*\* | \_\_Bold\_\_ | **Bold** -\# Heading 1 | Heading 1
    ========= | # Heading 1 -\## Heading 2 | Heading 2
    --------- | ## Heading 2 +\# Heading 1 | Heading 1
    ========= |

    Heading 1

    +\## Heading 2 | Heading 2
    --------- |
    Heading 2
    \[Link\](http://a.com) | \[Link\]\[1\]

    \[1\]: http://b.org | [Link](https://commonmark.org/) !\[Image\](http://url/a.png) | !\[Image\]\[1\]

    \[1\]: http://url/b.jpg | ![Markdown](https://commonmark.org/help/images/favicon.png) -\> Blockquote | | > Blockquote +\> Blockquote | |
    Blockquote
    \* List
    \* List
    \* List | \- List
    \- List
    \- List
    | * List
    * List
    * List
    1\. One
    2\. Two
    3\. Three | 1) One
    2) Two
    3) Three | 1. One
    2. Two
    3. Three -Horizontal Rule
    \--- | Horizontal Rule
    \*\*\* | Horizontal Rule
    * * * +Horizontal Rule
    \--- | Horizontal Rule
    \*\*\* | Horizontal Rule

    \`Inline code\` with backticks | |`Inline code` with backticks \`\`\`
    \# code block
    print '3 backticks or'
    print 'indent 4 spaces'
    \`\`\` | ····\# code block
    ····print '3 backticks or'
    ····print 'indent 4 spaces' | \# code block
    print '3 backticks or'
    print 'indent 4 spaces' -::: spoiler hidden stuff
    *a bunch of spoilers here*
    ::: | |
    hidden stuff

    a bunch of spoilers here

    +::: spoiler hidden or nsfw stuff
    *a bunch of spoilers here*
    ::: | |
    hidden or nsfw stuff

    a bunch of spoilers here

    [CommonMark Tutorial](https://commonmark.org/help/tutorial/) From 81f1aaf298793e0d58264a2b12c221733a74796b Mon Sep 17 00:00:00 2001 From: Dessalines Date: Tue, 18 Feb 2020 09:00:17 -0500 Subject: [PATCH 68/77] Fix iframely always refetching bug. --- ui/src/components/post-listing.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ui/src/components/post-listing.tsx b/ui/src/components/post-listing.tsx index 7b3d64798..50e1f30c9 100644 --- a/ui/src/components/post-listing.tsx +++ b/ui/src/components/post-listing.tsx @@ -101,11 +101,14 @@ export class PostListing extends Component { this.state.upvotes = nextProps.post.upvotes; this.state.downvotes = nextProps.post.downvotes; this.state.score = nextProps.post.score; - this.state.url = nextProps.post.url; - this.state.iframely = null; - if (nextProps.post.url) { - this.fetchIframely(); + if (nextProps.post.url !== this.state.url) { + this.state.url = nextProps.post.url; + if (this.state.url) { + this.fetchIframely(); + } else { + this.state.iframely = null; + } } this.setState(this.state); From 6ac491c58705458562d8d3af638975ab66a29d9f Mon Sep 17 00:00:00 2001 From: Andre Vallestero Date: Tue, 18 Feb 2020 10:18:08 -0500 Subject: [PATCH 69/77] Fixed incorrect keys --- ui/src/translations/fr.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/translations/fr.ts b/ui/src/translations/fr.ts index abc3332dd..f54f48a25 100644 --- a/ui/src/translations/fr.ts +++ b/ui/src/translations/fr.ts @@ -34,8 +34,8 @@ export const fr = { preview: 'prévisualiser', upload_image: 'envoyer une image', avatar: 'Avatar', - upload_avatars: 'Télécharger une avatar', - show_avatar: 'Afficher les avatars', + upload_avatar: 'Télécharger une avatar', + show_avatars: 'Afficher les avatars', formatting_help: 'aide au formattage', view_source: 'voir la source', unlock: 'débloquer', From beec263ae6013f61b7f8ac894e4dd310ceef474e Mon Sep 17 00:00:00 2001 From: Dessalines Date: Tue, 18 Feb 2020 12:37:35 -0500 Subject: [PATCH 70/77] Version v0.6.23 --- ansible/VERSION | 2 +- docker/prod/docker-compose.yml | 2 +- server/src/version.rs | 2 +- ui/src/version.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ansible/VERSION b/ansible/VERSION index 635026fb8..d44996fff 100644 --- a/ansible/VERSION +++ b/ansible/VERSION @@ -1 +1 @@ -v0.6.22 +v0.6.23 diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml index 2ada25123..382ecce1c 100644 --- a/docker/prod/docker-compose.yml +++ b/docker/prod/docker-compose.yml @@ -11,7 +11,7 @@ services: - lemmy_db:/var/lib/postgresql/data restart: always lemmy: - image: dessalines/lemmy:v0.6.22 + image: dessalines/lemmy:v0.6.23 ports: - "127.0.0.1:8536:8536" restart: always diff --git a/server/src/version.rs b/server/src/version.rs index e4fc27d2a..0a2225c16 100644 --- a/server/src/version.rs +++ b/server/src/version.rs @@ -1 +1 @@ -pub const VERSION: &str = "v0.6.22"; +pub const VERSION: &str = "v0.6.23"; diff --git a/ui/src/version.ts b/ui/src/version.ts index 0e9211a2b..7d9c8910c 100644 --- a/ui/src/version.ts +++ b/ui/src/version.ts @@ -1 +1 @@ -export const version: string = 'v0.6.22'; +export const version: string = 'v0.6.23'; From b147ca462c2bf79d43bc0e57c5a9102d17ffd90a Mon Sep 17 00:00:00 2001 From: Dessalines Date: Wed, 19 Feb 2020 13:35:15 -0500 Subject: [PATCH 71/77] Don't show post url if its local. --- README.md | 2 +- ui/src/components/post-listing.tsx | 33 ++++++++++++++++-------------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index a18b210b2..81eceb74a 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ fa | 71% | cross_post,cross_posted_to,subscribed_to_communities,trending_communi 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 | 100% | upload_avatar,show_avatars +fr | 100% | 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 diff --git a/ui/src/components/post-listing.tsx b/ui/src/components/post-listing.tsx index 50e1f30c9..c0ddedc18 100644 --- a/ui/src/components/post-listing.tsx +++ b/ui/src/components/post-listing.tsx @@ -210,21 +210,24 @@ export class PostListing extends Component { )} - {this.state.url && ( - -
    - {new URL(this.state.url).hostname} - - - - - - )} + {this.state.url && + !( + new URL(this.state.url).hostname == window.location.hostname + ) && ( + + + {new URL(this.state.url).hostname} + + + + + + )} {this.hasImage() && ( <> {!this.state.imageExpanded ? ( From 6cde97836b856e5abc857d06cfe43bab43004b99 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Wed, 19 Feb 2020 13:35:58 -0500 Subject: [PATCH 72/77] Version v0.6.24 --- ansible/VERSION | 2 +- docker/prod/docker-compose.yml | 2 +- server/src/version.rs | 2 +- ui/src/version.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ansible/VERSION b/ansible/VERSION index d44996fff..7a28637ab 100644 --- a/ansible/VERSION +++ b/ansible/VERSION @@ -1 +1 @@ -v0.6.23 +v0.6.24 diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml index 382ecce1c..20267236c 100644 --- a/docker/prod/docker-compose.yml +++ b/docker/prod/docker-compose.yml @@ -11,7 +11,7 @@ services: - lemmy_db:/var/lib/postgresql/data restart: always lemmy: - image: dessalines/lemmy:v0.6.23 + image: dessalines/lemmy:v0.6.24 ports: - "127.0.0.1:8536:8536" restart: always diff --git a/server/src/version.rs b/server/src/version.rs index 0a2225c16..67f9b75dc 100644 --- a/server/src/version.rs +++ b/server/src/version.rs @@ -1 +1 @@ -pub const VERSION: &str = "v0.6.23"; +pub const VERSION: &str = "v0.6.24"; diff --git a/ui/src/version.ts b/ui/src/version.ts index 7d9c8910c..545698d89 100644 --- a/ui/src/version.ts +++ b/ui/src/version.ts @@ -1 +1 @@ -export const version: string = 'v0.6.23'; +export const version: string = 'v0.6.24'; From c896e01b9e721e3b85818223fdf79622c4bb599f Mon Sep 17 00:00:00 2001 From: Dessalines Date: Fri, 21 Feb 2020 11:26:42 -0500 Subject: [PATCH 73/77] Adding a link overlay. Fixes #549 --- ui/assets/css/main.css | 10 +++++++++ ui/src/components/post-listing.tsx | 36 ++++++++++++++++++++---------- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/ui/assets/css/main.css b/ui/assets/css/main.css index e7784877b..000201773 100644 --- a/ui/assets/css/main.css +++ b/ui/assets/css/main.css @@ -187,3 +187,13 @@ hr { color: var(--white); border: unset; } + +.link-overlay { + position: absolute; + top: 0; + right: 0; + padding: 2px; + background: rgba(0,0,0,.4); + border-bottom-left-radius: 0.25rem !important; + border-top-right-radius: 0.25rem !important; +} diff --git a/ui/src/components/post-listing.tsx b/ui/src/components/post-listing.tsx index c0ddedc18..b3bde27fe 100644 --- a/ui/src/components/post-listing.tsx +++ b/ui/src/components/post-listing.tsx @@ -162,18 +162,30 @@ export class PostListing extends Component { )}
    {this.hasImage() && !this.state.imageExpanded && ( - - - +
    + + + + + + + + +
    )} {this.state.url && isVideo(this.state.url) && (
    {this.hasImage() && !this.state.imageExpanded && (
    - - - - + + + + + + + + + + +
    )} {this.state.url && isVideo(this.state.url) && ( @@ -263,10 +270,14 @@ export class PostListing extends Component { class="pointer" onClick={linkEvent(this, this.handleImageExpandClick)} > - + data={this.getImage()} + > + + + +
    diff --git a/ui/src/components/symbols.tsx b/ui/src/components/symbols.tsx index 0870efd71..559a97072 100644 --- a/ui/src/components/symbols.tsx +++ b/ui/src/components/symbols.tsx @@ -15,6 +15,12 @@ export class Symbols extends Component { xmlnsXlink="http://www.w3.org/1999/xlink" > + + image + + + + external-link From 2092cf3f4eac622a34b42ff6d4b94d467e79495a Mon Sep 17 00:00:00 2001 From: Dessalines Date: Thu, 27 Feb 2020 12:55:23 -0500 Subject: [PATCH 76/77] Use image thumbnails from pictshare. Fixes #555 --- ui/assets/css/main.css | 10 --- ui/package.json | 4 +- ui/src/components/post-listing.tsx | 108 +++++++++++++++++------------ ui/src/utils.ts | 10 +-- 4 files changed, 72 insertions(+), 60 deletions(-) diff --git a/ui/assets/css/main.css b/ui/assets/css/main.css index 704cddae5..048f687ec 100644 --- a/ui/assets/css/main.css +++ b/ui/assets/css/main.css @@ -188,16 +188,6 @@ hr { border: unset; } -.img-expand-overlay { - position: absolute; - top: 0; - right: 0; - padding: 2px; - 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; diff --git a/ui/package.json b/ui/package.json index 6d7ad7758..79756bc69 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,9 +1,9 @@ { "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", diff --git a/ui/src/components/post-listing.tsx b/ui/src/components/post-listing.tsx index d52df9d1c..56c1f0d9c 100644 --- a/ui/src/components/post-listing.tsx +++ b/ui/src/components/post-listing.tsx @@ -51,6 +51,7 @@ interface PostListingState { downvotes: number; url: string; iframely: FramelyData; + thumbnail: string; } interface PostListingProps { @@ -80,6 +81,7 @@ export class PostListing extends Component { downvotes: this.props.post.downvotes, url: this.props.post.url, iframely: null, + thumbnail: null, }; constructor(props: any, context: any) { @@ -92,6 +94,7 @@ export class PostListing extends Component { this.handleEditCancel = this.handleEditCancel.bind(this); if (this.state.url) { + this.setThumbnail(); this.fetchIframely(); } } @@ -105,9 +108,11 @@ export class PostListing extends Component { if (nextProps.post.url !== this.state.url) { this.state.url = nextProps.post.url; if (this.state.url) { + this.setThumbnail(); this.fetchIframely(); } else { this.state.iframely = null; + this.state.thumbnail = null; } } @@ -132,6 +137,18 @@ export class PostListing extends Component { ); } + imgThumbnail() { + let post = this.props.post; + return ( + + ); + } + listing() { let post = this.props.post; return ( @@ -161,37 +178,32 @@ export class PostListing extends Component { )}
    - {this.hasImage() && !this.state.imageExpanded && ( + {this.state.thumbnail && !this.state.imageExpanded && ( )} {this.state.url && isVideo(this.state.url) && ( @@ -247,7 +259,7 @@ export class PostListing extends Component { )} - {this.hasImage() && ( + {this.state.thumbnail && ( <> {!this.state.imageExpanded ? ( { > @@ -795,29 +807,39 @@ export class PostListing extends Component { .then(res => { this.state.iframely = res; this.setState(this.state); + + // Store and fetch the image in pictshare + if ( + this.state.iframely.thumbnail_url && + isImage(this.state.iframely.thumbnail_url) + ) { + fetch( + `/pictshare/api/geturl.php?url=${this.state.iframely.thumbnail_url}` + ) + .then(res => res.json()) + .then(res => { + let url = `${window.location.origin}/pictshare/${res.url}`; + if (res.filetype == 'mp4') { + url += '/raw'; + } + this.state.thumbnail = url; + this.setState(this.state); + }); + } }) .catch(error => { console.error(`Iframely service not set up properly. ${error}`); }); } - hasImage(): boolean { - return ( - (this.state.url && isImage(this.state.url)) || - (this.state.iframely && this.state.iframely.thumbnail_url !== undefined) - ); - } - - getImage(): string { + setThumbnail() { let simpleImg = isImage(this.state.url); if (simpleImg) { - return this.state.url; - } else if (this.state.iframely) { - let iframelyThumbnail = this.state.iframely.thumbnail_url; - if (iframelyThumbnail) { - return iframelyThumbnail; - } + this.state.thumbnail = this.state.url; + } else { + this.state.thumbnail = null; } + this.setState(this.state); } handlePostLike(i: PostListing) { diff --git a/ui/src/utils.ts b/ui/src/utils.ts index 1d5615abb..d329dcd58 100644 --- a/ui/src/utils.ts +++ b/ui/src/utils.ts @@ -220,9 +220,9 @@ export function routeSearchTypeToEnum(type: string): SearchType { } export async function getPageTitle(url: string) { - let res = await fetch(`https://textance.herokuapp.com/title/${url}`); - let data = await res.text(); - return data; + let res = await fetch(`/iframely/oembed?url=${url}`).then(res => res.json()); + let title = await res.title; + return title; } export function debounce( @@ -386,7 +386,7 @@ export function objectFlip(obj: any) { export function pictshareAvatarThumbnail(src: string): string { // sample url: http://localhost:8535/pictshare/gs7xuu.jpg let split = src.split('pictshare'); - let out = `${split[0]}pictshare/96x96${split[1]}`; + let out = `${split[0]}pictshare/96${split[1]}`; return out; } @@ -401,7 +401,7 @@ export function showAvatars(): boolean { export function imageThumbnailer(url: string): string { let split = url.split('pictshare'); if (split.length > 1) { - let out = `${split[0]}pictshare/192x192${split[1]}`; + let out = `${split[0]}pictshare/192${split[1]}`; return out; } else { return url; From 6e6cfa9cf8eca1123bdda1f54d6dc8ddb871fc47 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Thu, 27 Feb 2020 13:16:30 -0500 Subject: [PATCH 77/77] Version v0.6.25 --- ansible/VERSION | 2 +- docker/prod/docker-compose.yml | 2 +- server/src/version.rs | 2 +- ui/src/version.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ansible/VERSION b/ansible/VERSION index 7a28637ab..6095ec4eb 100644 --- a/ansible/VERSION +++ b/ansible/VERSION @@ -1 +1 @@ -v0.6.24 +v0.6.25 diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml index 20267236c..ac46ece30 100644 --- a/docker/prod/docker-compose.yml +++ b/docker/prod/docker-compose.yml @@ -11,7 +11,7 @@ services: - lemmy_db:/var/lib/postgresql/data restart: always lemmy: - image: dessalines/lemmy:v0.6.24 + image: dessalines/lemmy:v0.6.25 ports: - "127.0.0.1:8536:8536" restart: always diff --git a/server/src/version.rs b/server/src/version.rs index 67f9b75dc..e504bd466 100644 --- a/server/src/version.rs +++ b/server/src/version.rs @@ -1 +1 @@ -pub const VERSION: &str = "v0.6.24"; +pub const VERSION: &str = "v0.6.25"; diff --git a/ui/src/version.ts b/ui/src/version.ts index 545698d89..78b06fe35 100644 --- a/ui/src/version.ts +++ b/ui/src/version.ts @@ -1 +1 @@ -export const version: string = 'v0.6.24'; +export const version: string = 'v0.6.25';