Changing federated community and user links and searching.

- Search: !community@instance, @user@instance
- Representation: community@instance, user@instance
This commit is contained in:
Dessalines 2020-04-30 11:45:12 -04:00
parent ee4f923f60
commit 38cdfdf7e0
9 changed files with 57 additions and 27 deletions

2
README.md vendored
View File

@ -73,7 +73,7 @@ Each lemmy server can set its own moderation policy; appointing site-wide admins
- Full vote scores `(+/-)` like old reddit. - Full vote scores `(+/-)` like old reddit.
- Themes, including light, dark, and solarized. - Themes, including light, dark, and solarized.
- Emojis with autocomplete support. Start typing `:` - Emojis with autocomplete support. Start typing `:`
- User tagging using `@`, Community tagging using `#`. - User tagging using `@`, Community tagging using `!`.
- Integrated image uploading in both posts and comments. - 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. - 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, on comment replies and when you're tagged.

View File

@ -3,7 +3,7 @@
Start typing... Start typing...
- `@a_user_name` to get a list of usernames. - `@a_user_name` to get a list of usernames.
- `#a_community` to get a list of communities. - `!a_community` to get a list of communities.
- `:emoji` to get a list of emojis. - `:emoji` to get a list of emojis.
## Sorting ## Sorting

View File

@ -45,14 +45,31 @@ pub enum SearchAcceptedObjects {
/// Attempt to parse the query as URL, and fetch an ActivityPub object from it. /// Attempt to parse the query as URL, and fetch an ActivityPub object from it.
/// ///
/// Some working examples for use with the docker/federation/ setup: /// Some working examples for use with the docker/federation/ setup:
/// http://lemmy_alpha:8540/c/main, or /c/main@lemmy_alpha:8540 /// http://lemmy_alpha:8540/c/main, or !main@lemmy_alpha:8540
/// http://lemmy_alpha:8540/u/lemmy_alpha, or /u/lemmy_alpha@lemmy_alpha:8540 /// http://lemmy_alpha:8540/u/lemmy_alpha, or @lemmy_alpha@lemmy_alpha:8540
/// http://lemmy_alpha:8540/p/3 /// http://lemmy_alpha:8540/p/3
pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result<SearchResponse, Error> { pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result<SearchResponse, Error> {
// Parse the shorthand query url // Parse the shorthand query url
let query_url = if query.contains('@') { let query_url = if query.contains('@') {
debug!("{}", query);
let split = query.split('@').collect::<Vec<&str>>(); let split = query.split('@').collect::<Vec<&str>>();
let url = format!("{}://{}{}", get_apub_protocol_string(), split[1], split[0]);
// User type will look like ['', username, instance]
// Community will look like [!community, instance]
let (name, instance) = if split.len() == 3 {
(format!("/u/{}", split[1]), split[2])
} else if split.len() == 2 {
if split[0].contains('!') {
let split2 = split[0].split('!').collect::<Vec<&str>>();
(format!("/c/{}", split2[1]), split[1])
} else {
return Err(format_err!("Invalid search query: {}", query));
}
} else {
return Err(format_err!("Invalid search query: {}", query));
};
let url = format!("{}://{}{}", get_apub_protocol_string(), instance, name);
Url::parse(&url)? Url::parse(&url)?
} else { } else {
Url::parse(&query)? Url::parse(&query)?

View File

@ -96,7 +96,7 @@ describe('main', () => {
test('/u/lemmy_alpha follows and accepts lemmy_beta/c/main', async () => { test('/u/lemmy_alpha follows and accepts lemmy_beta/c/main', async () => {
// Make sure lemmy_beta/c/main is cached on lemmy_alpha // Make sure lemmy_beta/c/main is cached on lemmy_alpha
// Use short-hand search url // Use short-hand search url
let searchUrl = `${lemmyAlphaApiUrl}/search?q=/c/main@lemmy_beta:8550&type_=All&sort=TopAll`; let searchUrl = `${lemmyAlphaApiUrl}/search?q=!main@lemmy_beta:8550&type_=All&sort=TopAll`;
let searchResponse: SearchResponse = await fetch(searchUrl, { let searchResponse: SearchResponse = await fetch(searchUrl, {
method: 'GET', method: 'GET',

View File

@ -12,6 +12,7 @@ interface CommunityOther {
interface CommunityLinkProps { interface CommunityLinkProps {
community: Community | CommunityOther; community: Community | CommunityOther;
realLink?: boolean;
} }
export class CommunityLink extends Component<CommunityLinkProps, any> { export class CommunityLink extends Component<CommunityLinkProps, any> {
@ -27,8 +28,10 @@ export class CommunityLink extends Component<CommunityLinkProps, any> {
name_ = community.name; name_ = community.name;
link = `/c/${community.name}`; link = `/c/${community.name}`;
} else { } else {
name_ = `${hostname(community.actor_id)}/${community.name}`; name_ = `${community.name}@${hostname(community.actor_id)}`;
link = `/community/${community.id}`; link = !this.props.realLink
? `/community/${community.id}`
: community.actor_id;
} }
return <Link to={link}>{name_}</Link>; return <Link to={link}>{name_}</Link>;
} }

View File

@ -11,6 +11,7 @@ import { WebSocketService, UserService } from '../services';
import { mdToHtml, getUnixTime, hostname } from '../utils'; import { mdToHtml, getUnixTime, hostname } from '../utils';
import { CommunityForm } from './community-form'; import { CommunityForm } from './community-form';
import { UserListing } from './user-listing'; import { UserListing } from './user-listing';
import { CommunityLink } from './community-link';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
interface SidebarProps { interface SidebarProps {
@ -66,7 +67,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
name_ = community.name; name_ = community.name;
link = `/c/${community.name}`; link = `/c/${community.name}`;
} else { } else {
name_ = `${hostname(community.actor_id)}/${community.name}`; name_ = `${community.name}@${hostname(community.actor_id)}`;
link = community.actor_id; link = community.actor_id;
} }
return ( return (
@ -86,15 +87,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
</small> </small>
)} )}
</h5> </h5>
{community.local ? ( <CommunityLink community={community} realLink />
<Link className="text-muted" to={link}>
{name_}
</Link>
) : (
<a className="text-muted" href={link} target="_blank">
{name_}
</a>
)}
<ul class="list-inline mb-1 text-muted font-weight-bold"> <ul class="list-inline mb-1 text-muted font-weight-bold">
{this.canMod && ( {this.canMod && (
<> <>

View File

@ -13,6 +13,7 @@ interface UserOther {
interface UserListingProps { interface UserListingProps {
user: UserView | UserOther; user: UserView | UserOther;
realLink?: boolean;
} }
export class UserListing extends Component<UserListingProps, any> { export class UserListing extends Component<UserListingProps, any> {
@ -29,8 +30,8 @@ export class UserListing extends Component<UserListingProps, any> {
name_ = user.name; name_ = user.name;
link = `/u/${user.name}`; link = `/u/${user.name}`;
} else { } else {
name_ = `${hostname(user.actor_id)}/${user.name}`; name_ = `${user.name}@${hostname(user.actor_id)}`;
link = `/user/${user.id}`; link = !this.props.realLink ? `/user/${user.id}` : user.actor_id;
} }
return ( return (

View File

@ -402,7 +402,7 @@ export class User extends Component<any, UserState> {
<h5> <h5>
<ul class="list-inline mb-0"> <ul class="list-inline mb-0">
<li className="list-inline-item"> <li className="list-inline-item">
<UserListing user={user} /> <UserListing user={user} realLink />
</li> </li>
{user.banned && ( {user.banned && (
<li className="list-inline-item badge badge-danger"> <li className="list-inline-item badge badge-danger">

28
ui/src/utils.ts vendored
View File

@ -501,7 +501,10 @@ export function setupTribute(): Tribute {
{ {
trigger: '@', trigger: '@',
selectTemplate: (item: any) => { selectTemplate: (item: any) => {
return `[/u/${item.original.key}](/u/${item.original.key})`; let link = item.original.local
? `[@${item.original.key}](/u/${item.original.key})`
: `[@${item.original.key}](/user/${item.original.id})`;
return link;
}, },
values: (text: string, cb: any) => { values: (text: string, cb: any) => {
userSearch(text, (users: any) => cb(users)); userSearch(text, (users: any) => cb(users));
@ -514,9 +517,12 @@ export function setupTribute(): Tribute {
// Communities // Communities
{ {
trigger: '#', trigger: '!',
selectTemplate: (item: any) => { selectTemplate: (item: any) => {
return `[/c/${item.original.key}](/c/${item.original.key})`; let link = item.original.local
? `[!${item.original.key}](/c/${item.original.key})`
: `[!${item.original.key}](/community/${item.original.id})`;
return link;
}, },
values: (text: string, cb: any) => { values: (text: string, cb: any) => {
communitySearch(text, (communities: any) => cb(communities)); communitySearch(text, (communities: any) => cb(communities));
@ -559,7 +565,12 @@ function userSearch(text: string, cb: any) {
if (res.op == UserOperation.Search) { if (res.op == UserOperation.Search) {
let data = res.data as SearchResponse; let data = res.data as SearchResponse;
let users = data.users.map(u => { let users = data.users.map(u => {
return { key: u.name }; let name_ = u.local ? u.name : `${u.name}@${hostname(u.actor_id)}`;
return {
key: name_,
local: u.local,
id: u.id,
};
}); });
cb(users); cb(users);
this.userSub.unsubscribe(); this.userSub.unsubscribe();
@ -590,8 +601,13 @@ function communitySearch(text: string, cb: any) {
let res = wsJsonToRes(msg); let res = wsJsonToRes(msg);
if (res.op == UserOperation.Search) { if (res.op == UserOperation.Search) {
let data = res.data as SearchResponse; let data = res.data as SearchResponse;
let communities = data.communities.map(u => { let communities = data.communities.map(c => {
return { key: u.name }; let name_ = c.local ? c.name : `${c.name}@${hostname(c.actor_id)}`;
return {
key: name_,
local: c.local,
id: c.id,
};
}); });
cb(communities); cb(communities);
this.communitySub.unsubscribe(); this.communitySub.unsubscribe();