diff --git a/lib/hooks/useResettingState.js b/lib/hooks/useResettingState.js
new file mode 100644
--- /dev/null
+++ b/lib/hooks/useResettingState.js
@@ -0,0 +1,26 @@
+// @flow
+
+import _debounce from 'lodash/debounce.js';
+import * as React from 'react';
+
+import type { SetState } from '../types/hook-types.js';
+
+function useResettingState<T>(
+  initialState: (() => T) | T,
+  duration: number,
+): [T, SetState<T>] {
+  const [value, setValue] = React.useState(initialState);
+  const resetStatusAfterTimeout = React.useRef(
+    _debounce(() => setValue(initialState), duration),
+  );
+  React.useEffect(() => resetStatusAfterTimeout.current.cancel, []);
+
+  const setNewValue = React.useCallback((newValue: (T => T) | T) => {
+    setValue(newValue);
+    resetStatusAfterTimeout.current();
+  }, []);
+
+  return React.useMemo(() => [value, setNewValue], [setNewValue, value]);
+}
+
+export { useResettingState };
diff --git a/web/invite-links/view-invite-link-modal.react.js b/web/invite-links/view-invite-link-modal.react.js
--- a/web/invite-links/view-invite-link-modal.react.js
+++ b/web/invite-links/view-invite-link-modal.react.js
@@ -5,6 +5,7 @@
 import { useModalContext } from 'lib/components/modal-provider.react.js';
 import SWMansionIcon from 'lib/components/SWMansionIcon.react.js';
 import { inviteLinkUrl } from 'lib/facts/links.js';
+import { useResettingState } from 'lib/hooks/useResettingState.js';
 import { threadInfoSelector } from 'lib/selectors/thread-selectors.js';
 import type { InviteLink } from 'lib/types/link-types.js';
 import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js';
@@ -18,6 +19,7 @@
   +inviteLink: InviteLink,
 };
 
+const copiedMessageDurationMs = 2000;
 function ViewInviteLinkModal(props: Props): React.Node {
   const { inviteLink } = props;
   const threadInfo = useSelector(
@@ -27,9 +29,16 @@
   const { popModal } = useModalContext();
 
   const url = inviteLinkUrl(inviteLink.name);
-  const copyLink = React.useCallback(() => {
-    navigator.clipboard.writeText(url);
-  }, [url]);
+  const [copied, setCopied] = useResettingState(false, copiedMessageDurationMs);
+  const copyLink = React.useCallback(async () => {
+    try {
+      await navigator.clipboard.writeText(url);
+      setCopied(true);
+    } catch (e) {
+      setCopied(false);
+    }
+  }, [setCopied, url]);
+  const buttonText = copied ? 'Copied!' : 'Copy';
 
   return (
     <Modal
@@ -46,7 +55,7 @@
           <div className={css.linkUrl}>{url}</div>
           <Button className={css.linkCopyButton} onClick={copyLink}>
             <SWMansionIcon icon="link" size={24} />
-            Copy
+            {buttonText}
           </Button>
         </div>
       </div>
diff --git a/web/utils/tooltip-action-utils.js b/web/utils/tooltip-action-utils.js
--- a/web/utils/tooltip-action-utils.js
+++ b/web/utils/tooltip-action-utils.js
@@ -1,10 +1,10 @@
 // @flow
 
 import invariant from 'invariant';
-import _debounce from 'lodash/debounce.js';
 import * as React from 'react';
 
 import { useModalContext } from 'lib/components/modal-provider.react.js';
+import { useResettingState } from 'lib/hooks/useResettingState.js';
 import type { ChatMessageInfoItem } from 'lib/selectors/chat-selectors.js';
 import { useCanEditMessage } from 'lib/shared/edit-messages-utils.js';
 import { createMessageReply } from 'lib/shared/message-utils.js';
@@ -120,18 +120,11 @@
 ): ?MessageTooltipAction {
   const { messageInfo } = item;
 
-  const [successful, setSuccessful] = React.useState(false);
-  const resetStatusAfterTimeout = React.useRef(
-    _debounce(() => setSuccessful(false), copiedMessageDurationMs),
+  const [successful, setSuccessful] = useResettingState(
+    false,
+    copiedMessageDurationMs,
   );
 
-  const onSuccess = React.useCallback(() => {
-    setSuccessful(true);
-    resetStatusAfterTimeout.current();
-  }, []);
-
-  React.useEffect(() => resetStatusAfterTimeout.current.cancel, []);
-
   return React.useMemo(() => {
     if (messageInfo.type !== messageTypes.TEXT) {
       return null;
@@ -140,7 +133,7 @@
     const onClick = async () => {
       try {
         await navigator.clipboard.writeText(messageInfo.text);
-        onSuccess();
+        setSuccessful(true);
       } catch (e) {
         setSuccessful(false);
       }
@@ -150,7 +143,7 @@
       onClick,
       label: successful ? 'Copied!' : 'Copy',
     };
-  }, [messageInfo.text, messageInfo.type, onSuccess, successful]);
+  }, [messageInfo.text, messageInfo.type, setSuccessful, successful]);
 }
 
 function useMessageReactAction(