-
+
diff --git a/app/javascript/flavours/glitch/features/ui/index.jsx b/app/javascript/flavours/glitch/features/ui/index.jsx
index 4a7b9ebfde..2f0bfd3355 100644
--- a/app/javascript/flavours/glitch/features/ui/index.jsx
+++ b/app/javascript/flavours/glitch/features/ui/index.jsx
@@ -15,6 +15,7 @@ import { HotKeys } from 'react-hotkeys';
import { changeLayout } from 'flavours/glitch/actions/app';
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'flavours/glitch/actions/markers';
import { INTRODUCTION_VERSION } from 'flavours/glitch/actions/onboarding';
+import { HoverCardController } from 'flavours/glitch/components/hover_card_controller';
import { Permalink } from 'flavours/glitch/components/permalink';
import { PictureInPicture } from 'flavours/glitch/features/picture_in_picture';
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
@@ -648,6 +649,7 @@ class UI extends PureComponent {
{layout !== 'mobile' &&
}
+
diff --git a/app/javascript/flavours/glitch/hooks/useLinks.ts b/app/javascript/flavours/glitch/hooks/useLinks.ts
new file mode 100644
index 0000000000..bb988665fd
--- /dev/null
+++ b/app/javascript/flavours/glitch/hooks/useLinks.ts
@@ -0,0 +1,61 @@
+import { useCallback } from 'react';
+
+import { useHistory } from 'react-router-dom';
+
+import { openURL } from 'flavours/glitch/actions/search';
+import { useAppDispatch } from 'flavours/glitch/store';
+
+const isMentionClick = (element: HTMLAnchorElement) =>
+ element.classList.contains('mention');
+
+const isHashtagClick = (element: HTMLAnchorElement) =>
+ element.textContent?.[0] === '#' ||
+ element.previousSibling?.textContent?.endsWith('#');
+
+export const useLinks = () => {
+ const history = useHistory();
+ const dispatch = useAppDispatch();
+
+ const handleHashtagClick = useCallback(
+ (element: HTMLAnchorElement) => {
+ const { textContent } = element;
+
+ if (!textContent) return;
+
+ history.push(`/tags/${textContent.replace(/^#/, '')}`);
+ },
+ [history],
+ );
+
+ const handleMentionClick = useCallback(
+ (element: HTMLAnchorElement) => {
+ dispatch(
+ openURL(element.href, history, () => {
+ window.location.href = element.href;
+ }),
+ );
+ },
+ [dispatch, history],
+ );
+
+ const handleClick = useCallback(
+ (e: React.MouseEvent) => {
+ const target = (e.target as HTMLElement).closest('a');
+
+ if (!target || e.button !== 0 || e.ctrlKey || e.metaKey) {
+ return;
+ }
+
+ if (isMentionClick(target)) {
+ e.preventDefault();
+ handleMentionClick(target);
+ } else if (isHashtagClick(target)) {
+ e.preventDefault();
+ handleHashtagClick(target);
+ }
+ },
+ [handleMentionClick, handleHashtagClick],
+ );
+
+ return handleClick;
+};
diff --git a/app/javascript/flavours/glitch/hooks/useTimeout.ts b/app/javascript/flavours/glitch/hooks/useTimeout.ts
new file mode 100644
index 0000000000..f1814ae8e3
--- /dev/null
+++ b/app/javascript/flavours/glitch/hooks/useTimeout.ts
@@ -0,0 +1,29 @@
+import { useRef, useCallback, useEffect } from 'react';
+
+export const useTimeout = () => {
+ const timeoutRef = useRef
>();
+
+ const set = useCallback((callback: () => void, delay: number) => {
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current);
+ }
+
+ timeoutRef.current = setTimeout(callback, delay);
+ }, []);
+
+ const cancel = useCallback(() => {
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current);
+ timeoutRef.current = undefined;
+ }
+ }, []);
+
+ useEffect(
+ () => () => {
+ cancel();
+ },
+ [cancel],
+ );
+
+ return [set, cancel] as const;
+};
diff --git a/app/javascript/flavours/glitch/styles/components.scss b/app/javascript/flavours/glitch/styles/components.scss
index 9980f87adc..11f06dd010 100644
--- a/app/javascript/flavours/glitch/styles/components.scss
+++ b/app/javascript/flavours/glitch/styles/components.scss
@@ -120,8 +120,27 @@
text-decoration: none;
}
- &:disabled {
- opacity: 0.5;
+ &.button--destructive {
+ &:active,
+ &:focus,
+ &:hover {
+ border-color: $ui-button-destructive-focus-background-color;
+ color: $ui-button-destructive-focus-background-color;
+ }
+ }
+
+ &:disabled,
+ &.disabled {
+ opacity: 0.7;
+ border-color: $ui-primary-color;
+ color: $ui-primary-color;
+
+ &:active,
+ &:focus,
+ &:hover {
+ border-color: $ui-primary-color;
+ color: $ui-primary-color;
+ }
}
}
@@ -2629,7 +2648,7 @@ a.account__display-name {
}
.dropdown-animation {
- animation: dropdown 150ms cubic-bezier(0.1, 0.7, 0.1, 1);
+ animation: dropdown 250ms cubic-bezier(0.1, 0.7, 0.1, 1);
@keyframes dropdown {
from {
@@ -10908,3 +10927,156 @@ noscript {
}
}
}
+
+.hover-card-controller[data-popper-reference-hidden='true'] {
+ opacity: 0;
+ pointer-events: none;
+}
+
+.hover-card {
+ box-shadow: var(--dropdown-shadow);
+ background: var(--modal-background-color);
+ backdrop-filter: var(--background-filter);
+ border: 1px solid var(--modal-border-color);
+ border-radius: 8px;
+ padding: 16px;
+ width: 270px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+
+ &--loading {
+ position: relative;
+ min-height: 100px;
+ }
+
+ &__name {
+ display: flex;
+ gap: 12px;
+ text-decoration: none;
+ color: inherit;
+ }
+
+ &__number {
+ font-size: 15px;
+ line-height: 22px;
+ color: $secondary-text-color;
+
+ strong {
+ font-weight: 700;
+ }
+ }
+
+ &__text-row {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ &__bio {
+ color: $secondary-text-color;
+ font-size: 14px;
+ line-height: 20px;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ max-height: 2 * 20px;
+ overflow: hidden;
+
+ p {
+ margin-bottom: 0;
+ }
+
+ a {
+ color: inherit;
+ text-decoration: underline;
+
+ &:hover,
+ &:focus,
+ &:active {
+ text-decoration: none;
+ }
+ }
+ }
+
+ .display-name {
+ font-size: 15px;
+ line-height: 22px;
+
+ bdi {
+ font-weight: 500;
+ color: $primary-text-color;
+ }
+
+ &__account {
+ display: block;
+ color: $dark-text-color;
+ }
+ }
+
+ .account-fields {
+ color: $secondary-text-color;
+ font-size: 14px;
+ line-height: 20px;
+
+ a {
+ color: inherit;
+ text-decoration: none;
+
+ &:focus,
+ &:hover,
+ &:active {
+ text-decoration: underline;
+ }
+ }
+
+ dl {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+
+ dt {
+ flex: 0 0 auto;
+ color: $dark-text-color;
+ min-width: 0;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ dd {
+ flex: 1 1 auto;
+ font-weight: 500;
+ min-width: 0;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ &.verified {
+ dd {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ overflow: hidden;
+ white-space: nowrap;
+ color: $valid-value-color;
+
+ & > span {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ a {
+ font-weight: 500;
+ }
+
+ .icon {
+ width: 16px;
+ height: 16px;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss b/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss
index 3cdbd9bf67..9f571b3f26 100644
--- a/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss
+++ b/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss
@@ -59,6 +59,8 @@ $emojis-requiring-inversion: 'chains';
body {
--dropdown-border-color: #d9e1e8;
--dropdown-background-color: #fff;
+ --modal-border-color: #d9e1e8;
+ --modal-background-color: var(--background-color-tint);
--background-border-color: #d9e1e8;
--background-color: #fff;
--background-color-tint: rgba(255, 255, 255, 80%);