mirror of
https://github.com/h3poteto/whalebird-desktop
synced 2025-02-03 18:57:43 +01:00
refs #4653 Open link in browser
This commit is contained in:
parent
a5811bc96f
commit
6a9a198e59
@ -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>
|
||||
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -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} />}
|
||||
|
129
renderer/utils/statusParser.ts
Normal file
129
renderer/utils/statusParser.ts
Normal 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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user