]> Untitled Git - lemmy.git/commitdiff
Adding Community and Site transfer
authorDessalines <tyhou13@gmx.com>
Sat, 24 Aug 2019 02:40:41 +0000 (19:40 -0700)
committerDessalines <tyhou13@gmx.com>
Sat, 24 Aug 2019 02:40:41 +0000 (19:40 -0700)
- Fixes #139

14 files changed:
README.md
server/src/api/community.rs
server/src/api/mod.rs
server/src/api/post.rs
server/src/api/site.rs
server/src/api/user.rs
server/src/db/comment.rs
server/src/db/community.rs
server/src/websocket/server.rs
ui/src/components/comment-node.tsx
ui/src/components/post.tsx
ui/src/interfaces.ts
ui/src/services/WebSocketService.ts
ui/src/translations/en.ts

index 915df5e87c3c1dd590046827bbbf8ef2a18f725d..7830991f84f3c7b5181d673985c6bc711fd32760 100644 (file)
--- a/README.md
+++ b/README.md
@@ -39,6 +39,8 @@ Front Page|Post
 - Clean, mobile-friendly interface.
 - i18n / internationalization support.
 - NSFW post / community support.
+- Cross-posting support.
+- Can transfer site and communities to others.
 - High performance.
   - Server is written in rust.
   - Front end is `~80kB` gzipped.
@@ -161,7 +163,7 @@ Lemmy is free, open-source software, meaning no advertising, monetizing, or vent
 
 If you'd like to add translations, take a look a look at the [english translation file](ui/src/translations/en.ts).
 
-- Languages supported: English (`en`), Chinese (`zh`), French (`fr`), Swedish (`sv`), German (`de`), Russian (`ru`).
+- Languages supported: English (`en`), Chinese (`zh`), French (`fr`), Spanish (`es`), Swedish (`sv`), German (`de`), Russian (`ru`).
 
 ## Credits
 
index 7405848882108537f3ad79e4e7c3c43bb6abf462..37bc20dbf6b836fadf588823ae0958eae45c2163 100644 (file)
@@ -111,6 +111,13 @@ pub struct GetFollowedCommunitiesResponse {
   communities: Vec<CommunityFollowerView>
 }
 
+#[derive(Serialize, Deserialize)]
+pub struct TransferCommunity {
+  community_id: i32,
+  user_id: i32,
+  auth: String
+}
+
 impl Perform<GetCommunityResponse> for Oper<GetCommunity> {
   fn perform(&self) -> Result<GetCommunityResponse, Error> {
     let data: &GetCommunity = &self.data;
@@ -148,7 +155,11 @@ impl Perform<GetCommunityResponse> for Oper<GetCommunity> {
       }
     };
 
-    let admins = UserView::admins(&conn)?;
+    let site_creator_id = Site::read(&conn, 1)?.creator_id;
+    let mut admins = UserView::admins(&conn)?;
+    let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
+    let creator_user = admins.remove(creator_index);
+    admins.insert(0, creator_user);
 
     // Return the jwt
     Ok(
@@ -577,3 +588,107 @@ impl Perform<AddModToCommunityResponse> for Oper<AddModToCommunity> {
       )
   }
 }
+  
+impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
+  fn perform(&self) -> Result<GetCommunityResponse, Error> {
+    let data: &TransferCommunity = &self.data;
+    let conn = establish_connection();
+
+    let claims = match Claims::decode(&data.auth) {
+      Ok(claims) => claims.claims,
+      Err(_e) => {
+        return Err(APIError::err(&self.op, "not_logged_in"))?
+      }
+    };
+
+    let user_id = claims.id;
+
+    let read_community = Community::read(&conn, data.community_id)?;
+
+    // Make sure user is the creator
+    if read_community.creator_id != user_id {
+      return Err(APIError::err(&self.op, "not_an_admin"))?
+    }
+    
+    let community_form = CommunityForm {
+      name: read_community.name,
+      title: read_community.title,
+      description: read_community.description,
+      category_id: read_community.category_id,
+      creator_id: data.user_id,
+      removed: None,
+      deleted: None,
+      nsfw: read_community.nsfw,
+      updated: Some(naive_now())
+    };
+
+    let _updated_community = match Community::update(&conn, data.community_id, &community_form) {
+      Ok(community) => community,
+      Err(_e) => {
+        return Err(APIError::err(&self.op, "couldnt_update_community"))?
+      }
+    };
+
+    // You also have to re-do the community_moderator table, reordering it.
+    let mut community_mods = CommunityModeratorView::for_community(&conn, data.community_id)?;
+    let creator_index = community_mods.iter().position(|r| r.user_id == data.user_id).unwrap();
+    let creator_user = community_mods.remove(creator_index);
+    community_mods.insert(0, creator_user);
+
+    CommunityModerator::delete_for_community(&conn, data.community_id)?;
+
+    for cmod in &community_mods {
+
+      let community_moderator_form = CommunityModeratorForm {
+        community_id: cmod.community_id,
+        user_id: cmod.user_id
+      };
+
+      let _inserted_community_moderator = match CommunityModerator::join(&conn, &community_moderator_form) {
+        Ok(user) => user,
+        Err(_e) => {
+          return Err(APIError::err(&self.op, "community_moderator_already_exists"))?
+        }
+      };
+    }
+
+    // Mod tables
+    let form = ModAddCommunityForm {
+      mod_user_id: user_id,
+      other_user_id: data.user_id,
+      community_id: data.community_id,
+      removed: Some(false),
+    };
+    ModAddCommunity::create(&conn, &form)?;
+
+    let community_view = match CommunityView::read(&conn, data.community_id, Some(user_id)) {
+      Ok(community) => community,
+      Err(_e) => {
+        return Err(APIError::err(&self.op, "couldnt_find_community"))?
+      }
+    };
+
+    let moderators = match CommunityModeratorView::for_community(&conn, data.community_id) {
+      Ok(moderators) => moderators,
+      Err(_e) => {
+        return Err(APIError::err(&self.op, "couldnt_find_community"))?
+      }
+    };
+
+    let site_creator_id = Site::read(&conn, 1)?.creator_id;
+    let mut admins = UserView::admins(&conn)?;
+    let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
+    let creator_user = admins.remove(creator_index);
+    admins.insert(0, creator_user);
+
+    // Return the jwt
+    Ok(
+      GetCommunityResponse {
+        op: self.op.to_string(),
+        community: community_view,
+        moderators: moderators,
+        admins: admins,
+      }
+      )
+  }
+}
index 3a4a08658a0dc31e32d95aa4ee2d86b827fd06b4..ac11d30cceb77c886c5727449e700aa0f6f93bf1 100644 (file)
@@ -22,7 +22,7 @@ pub mod site;
 
 #[derive(EnumString,ToString,Debug)]
 pub enum UserOperation {
-  Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetReplies, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser, Search, MarkAllAsRead, SaveUserSettings
+  Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetReplies, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser, Search, MarkAllAsRead, SaveUserSettings, TransferCommunity, TransferSite
 }
 
 #[derive(Fail, Debug)]
index c5985f467e14fd4c5c2bba60b878175ecc5814d2..8fc24ac1858fc6f171757874c552a5f627a71fd2 100644 (file)
@@ -200,7 +200,11 @@ impl Perform<GetPostResponse> for Oper<GetPost> {
 
     let moderators = CommunityModeratorView::for_community(&conn, post_view.community_id)?;
 
-    let admins = UserView::admins(&conn)?;
+    let site_creator_id = Site::read(&conn, 1)?.creator_id;
+    let mut admins = UserView::admins(&conn)?;
+    let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
+    let creator_user = admins.remove(creator_index);
+    admins.insert(0, creator_user);
 
     // Return the jwt
     Ok(
index c98539bee8222a509194f428514f8f2beca6309c..7827913b970c7563c584269f26d3eba4c8b445b1 100644 (file)
@@ -83,6 +83,12 @@ pub struct GetSiteResponse {
   banned: Vec<UserView>,
 }
 
+#[derive(Serialize, Deserialize)]
+pub struct TransferSite {
+  user_id: i32,
+  auth: String
+}
+
 impl Perform<ListCategoriesResponse> for Oper<ListCategories> {
   fn perform(&self) -> Result<ListCategoriesResponse, Error> {
     let _data: &ListCategories = &self.data;
@@ -251,7 +257,14 @@ impl Perform<GetSiteResponse> for Oper<GetSite> {
       Err(_e) => None
     };
 
-    let admins = UserView::admins(&conn)?;
+    let mut admins = UserView::admins(&conn)?;
+    if site_view.is_some() {
+      let site_creator_id = site_view.to_owned().unwrap().creator_id;
+      let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
+      let creator_user = admins.remove(creator_index);
+      admins.insert(0, creator_user);
+    }
+
     let banned = UserView::banned(&conn)?;
 
     Ok(
@@ -399,3 +412,68 @@ impl Perform<SearchResponse> for Oper<Search> {
       )
   }
 }
+
+impl Perform<GetSiteResponse> for Oper<TransferSite> {
+  fn perform(&self) -> Result<GetSiteResponse, Error> {
+    let data: &TransferSite = &self.data;
+    let conn = establish_connection();
+
+    let claims = match Claims::decode(&data.auth) {
+      Ok(claims) => claims.claims,
+      Err(_e) => {
+        return Err(APIError::err(&self.op, "not_logged_in"))?
+      }
+    };
+
+    let user_id = claims.id;
+
+    let read_site = Site::read(&conn, 1)?;
+
+    // Make sure user is the creator
+    if read_site.creator_id != user_id {
+      return Err(APIError::err(&self.op, "not_an_admin"))?
+    }
+
+    let site_form = SiteForm {
+      name: read_site.name,
+      description: read_site.description,
+      creator_id: data.user_id,
+      updated: Some(naive_now()),
+    };
+
+    match Site::update(&conn, 1, &site_form) {
+      Ok(site) => site,
+      Err(_e) => {
+        return Err(APIError::err(&self.op, "couldnt_update_site"))?
+      }
+    };
+
+    // Mod tables
+    let form = ModAddForm {
+      mod_user_id: user_id,
+      other_user_id: data.user_id,
+      removed: Some(false),
+    };
+
+    ModAdd::create(&conn, &form)?;
+
+    let site_view = SiteView::read(&conn)?;
+
+    let mut admins = UserView::admins(&conn)?;
+    let creator_index = admins.iter().position(|r| r.id == site_view.creator_id).unwrap();
+    let creator_user = admins.remove(creator_index);
+    admins.insert(0, creator_user);
+
+    let banned = UserView::banned(&conn)?;
+
+    Ok(
+      GetSiteResponse {
+        op: self.op.to_string(), 
+        site: Some(site_view),
+        admins: admins,
+        banned: banned,
+      }
+      )
+  }
+}
+
index 425cc1cbddb2f775b74693ae68ac0f72b8c2132e..d8610fa9a9b40d7bb3fe42bb87ad5ff6e03a6452 100644 (file)
@@ -432,7 +432,11 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
 
     ModAdd::create(&conn, &form)?;
 
-    let admins = UserView::admins(&conn)?;
+    let site_creator_id = Site::read(&conn, 1)?.creator_id;
+    let mut admins = UserView::admins(&conn)?;
+    let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
+    let creator_user = admins.remove(creator_index);
+    admins.insert(0, creator_user);
 
     Ok(
       AddAdminResponse {
index ce0b5a632a253e8fec48103e5f7537cc495a77af..7901357f04d43371e3fa68a4bb45d181b77f94f6 100644 (file)
@@ -103,9 +103,10 @@ impl Likeable <CommentLikeForm> for CommentLike {
   }
   fn remove(conn: &PgConnection, comment_like_form: &CommentLikeForm) -> Result<usize, Error> {
     use crate::schema::comment_like::dsl::*;
-    diesel::delete(comment_like
-                   .filter(comment_id.eq(comment_like_form.comment_id))
-                   .filter(user_id.eq(comment_like_form.user_id)))
+    diesel::delete(
+      comment_like
+      .filter(comment_id.eq(comment_like_form.comment_id))
+      .filter(user_id.eq(comment_like_form.user_id)))
       .execute(conn)
   }
 }
index dd6ea94b0171f306bc6cb2175d3c63e5ee20e4e2..e07b5c00306f00e97999b9ec0d4ce393efa5ad22 100644 (file)
@@ -101,6 +101,16 @@ impl Joinable<CommunityModeratorForm> for CommunityModerator {
   }
 }
 
+impl CommunityModerator {
+  pub fn delete_for_community(conn: &PgConnection, for_community_id: i32) -> Result<usize, Error> {
+    use crate::schema::community_moderator::dsl::*;
+    diesel::delete(
+      community_moderator
+      .filter(community_id.eq(for_community_id)))
+      .execute(conn)
+  }
+}
+
 #[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
 #[belongs_to(Community)]
 #[table_name = "community_user_ban"]
index c0dee2679188c49badc1bfe8069658281e94de36..b4cbce3af8c49b0aed296356cd3395d15f9491c4 100644 (file)
@@ -490,5 +490,15 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
       let res = Oper::new(user_operation, search).perform()?;
       Ok(serde_json::to_string(&res)?)
     },
+    UserOperation::TransferCommunity => {
+      let transfer_community: TransferCommunity = serde_json::from_str(data)?;
+      let res = Oper::new(user_operation, transfer_community).perform()?;
+      Ok(serde_json::to_string(&res)?)
+    },
+    UserOperation::TransferSite => {
+      let transfer_site: TransferSite = serde_json::from_str(data)?;
+      let res = Oper::new(user_operation, transfer_site).perform()?;
+      Ok(serde_json::to_string(&res)?)
+    },
   }
 }
index 610252eab236f2fedbc46f96990381dc5785695f..f518da904e0e8a1b1e080293fe558511470845c8 100644 (file)
@@ -1,6 +1,6 @@
 import { Component, linkEvent } from 'inferno';
 import { Link } from 'inferno-router';
-import { CommentNode as CommentNodeI, CommentLikeForm, CommentForm as CommentFormI, SaveCommentForm, BanFromCommunityForm, BanUserForm, CommunityUser, UserView, AddModToCommunityForm, AddAdminForm } from '../interfaces';
+import { CommentNode as CommentNodeI, CommentLikeForm, CommentForm as CommentFormI, SaveCommentForm, BanFromCommunityForm, BanUserForm, CommunityUser, UserView, AddModToCommunityForm, AddAdminForm, TransferCommunityForm, TransferSiteForm } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
 import { mdToHtml, getUnixTime, canMod, isMod } from '../utils';
 import * as moment from 'moment';
@@ -148,6 +148,12 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                         }
                       </>
                     }
+                    {/* Community creators can transfer community to another mod */}
+                    {this.amCommunityCreator && this.isMod &&
+                      <li className="list-inline-item">
+                        <span class="pointer" onClick={linkEvent(this, this.handleTransferCommunity)}><T i18nKey="transfer_community">#</T></span>
+                      </li>
+                    }
                     {/* Admins can ban from all, and appoint other admins */}
                     {this.canAdmin &&
                       <>
@@ -166,6 +172,12 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                         }
                       </>
                     }
+                    {/* Site Creator can transfer to another admin */}
+                    {this.amSiteCreator && this.isAdmin &&
+                      <li className="list-inline-item">
+                        <span class="pointer" onClick={linkEvent(this, this.handleTransferSite)}><T i18nKey="transfer_site">#</T></span>
+                      </li>
+                    }
                   </>
                 }
                 <li className="list-inline-item">
@@ -251,6 +263,20 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
     return this.props.admins && canMod(UserService.Instance.user, this.props.admins.map(a => a.id), this.props.node.comment.creator_id);
   }
 
+  get amCommunityCreator(): boolean {
+    return this.props.moderators && 
+      UserService.Instance.user && 
+      (this.props.node.comment.creator_id != UserService.Instance.user.id) &&
+      (UserService.Instance.user.id == this.props.moderators[0].user_id);
+  }
+
+  get amSiteCreator(): boolean {
+    return this.props.admins && 
+      UserService.Instance.user && 
+      (this.props.node.comment.creator_id != UserService.Instance.user.id) &&
+      (UserService.Instance.user.id == this.props.admins[0].id);
+  }
+
   handleReplyClick(i: CommentNode) {
     i.state.showReply = true;
     i.setState(i.state);
@@ -431,6 +457,23 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
     i.setState(i.state);
   }
 
+  handleTransferCommunity(i: CommentNode) {
+    let form: TransferCommunityForm = {
+      community_id: i.props.node.comment.community_id,
+      user_id: i.props.node.comment.creator_id,
+    };
+    WebSocketService.Instance.transferCommunity(form);
+    i.setState(i.state);
+  }
+
+  handleTransferSite(i: CommentNode) {
+    let form: TransferSiteForm = {
+      user_id: i.props.node.comment.creator_id,
+    };
+    WebSocketService.Instance.transferSite(form);
+    i.setState(i.state);
+  }
+
   get isCommentNew(): boolean {
     let now = moment.utc().subtract(10, 'minutes');
     let then = moment.utc(this.props.node.comment.published);
index ab82ca4fea138cec865ad150d3ae510ff7c470e3..a6df4105edc7db7bdbb5c053d94288a52da692bf 100644 (file)
@@ -1,7 +1,7 @@
 import { Component, linkEvent } from 'inferno';
 import { Subscription } from "rxjs";
 import { retryWhen, delay, take } from 'rxjs/operators';
-import { UserOperation, Community, Post as PostI, GetPostResponse, PostResponse, Comment, CommentForm as CommentFormI, CommentResponse, CommentSortType, CreatePostLikeResponse, CommunityUser, CommunityResponse, CommentNode as CommentNodeI, BanFromCommunityResponse, BanUserResponse, AddModToCommunityResponse, AddAdminResponse, UserView, SearchType, SortType, SearchForm, SearchResponse } from '../interfaces';
+import { UserOperation, Community, Post as PostI, GetPostResponse, PostResponse, Comment, CommentForm as CommentFormI, CommentResponse, CommentSortType, CreatePostLikeResponse, CommunityUser, CommunityResponse, CommentNode as CommentNodeI, BanFromCommunityResponse, BanUserResponse, AddModToCommunityResponse, AddAdminResponse, UserView, SearchType, SortType, SearchForm, SearchResponse, GetSiteResponse, GetCommunityResponse } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
 import { msgOp, hotRank } from '../utils';
 import { PostListing } from './post-listing';
@@ -370,6 +370,17 @@ export class Post extends Component<any, PostState> {
       let res: SearchResponse = msg;
       this.state.crossPosts = res.posts.filter(p => p.id != this.state.post.id);
       this.setState(this.state);
+    } else if (op == UserOperation.TransferSite) { 
+      let res: GetSiteResponse = msg;
+
+      this.state.admins = res.admins;
+      this.setState(this.state);
+    } else if (op == UserOperation.TransferCommunity) { 
+      let res: GetCommunityResponse = msg;
+      this.state.community = res.community;
+      this.state.moderators = res.moderators;
+      this.state.admins = res.admins;
+      this.setState(this.state);
     }
 
   }
index 0a3daf668b22238a9c4917009644f87e45b84f95..251b64a0dbf52b907c9560273c4dadfba9c02a7b 100644 (file)
@@ -1,5 +1,5 @@
 export enum UserOperation {
-  Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetReplies, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser, Search, MarkAllAsRead, SaveUserSettings
+  Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetReplies, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser, Search, MarkAllAsRead, SaveUserSettings, TransferCommunity, TransferSite
 }
 
 export enum CommentSortType {
@@ -202,6 +202,17 @@ export interface AddModToCommunityForm {
   auth?: string;
 }
 
+export interface TransferCommunityForm {
+  community_id: number;
+  user_id: number;
+  auth?: string;
+}
+
+export interface TransferSiteForm {
+  user_id: number;
+  auth?: string;
+}
+
 export interface AddModToCommunityResponse {
   op: string;
   moderators: Array<CommunityUser>;
index c34b6b3c15e0bde6bf28d0bd9e05c23cae95fe24..f67dbf6d94b82f11a4fcd2090a5d012b312b92f4 100644 (file)
@@ -1,5 +1,5 @@
 import { wsUri } from '../env';
-import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, SavePostForm, CommentForm, SaveCommentForm, CommentLikeForm, GetPostsForm, CreatePostLikeForm, FollowCommunityForm, GetUserDetailsForm, ListCommunitiesForm, GetModlogForm, BanFromCommunityForm, AddModToCommunityForm, AddAdminForm, BanUserForm, SiteForm, Site, UserView, GetRepliesForm, SearchForm, UserSettingsForm } from '../interfaces';
+import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, SavePostForm, CommentForm, SaveCommentForm, CommentLikeForm, GetPostsForm, CreatePostLikeForm, FollowCommunityForm, GetUserDetailsForm, ListCommunitiesForm, GetModlogForm, BanFromCommunityForm, AddModToCommunityForm, TransferCommunityForm, AddAdminForm, TransferSiteForm, BanUserForm, SiteForm, Site, UserView, GetRepliesForm, SearchForm, UserSettingsForm } from '../interfaces';
 import { webSocket } from 'rxjs/webSocket';
 import { Subject } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
@@ -136,6 +136,16 @@ export class WebSocketService {
     this.subject.next(this.wsSendWrapper(UserOperation.AddModToCommunity, form));
   }
 
+  public transferCommunity(form: TransferCommunityForm) {
+    this.setAuth(form);
+    this.subject.next(this.wsSendWrapper(UserOperation.TransferCommunity, form));
+  }
+
+  public transferSite(form: TransferSiteForm) {
+    this.setAuth(form);
+    this.subject.next(this.wsSendWrapper(UserOperation.TransferSite, form));
+  }
+  
   public banUser(form: BanUserForm) {
     this.setAuth(form);
     this.subject.next(this.wsSendWrapper(UserOperation.BanUser, form));
index ff38a84b2605e8f81d4251d55814cac65ee4efb5..c854ea7be85dbc9793f2e36c6042c22364bf874e 100644 (file)
@@ -130,6 +130,8 @@ export const en = {
     joined: 'Joined',
     by: 'by',
     to: 'to',
+    transfer_community: 'transfer community',
+    transfer_site: 'transfer site',
     powered_by: 'Powered by',
     landing_0: 'Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It\'s self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
     not_logged_in: 'Not logged in.',