refs #4653 Open link in browser

This commit is contained in:
AkiraFukushima 2023-12-03 00:52:51 +09:00
parent a5811bc96f
commit 6a9a198e59
No known key found for this signature in database
GPG Key ID: B6E51BAC4DE1A957
4 changed files with 158 additions and 5 deletions

View File

@ -1,12 +1,13 @@
import emojify from '@/utils/emojify'
import { Avatar, Button, CustomFlowbiteTheme, Dropdown, Flowbite, Tabs } from 'flowbite-react'
import { Entity, MegalodonInterface } from 'megalodon'
import { useEffect, useState } from 'react'
import { MouseEventHandler, useEffect, useState } from 'react'
import { FaEllipsisVertical } from 'react-icons/fa6'
import { FormattedMessage, useIntl } from 'react-intl'
import Timeline from './profile/Timeline'
import Followings from './profile/Followings'
import Followers from './profile/Followers'
import { findLink } from '@/utils/statusParser'
type Props = {
client: MegalodonInterface
@ -55,6 +56,15 @@ export default function Profile(props: Props) {
global.ipc.invoke('open-browser', url)
}
const profileClicked: MouseEventHandler<HTMLDivElement> = async e => {
const url = findLink(e.target as HTMLElement, 'profile')
if (url) {
global.ipc.invoke('open-browser', url)
e.preventDefault()
e.stopPropagation()
}
}
return (
<div style={{ height: 'calc(100% - 50px)' }} className="overflow-y-auto timeline-scrollable">
<Flowbite theme={{ theme: customTheme }}>
@ -93,13 +103,13 @@ export default function Profile(props: Props) {
<div className="pt-4">
<div className="font-bold" dangerouslySetInnerHTML={{ __html: emojify(user.display_name, user.emojis) }} />
<div className="text-gray-500">@{user.acct}</div>
<div className="mt-4 raw-html">
<div className="mt-4 raw-html profile" onClick={profileClicked}>
<span
dangerouslySetInnerHTML={{ __html: emojify(user.note, user.emojis) }}
className="overflow-hidden break-all text-gray-800"
/>
</div>
<div className="bg-gray-100 overflow-hidden break-all raw-html mt-2">
<div className="bg-gray-100 overflow-hidden break-all raw-html mt-2 profile" onClick={profileClicked}>
{user.fields.map((data, index) => (
<dl key={index} className="px-4 py-2 border-gray-200 border-b">
<dt className="text-gray-500">{data.name}</dt>

View File

@ -8,6 +8,7 @@ type Props = {
status: Entity.Status
spoilered: boolean
setSpoilered: Dispatch<SetStateAction<boolean>>
onClick?: (e: any) => void
} & HTMLAttributes<HTMLElement>
export default function Body(props: Props) {
@ -41,6 +42,7 @@ export default function Body(props: Props) {
className={`${props.className} raw-html`}
style={Object.assign({ wordWrap: 'break-word' }, props.style)}
dangerouslySetInnerHTML={{ __html: emojify(props.status.content, props.status.emojis) }}
onClick={props.onClick}
/>
)}
</>

View File

@ -9,7 +9,8 @@ import Poll from './Poll'
import { FormattedMessage } from 'react-intl'
import Actions from './Actions'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { MouseEventHandler, useState } from 'react'
import { findLink } from '@/utils/statusParser'
type Props = {
status: Entity.Status
@ -36,6 +37,15 @@ export default function Status(props: Props) {
router.push({ query: { id: router.query.id, timeline: router.query.timeline, user_id: id, detail: true } })
}
const statusClicked: MouseEventHandler<HTMLDivElement> = async e => {
const url = findLink(e.target as HTMLElement, 'status-body')
if (url) {
global.ipc.invoke('open-browser', url)
e.preventDefault()
e.stopPropagation()
}
}
return (
<div className="border-b mr-2 py-1">
{rebloggedHeader(props.status)}
@ -56,7 +66,9 @@ export default function Status(props: Props) {
<time dateTime={status.created_at}>{dayjs(status.created_at).format('YYYY-MM-DD HH:mm:ss')}</time>
</div>
</div>
<Body status={status} spoilered={spoilered} setSpoilered={setSpoilered} />
<div className="status-body">
<Body status={status} spoilered={spoilered} setSpoilered={setSpoilered} onClick={statusClicked} />
</div>
{!spoilered && (
<>
{status.poll && <Poll poll={status.poll} onRefresh={onRefresh} client={props.client} />}

View File

@ -0,0 +1,129 @@
import { Entity } from 'megalodon'
export type ParsedAccount = {
username: string
acct: string
url: string
}
export function findLink(target: HTMLElement | null, parentClassName: string): string | null {
if (!target) {
return null
}
if (target.localName === 'a') {
return (target as HTMLLinkElement).href
}
if (target.parentNode === undefined || target.parentNode === null) {
return null
}
const parent = target.parentNode as HTMLElement
if (parent.getAttribute && parent.getAttribute('class') === parentClassName) {
return null
}
return findLink(parent, parentClassName)
}
export function findTag(target: HTMLElement, parentClass = 'toot'): string | null {
if (!target || !target.getAttribute) {
return null
}
const targetClass = target.getAttribute('class')
if (targetClass && targetClass.includes('hashtag')) {
return parseTag((target as HTMLLinkElement).href)
}
// In Pleroma, link does not have class.
// So I have to check URL.
const link = target as HTMLLinkElement
if (link.href && link.href.match(/^https:\/\/[a-zA-Z0-9-.]+\/(tag|tags)\/.+/)) {
return parseTag(link.href)
}
if (target.parentNode === undefined || target.parentNode === null) {
return null
}
const parent = target.parentNode as HTMLElement
if (parent.getAttribute && parent.getAttribute('class') === parentClass) {
return null
}
return findTag(parent, parentClass)
}
function parseTag(tagURL: string): string | null {
const res = tagURL.match(/^https:\/\/([a-zA-Z0-9-.]+)\/(tag|tags)\/(.+)/)
if (!res) {
return null
}
return res[3]
}
export function findAccount(target: HTMLElement | null, parentClassName: string): ParsedAccount | null {
if (!target || !target.getAttribute) {
return null
}
const targetClass = target.getAttribute('class')
const link = target as HTMLLinkElement
if (targetClass && targetClass.includes('u-url')) {
if (link.href && link.href.match(/^https:\/\/[a-zA-Z0-9-.]+\/users\/[a-zA-Z0-9-_.]+$/)) {
return parsePleromaAccount(link.href)
} else {
return parseMastodonAccount(link.href)
}
}
// In Pleroma, link does not have class.
// So we have to check URL.
if (link.href && link.href.match(/^https:\/\/[a-zA-Z0-9-.]+\/@[a-zA-Z0-9-_.]+$/)) {
return parseMastodonAccount(link.href)
}
// Toot URL of Pleroma does not contain @.
if (link.href && link.href.match(/^https:\/\/[a-zA-Z0-9-.]+\/users\/[a-zA-Z0-9-_.]+$/)) {
return parsePleromaAccount(link.href)
}
if (target.parentNode === undefined || target.parentNode === null) {
return null
}
const parent = target.parentNode as HTMLElement
if (parent.getAttribute && parent.getAttribute('class') === parentClassName) {
return null
}
return findAccount(parent, parentClassName)
}
export function parseMastodonAccount(accountURL: string): ParsedAccount | null {
const res = accountURL.match(/^https:\/\/([a-zA-Z0-9-.]+)\/(@[a-zA-Z0-9-_.]+)$/)
if (!res) {
return null
}
const domainName = res[1]
const accountName = res[2]
return {
username: accountName,
acct: `${accountName}@${domainName}`,
url: accountURL
}
}
export function parsePleromaAccount(accountURL: string): ParsedAccount | null {
const res = accountURL.match(/^https:\/\/([a-zA-Z0-9-.]+)\/users\/([a-zA-Z0-9-_.]+)$/)
if (!res) {
return null
}
const domainName = res[1]
const accountName = res[2]
return {
username: `@${accountName}`,
acct: `@${accountName}@${domainName}`,
url: accountURL
}
}
export function accountMatch(findAccounts: Array<Entity.Account>, parsedAccount: ParsedAccount, domain: string): Entity.Account | false {
const account = findAccounts.find(a => `@${a.acct}` === parsedAccount.acct)
if (account) return account
const pleromaUser = findAccounts.find(a => a.acct === parsedAccount.acct)
if (pleromaUser) return pleromaUser
const localUser = findAccounts.find(a => `@${a.username}@${domain}` === parsedAccount.acct)
if (localUser) return localUser
const user = findAccounts.find(a => a.url === parsedAccount.url)
if (!user) return false
return user
}