Skip to content

Commit

Permalink
Merge pull request #253 from podStation/enhancement#233__reply-to-com…
Browse files Browse the repository at this point in the history
…ments

Enhancements #240, #233 Comments reply and other actions
  • Loading branch information
daveajones authored May 11, 2023
2 parents 0ad4ff4 + 851afe1 commit 32b2bf5
Show file tree
Hide file tree
Showing 5 changed files with 287 additions and 4 deletions.
20 changes: 20 additions & 0 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,26 @@ function writeThreadcapChunk(processedNodeId, threadcap, sentCommenters, res) {
res.write(JSON.stringify(threadcapChunk) + '\n')
}

// ---------------------------------------------------------
// --------- API to get remote interact url for comments ---
// ---------------------------------------------------------
app.use('/api/comments/remoteInteractUrlPattern', async (req, res) => {
const interactorAccount = req.query.interactorAccount;

console.log('Debug interactorAccount', interactorAccount);

const interactorInstanceHost = interactorAccount.split('@')[1];

const response = await fetch(`https://${interactorInstanceHost}/.well-known/webfinger?` + new URLSearchParams({resource:`acct:${interactorAccount}`}));
const parsedResponse = await response.json();

const linkOStatusSubscribe = parsedResponse.links.find((link) => link.rel === 'http://ostatus.org/schema/1.0/subscribe');

res.send({
remoteInteractUrlPattern: linkOStatusSubscribe.template
})
})

// ------------------------------------------------
// ---------- Static files for API data -----------
// ------------------------------------------------
Expand Down
129 changes: 129 additions & 0 deletions ui/src/components/CommentMenu/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import * as React from 'react'

import './styles.scss'

interface ICommentMenuProps {
/**
* URL of the comment
*/
url: string,
commenterUrl: string,
commenterAccount: string,
}

interface ICommentMenuState {
interactorAccount?: string,
}

class CommentMenu extends React.PureComponent<ICommentMenuProps, ICommentMenuState> {
constructor(props) {
super(props);
this.state = {
interactorAccount: localStorage.getItem('commentsInteractorAccount')
}
}

onClickCopyLink(e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) {
e.preventDefault();
navigator.clipboard.writeText(this.props.url);

// Some form of "toast message" would be better, but I don't
// think something like that is already implemented, meanwhile,
// we use an alert, it is ugly, but it show the user there was
// a reaction.
alert('Link to post copied');
}

async onClickReply(e: React.MouseEvent<HTMLAnchorElement, MouseEvent>): Promise<void> {
e.preventDefault();

const remoteInteractUrlPattern = await this.fetchRemoteInteractUrlPattern();

if(!remoteInteractUrlPattern) {
// TODO: error handling
}

// alert(remoteInteractUrlPattern);
const remoteInteractUrl = remoteInteractUrlPattern.replace('{uri}', encodeURI(this.props.url));
window.open(remoteInteractUrl, '_blank');
}

async onClickFollow(e: React.MouseEvent<HTMLAnchorElement, MouseEvent>): Promise<void> {
e.preventDefault();

const remoteInteractUrlPattern = await this.fetchRemoteInteractUrlPattern();

if(!remoteInteractUrlPattern) {
// TODO: error handling
}

// alert(remoteInteractUrlPattern);
const remoteInteractUrl = remoteInteractUrlPattern.replace('{uri}', encodeURI(CommentMenu.stripLeadingAt(this.props.commenterAccount)));
window.open(remoteInteractUrl, '_blank');
}

async fetchRemoteInteractUrlPattern(): Promise<string> {
let interactorAccount = localStorage.getItem('commentsInteractorAccount');

if(!interactorAccount) {
interactorAccount = prompt('What is your Fediverse account?');

if(interactorAccount) {
localStorage.setItem('commentsInteractorAccount', interactorAccount);
}
}

if(!interactorAccount) {
return;
}

let remoteInteractUrlPattern = localStorage.getItem('commentsRemoteInteractUrlPattern');

if(!remoteInteractUrlPattern) {
const response = await fetch('/api/comments/remoteInteractUrlPattern?' + new URLSearchParams({
interactorAccount: interactorAccount
}));

const parsedResponse = await response.json();

remoteInteractUrlPattern = parsedResponse.remoteInteractUrlPattern;

if(remoteInteractUrlPattern) {
localStorage.setItem('commentsRemoteInteractUrlPattern', remoteInteractUrlPattern);
}
}

return remoteInteractUrlPattern;
}

static stripLeadingAt(account: string) {
return account.substring(1);
}

onClickForgetHomeInstanceHost(e: React.MouseEvent<HTMLAnchorElement, MouseEvent>): void {
e.preventDefault();

localStorage.removeItem('commentsInteractorAccount');
localStorage.removeItem('commentsRemoteInteractUrlPattern');

this.setState({
interactorAccount: undefined
});
}

render(): React.ReactNode {
return (
<menu>
<a href={this.props.url} target='_blank' onClick={(e) => this.onClickReply(e)}>Reply to this post</a>
<a href={this.props.url} onClick={(e) => this.onClickCopyLink(e)}>Copy link to this post</a>
<a href={this.props.url} target='_blank'>Open in original site</a>
<a href={this.props.commenterUrl} target='_blank' onClick={(e) => this.onClickFollow(e)}>Follow {this.props.commenterAccount}</a>
{this.state.interactorAccount &&
<a onClick={(e) => this.onClickForgetHomeInstanceHost(e)}>Forget {this.state.interactorAccount}</a>
}
</menu>
)
}
}

export default CommentMenu;
84 changes: 84 additions & 0 deletions ui/src/components/CommentMenu/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
.context-menu {
all: unset;
position: relative;
}

.context-menu svg {
display: block;
}

.context-menu menu {
position: absolute;
top: 100%;
right: 0;
display: flex;
flex-direction: column;
background: var(--button-background);
margin: 0;
padding: .75rem;
z-index: 1;
border-radius: .25rem;
box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.25);
}

.context-menu menu a {
white-space: nowrap;
text-decoration: none;
line-height: 2;
}

.dialog-homeinstance {
border: 1px solid rgba(0, 0, 0, 0.25);
border-radius: 0.5rem;
box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.25);
width: calc((100% - 6px) - 2em);
max-width: 28rem;
}

.dialog-homeinstance form {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin: 0;
}

.dialog-homeinstance input {
padding: 0.5rem;
}

.dialog-homeinstance menu {
display: flex;
flex-wrap: wrap;
flex-direction: row;
justify-content: flex-end;
gap: .5rem;
margin: 1.5rem 0 0 0;
}

.dialog-homeinstance button {
border: none;
padding: .5rem;
border-radius: 4px;
min-width: 4.5rem;
background-color: #2962ff;
font-size: 0.875rem;
font-weight: 500;
letter-spacing: .25px;
line-height: 1rem;
outline: none;
color: #FFF;
}

.dialog-homeinstance button:hover {
background-color: #2F7DE2;
}

.dialog-homeinstance button[type="reset"] {
background-color: transparent;
box-shadow: inset 0 0 0 1px #DADCE0;
color: #2962ff;
}

.dialog-homeinstance button[type="reset"]:hover {
background-color: #EAF2FD;
}
56 changes: 54 additions & 2 deletions ui/src/components/Comments/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import DOMPurify from 'dompurify'
import Button from '../Button'

import './styles.scss'
import CommentMenu from '../CommentMenu'

interface IProps {
id: number,
Expand Down Expand Up @@ -37,9 +38,47 @@ interface ICommentProps {
comment: StateComment
}

class Comment extends React.PureComponent<ICommentProps> {
interface ICommentState {
showMenu: boolean
}

class Comment extends React.PureComponent<ICommentProps, ICommentState> {
menuWrapperRef: React.Ref<HTMLDivElement> = React.createRef();
boundHandleMouseDown: (this: Document, ev: MouseEvent) => any;

constructor(props) {
super(props);
this.state = {
showMenu: false
}

this.boundHandleMouseDown = this.handleMouseDown.bind(this);
}

onClickShowMenu() {
this.setState({showMenu: !this.state.showMenu});
}

componentDidMount() {
document.addEventListener("mousedown", this.boundHandleMouseDown);
}

componentWillUnmount() {
document.removeEventListener("mousedown", this.boundHandleMouseDown);
}

handleMouseDown(event: MouseEvent) {
this.closeMenuOnClickOutside(event);
}

closeMenuOnClickOutside(event: MouseEvent) {
// @ts-ignore
if (this.menuWrapperRef?.current && !this.menuWrapperRef.current.contains(event.target)) {

this.setState({
showMenu: false,
})
}
}

render(): React.ReactNode {
Expand All @@ -54,10 +93,23 @@ class Comment extends React.PureComponent<ICommentProps> {
<span className='handle'>{this.props.comment.attributedTo.account}</span>
</div>
</a>
<span aria-hidden="true">·</span>
<a href={this.props.comment.url} className='permalink'>
<time>{this.props.comment.publishedAt.toLocaleString()}</time>
</a>
<button className="context-menu" aria-label="More" onClick={() => this.onClickShowMenu()}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" width="24" height="24" strokeWidth="1.5" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M6.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM12.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM18.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0z" />
</svg>
{ this.state.showMenu &&
<div ref={this.menuWrapperRef}>
<CommentMenu
url={this.props.comment.url}
commenterUrl={this.props.comment.attributedTo.url}
commenterAccount={this.props.comment.attributedTo.account}
/>
</div>
}
</button>
</summary>
}
{ this.props.comment.summary ?
Expand Down
2 changes: 0 additions & 2 deletions ui/src/components/Comments/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@
font-size: .75rem;
line-height: 1rem;
width: 100%;
overflow: hidden;
white-space: nowrap;
}

details > summary::-webkit-details-marker {
Expand Down

0 comments on commit 32b2bf5

Please sign in to comment.