diff --git a/src/components/modals/CreateBoardModal.jsx b/src/components/modals/CreateBoardModal.jsx
index 3da4044b..1a16b58b 100644
--- a/src/components/modals/CreateBoardModal.jsx
+++ b/src/components/modals/CreateBoardModal.jsx
@@ -1,22 +1,34 @@
-import React, { useRef, useState } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useAccount, useCreateSubplebbit,
+ // useSubplebbits, useAccountSubplebbits
+ } from '@plebbit/plebbit-react-hooks';
import { StyledModal } from '../styled/modals/CreateBoardModal.styled';
import useGeneralStore from '../../hooks/stores/useGeneralStore';
import Modal from 'react-modal';
import useError from '../../hooks/useError';
+import useSuccess from '../../hooks/useSuccess';
const CreateBoardModal = ({ isOpen, closeModal }) => {
const { selectedStyle } = useGeneralStore(state => state);
- const nodeRef = useRef(null);
+ const account = useAccount();
+ const navigate = useNavigate();
- const [boardTitle, setBoardTitle] = useState('');
+ const nodeRef = useRef(null);
+ const ruleInputRef = useRef(null);
+
+ const [, setNewErrorMessage] = useError();
+ const [, setNewSuccessMessage] = useSuccess();
+
+ const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
+ const [avatar, setAvatar] = useState('');
+ const [moderators, setModerators] = useState('');
const [rule, setRule] = useState('');
const [rules, setRules] = useState([]);
const [editIndex, setEditIndex] = useState(-1);
- const [, setNewErrorMessage] = useError();
-
const handleAddRule = () => {
if (rule.trim() === '') {
@@ -39,6 +51,13 @@ const CreateBoardModal = ({ isOpen, closeModal }) => {
}
+ useEffect(() => {
+ if (ruleInputRef.current) {
+ ruleInputRef.current.focus();
+ }
+ }, [rules]);
+
+
const handleKeyDown = (e) => {
if (e.key === 'Enter') {
handleAddRule();
@@ -49,6 +68,7 @@ const CreateBoardModal = ({ isOpen, closeModal }) => {
const handleEditRule = (index) => {
setRule(rules[index]);
setEditIndex(index);
+ ruleInputRef.current.focus();
}
@@ -58,8 +78,88 @@ const CreateBoardModal = ({ isOpen, closeModal }) => {
}
setRules(rules.filter((_, i) => i !== index));
}
+
+
+ const createSubplebbitOptions = {
+ title: title || undefined,
+ description: description || undefined,
+ suggested: {
+ avatarUrl : avatar || undefined,
+ },
+ roles: moderators || undefined,
+ rules: rules || undefined,
+ }
+
+
+ const resetFields = () => {
+ setTitle('');
+ setDescription('');
+ setAvatar('');
+ setModerators('');
+ setRule('');
+ setRules([]);
+ }
+
+
+ const { createdSubplebbit, createSubplebbit } = useCreateSubplebbit(createSubplebbitOptions);
+ const handleCreateBoard = async () => {
+ let moderatorAddresses = moderators.trim() ? moderators.split(',').map(addr => addr.trim()) : [];
+ let invalidAddresses = moderatorAddresses.filter(addr => !(addr.endsWith('.eth') || (addr.startsWith('12D3KooW') && addr.length === 52)));
+
+ if (invalidAddresses.length > 0) {
+ setNewErrorMessage("Invalid moderator addresses: " + invalidAddresses.join(", "));
+ return;
+ }
+
+ const roles = {};
+ moderatorAddresses.forEach(addr => {
+ roles[addr] = { role: 'moderator' };
+ });
+ roles[account.author.address] = { role: 'admin' };
+
+ let createSubplebbitOptions = {
+ roles: roles,
+ };
+
+ if (title) {
+ createSubplebbitOptions.title = title;
+ }
+
+ if (description) {
+ createSubplebbitOptions.description = description;
+ }
+
+ if (avatar) {
+ createSubplebbitOptions.suggested = {
+ avatarUrl : avatar,
+ };
+ }
+
+ if (rules.length > 0) {
+ createSubplebbitOptions.rules = rules;
+ }
+
+ try {
+ await createSubplebbit(createSubplebbitOptions);
+ } catch (error) {
+ setNewErrorMessage(error.message);
+ }
+
+ if (createdSubplebbit) {
+ resetFields();
+ closeModal();
+ setNewSuccessMessage('Board created successfully, address: ' + createdSubplebbit.address);
+ navigate(`/p/${createdSubplebbit.address}`);
+ }
+ }
+
+ // remove after testing:
+ // const {accountSubplebbits} = useAccountSubplebbits()
+ // const ownerSubplebbitAddresses = Object.keys(accountSubplebbits).map(subplebbitAddress => accountSubplebbits[subplebbitAddress].role === 'owner')
+ // const subplebbits = useSubplebbits({subplebbitAddresses: ownerSubplebbitAddresses})
+ // console.log(subplebbits);
return (
{
setBoardTitle(e.target.value)}
+ value={title}
+ onChange={(e) => setTitle(e.target.value)}
placeholder="Board Title"
/>
@@ -116,8 +216,8 @@ const CreateBoardModal = ({ isOpen, closeModal }) => {
setBoardTitle(e.target.value)}
+ value={avatar}
+ onChange={(e) => setAvatar(e.target.value)}
placeholder="https://example.com/image.png"
/>
@@ -128,11 +228,11 @@ const CreateBoardModal = ({ isOpen, closeModal }) => {
Optional, let other users help you moderate your board.
- setBoardTitle(e.target.value)}
+ value={moderators}
+ onChange={(e) => setModerators(e.target.value)}
placeholder='username.eth, 12D3KooW..., username2.eth'
/>
@@ -144,6 +244,7 @@ const CreateBoardModal = ({ isOpen, closeModal }) => {
{
))}
)}
- Create Board
+ Create Board
@@ -180,4 +281,4 @@ const CreateBoardModal = ({ isOpen, closeModal }) => {
Modal.setAppElement('#root');
-export default CreateBoardModal;
+export default CreateBoardModal;
\ No newline at end of file
diff --git a/src/components/modals/SettingsModal.jsx b/src/components/modals/SettingsModal.jsx
index dbbe614d..b2b3183b 100755
--- a/src/components/modals/SettingsModal.jsx
+++ b/src/components/modals/SettingsModal.jsx
@@ -1,7 +1,7 @@
import React, { useState, useEffect, useRef } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import Modal from "react-modal";
-import { deleteCaches, exportAccount, importAccount, setAccount, setActiveAccount, useAccount, useAccounts } from "@plebbit/plebbit-react-hooks";
+import { deleteCaches, exportAccount, importAccount, setAccount, setActiveAccount, useAccount, useAccounts, useResolvedAuthorAddress } from "@plebbit/plebbit-react-hooks";
import { StyledModal } from "../styled/modals/SettingsModal.styled";
import useError from "../../hooks/useError";
import useSuccess from "../../hooks/useSuccess";
@@ -22,6 +22,9 @@ const SettingsModal = ({ isOpen, closeModal }) => {
const location = useLocation();
const [expanded, setExpanded] = useState([]);
const [accountJson, setAccountJson] = useState(null);
+ const [copyStatus, setCopyStatus] = useState(false);
+ const [ensName, setEnsName] = useState('');
+ const [checkedENS, setCheckedENS] = useState(false);
const [, setNewErrorMessage] = useError();
const [, setNewSuccessMessage] = useSuccess();
@@ -37,6 +40,52 @@ const SettingsModal = ({ isOpen, closeModal }) => {
const dataPathRef = useRef();
const importRef = useRef();
const nameRef = useRef();
+ const ensRef = useRef();
+
+ const author = {...account?.author, address: ensName};
+ const { resolvedAddress, state } = useResolvedAuthorAddress({ author, cache: false });
+
+
+ useEffect(() => {
+ if (checkedENS && resolvedAddress && state === 'succeeded') {
+ setCheckedENS(false);
+ }
+ }, [checkedENS, state, resolvedAddress]);
+
+
+
+ const handleENSChange = () => {
+ const newEnsName = ensRef.current.value;
+ setEnsName(newEnsName);
+ };
+
+ const handleENSCheck = () => {
+ const newEnsName = ensRef.current.value;
+ setEnsName(newEnsName);
+ setCheckedENS(true);
+ };
+
+ const handleENSSave = async () => {
+ if (resolvedAddress === account?.signer?.address) {
+ try {
+ await setAccount({
+ ...account,
+ author: {
+ ...account?.author,
+ address: ensName,
+ },
+ });
+ setNewSuccessMessage("ENS Name Saved");
+
+ } catch (error) {
+ setNewErrorMessage(error.message);
+ console.log(error);
+ }
+ } else if (resolvedAddress !== account?.signer?.address) {
+ setNewErrorMessage(`Failed resolving address, ${ensName}`);
+ }
+ };
+
const defaultGatewayUrls = [
'https://ipfs.io',
@@ -60,6 +109,7 @@ const SettingsModal = ({ isOpen, closeModal }) => {
'https://polygon-rpc.com',
];
+
const isValidURL = (url) => {
try {
new URL(url);
@@ -70,6 +120,32 @@ const SettingsModal = ({ isOpen, closeModal }) => {
};
+ const handleCopyAddress = async () => {
+ try {
+ await navigator.clipboard.writeText(account?.author.address);
+ setCopyStatus(true);
+ } catch (err) {
+ console.error('Failed to copy text: ', err);
+ }
+ };
+
+
+ useEffect(() => {
+ let copyStatusTimeoutId;
+ let checkedENSTimeoutId;
+ if (copyStatus) {
+ copyStatusTimeoutId = setTimeout(() => setCopyStatus(false), 3000);
+ }
+ if (checkedENS) {
+ checkedENSTimeoutId = setTimeout(() => setCheckedENS(false), 5000);
+ }
+ return () => {
+ clearTimeout(copyStatusTimeoutId);
+ clearTimeout(checkedENSTimeoutId);
+ };
+ }, [copyStatus, checkedENS]);
+
+
const handleSavePlebbitOptions = async () => {
let gatewayUrls = gatewayRef.current.value.split('\n').filter(url => url.trim());
let ipfsClientsOptions = ipfsRef.current.value.split('\n').filter(url => url.trim());
@@ -151,7 +227,7 @@ const SettingsModal = ({ isOpen, closeModal }) => {
await setAccount({
...account,
plebbitOptions: {
- ...account.plebbitOptions,
+ ...account?.plebbitOptions,
chainProviders,
},
});
@@ -211,7 +287,7 @@ const SettingsModal = ({ isOpen, closeModal }) => {
await setAccount({
...account,
plebbitOptions: {
- ...account.plebbitOptions,
+ ...account?.plebbitOptions,
chainProviders,
},
});
@@ -297,9 +373,9 @@ const SettingsModal = ({ isOpen, closeModal }) => {
try {
await setAccount({
...account,
- name: name || account.name,
+ name: name || account?.name,
author: {
- ...account.author,
+ ...account?.author,
displayName: name,
}});
setNewSuccessMessage("Account Name Saved");
@@ -353,6 +429,11 @@ const SettingsModal = ({ isOpen, closeModal }) => {
+ Export
+ Import
@@ -447,7 +562,7 @@ const SettingsModal = ({ isOpen, closeModal }) => {
onClick={handleResetPlebbitOptions}>Reset
-
+
IPFS Gateway URLs
@@ -509,23 +624,23 @@ const SettingsModal = ({ isOpen, closeModal }) => {
Reset
-
+
Ethereum RPC
- Needed for .eth addresses
+ Needed for .eth addresses.
- Polygon RPC
- Needed for XPLEB NFTs
+ {/* Polygon RPC
+ Needed for XPLEB NFTs.
-
+ */}
diff --git a/src/components/styled/modals/CreateBoardModal.styled.jsx b/src/components/styled/modals/CreateBoardModal.styled.jsx
index 36082690..4e4cc074 100644
--- a/src/components/styled/modals/CreateBoardModal.styled.jsx
+++ b/src/components/styled/modals/CreateBoardModal.styled.jsx
@@ -3,14 +3,15 @@ import Modal from "react-modal";
export const StyledModal = styled(Modal)`
.modal-content {
- top: 25%;
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
width: 450px;
- max-height: 650px;
- left: calc(50% - 50px);
+ height: auto;
+ max-height: calc(100vh - 2*50px);
overflow-y: auto;
- margin-left: -178px;
- position: absolute;
- padding: 5px 5px 5px;
+ padding: 5px;
box-shadow: 0 0 5px rgba(0, 0, 0, .25);
}
diff --git a/src/components/styled/modals/SettingsModal.styled.jsx b/src/components/styled/modals/SettingsModal.styled.jsx
index 02476392..0efef70c 100644
--- a/src/components/styled/modals/SettingsModal.styled.jsx
+++ b/src/components/styled/modals/SettingsModal.styled.jsx
@@ -114,6 +114,10 @@ export const StyledModal = styled(Modal)`
position: absolute;
}
+ .check-button {
+ margin-left: 58px !important;
+ }
+
.cache-button {
margin: auto;
margin-bottom: 10px;
@@ -122,9 +126,10 @@ export const StyledModal = styled(Modal)`
}
.settings-option {
- margin: 5px 0 5px 0;
+ margin: 15px 0 5px 0 !important;
padding-left: 23px;
display: inline-block;
+ position: relative;
}
.settings-tip {
@@ -145,13 +150,6 @@ export const StyledModal = styled(Modal)`
margin-top: 6px;
}
- .settings-option {
- margin: 5px 0 5px 0;
- padding-left: 23px;
- display: inline-block;
- position: relative;
- }
-
.settings-input {
position: relative;
padding-left: 23px;
@@ -195,7 +193,7 @@ export const StyledModal = styled(Modal)`
}
.anon-tip {
- margin-bottom: 10px;
+ margin-bottom: 0px;
}
.anon-tip::before {
diff --git a/src/components/styled/views/Board.styled.jsx b/src/components/styled/views/Board.styled.jsx
index 0d2457de..20fcc6fa 100644
--- a/src/components/styled/views/Board.styled.jsx
+++ b/src/components/styled/views/Board.styled.jsx
@@ -1905,7 +1905,7 @@ export const BoardForm = styled.div`
.file-thumb {
float: left;
margin: 3px 20px 5px 20px;
- background-color: rgba(0, 0, 0, 0.1);
+ background-color: rgba(0, 0, 0, 0.05);
}
.reply-file-text {
@@ -2070,6 +2070,19 @@ export const BoardForm = styled.div`
margin: 7px 2px 0;
padding-left: 0;
+ .button-mobile {
+ float: right;
+ border-radius: 3px;
+ padding: 6px 10px 5px;
+ user-select: none;
+ text-decoration: none;
+ }
+
+ video {
+ width: 100%;
+ height: auto;
+ }
+
.post-reply {
margin-top: 0 !important;
width: 100%;
@@ -2849,6 +2862,10 @@ export const BoardForm = styled.div`
clear: both;
}
+ .file-thumb {
+ background-color: rgba(255, 255, 255, 0.01) !important;
+ }
+
.op-container {
display: grid;
}
diff --git a/src/components/views/Board.jsx b/src/components/views/Board.jsx
index b7fdbf80..53853fc2 100755
--- a/src/components/views/Board.jsx
+++ b/src/components/views/Board.jsx
@@ -2017,6 +2017,16 @@ const Board = () => {
replyMediaInfo?.url.slice(0, 30) + "(...)" :
replyMediaInfo?.url
} ({replyMediaInfo?.type})
+ {replyMediaInfo?.type === "video" || "iframe" ? (
+ isReplyThumbnailClicked[index] ? (
+
+ -[
+ {handleThumbnailClick(index, 'reply')}}>Close
+ ]
+ ) : null)
+ : null}
{replyMediaInfo?.type === "webpage" ? (
@@ -2043,12 +2053,29 @@ const Board = () => {
) : null}
{replyMediaInfo?.type === "video" ? (
+
- e.target.src = fallbackImgUrl} />
+ {isReplyThumbnailClicked[index] ? (
+ e.target.src = fallbackImgUrl}
+ />
+ ) : (
+ {handleThumbnailClick(index, 'reply')}}
+ style={{cursor: "pointer"}}
+ onError={(e) => e.target.src = fallbackImgUrl}
+ />
+ )}
+
) : null}
{replyMediaInfo?.type === "audio" ? (
@@ -2625,13 +2652,35 @@ const Board = () => {
) : replyMediaInfo.type === "video" ? (
-
- e.target.src = fallbackImgUrl} />
- {replyMediaInfo.type}
-
+
+ {isMobileReplyThumbnailClicked[index] ? (
+ e.target.src = fallbackImgUrl} />
+ ) : (
+ {handleThumbnailClick(index, 'mobileReply')}}
+ style={{cursor: "pointer"}}
+ id="video-thumbnail-mobile"
+ onError={(e) => e.target.src = fallbackImgUrl}
+ />
+ )}
+ {replyMediaInfo?.type === "video" || "iframe" ? (
+ isMobileReplyThumbnailClicked[index] ? (
+
+ {handleThumbnailClick(index, 'mobileReply')}}
+ >Close
+
+ ) : (
+ video
+ )) : video
}
+
) : replyMediaInfo.type === "audio" ? (
{
const [postOnHoverHeight, setPostOnHoverHeight] = useState(0);
const [executeAnonMode, setExecuteAnonMode] = useState(false);
const [cidTracker, setCidTracker] = useState({});
- const [isThumbnailClicked, setIsThumbnailClicked] = useState({});
- const [isMobileThumbnailClicked, setIsMobileThumbnailClicked] = useState({});
+ const [isThreadThumbnailClicked, setIsThreadThumbnailClicked] = useState({});
+ const [isReplyThumbnailClicked, setIsReplyThumbnailClicked] = useState({});
+ const [isMobileThreadThumbnailClicked, setIsMobileThreadThumbnailClicked] = useState({});
const [isCreateBoardOpen, setIsCreateBoardOpen] = useState(false);
@@ -131,17 +132,28 @@ const Thread = () => {
const fallbackImgUrl = "assets/filedeleted-res.gif";
- const handleThumbnailClick = (index, isMobile=false) => {
- if (isMobile) {
- setIsMobileThumbnailClicked(prevState => ({
- ...prevState,
- [index]: !prevState[index],
- }));
- } else {
- setIsThumbnailClicked(prevState => ({
- ...prevState,
- [index]: !prevState[index],
- }));
+ const handleThumbnailClick = (index, type) => {
+ switch(type) {
+ case 'thread':
+ setIsThreadThumbnailClicked(prevState => ({
+ ...prevState,
+ [index]: !prevState[index],
+ }));
+ break;
+ case 'reply':
+ setIsReplyThumbnailClicked(prevState => ({
+ ...prevState,
+ [index]: !prevState[index],
+ }));
+ break;
+ case 'mobileThread':
+ setIsMobileThreadThumbnailClicked(prevState => ({
+ ...prevState,
+ [index]: !prevState[index],
+ }));
+ break;
+ default:
+ break;
}
};
@@ -844,21 +856,21 @@ const Thread = () => {
commentMediaInfo?.url.slice(0, 30) + "(...)" :
commentMediaInfo?.url
} ({commentMediaInfo?.type === "iframe" ? "video" : commentMediaInfo?.type})
- {isThumbnailClicked[index] ? (
+ {isThreadThumbnailClicked[index] ? (
-[
{handleThumbnailClick(index)}}>Close
+ onClick={() => {handleThumbnailClick(index, 'thread')}}>Close
]
) : null}
{commentMediaInfo?.type === 'iframe' && (
+ className={`img-container ${isThreadThumbnailClicked[index] ? 'expanded-container' : ''}`}>
- {(isThumbnailClicked[index] || !commentMediaInfo.thumbnail) && commentMediaInfo.embedUrl ? (
+ {(isThreadThumbnailClicked[index] || !commentMediaInfo.thumbnail) && commentMediaInfo.embedUrl ? (
) : null}
{commentMediaInfo?.type === "video" ? (
-
+
- {isThumbnailClicked[index] ? (
+ {isThreadThumbnailClicked[index] ? (
{
key={`fti-${index}`}
src={commentMediaInfo.url}
alt="thumbnail"
- onClick={() => {handleThumbnailClick(index)}}
+ onClick={() => {handleThumbnailClick(index, 'thread')}}
style={{cursor: "pointer"}}
onError={(e) => e.target.src = fallbackImgUrl}
/>
@@ -1193,6 +1205,15 @@ const Thread = () => {
if (storedSigners[selectedThread]) {
signerAddress = storedSigners[selectedThread].address;
}
+ let replyDisplayWidth, replyDisplayHeight;
+ if (reply.linkWidth && reply.linkHeight) {
+ const scale = Math.min(1, 125 / Math.max(reply.linkWidth, reply.linkHeight));
+ replyDisplayWidth = `${reply.linkWidth * scale}px`;
+ replyDisplayHeight = `${reply.linkHeight * scale}px`;
+ } else {
+ replyDisplayWidth = '125px';
+ replyDisplayHeight = '125px';
+ }
return (
{'>>'}
@@ -1424,57 +1445,86 @@ const Thread = () => {
{replyMediaInfo?.url ? (
-
-
- {replyMediaInfo?.type === "webpage" ? (
-
-
- {reply.thumbnailUrl ? (
- e.target.src = fallbackImgUrl} />
- ) : null}
-
-
- ) : null}
- {replyMediaInfo?.type === "image" ? (
-
-
+
+
+ {replyMediaInfo?.type === "webpage" ? (
+
+
+ {reply.thumbnailUrl ? (
{handleImageClick(e); handleThumbnailClick(index, 'reply')}}
style={{cursor: "pointer"}}
onError={(e) => e.target.src = fallbackImgUrl} />
-
+ ) : null}
+
- ) : null}
- {replyMediaInfo?.type === "video" ? (
-
-
+
+ {handleImageClick(e); handleThumbnailClick(index, 'reply')}}
+ style={{cursor: "pointer"}}
onError={(e) => e.target.src = fallbackImgUrl} />
- ) : null}
- {replyMediaInfo?.type === "audio" ? (
-
- e.target.src = fallbackImgUrl} />
-
- ) : null}
+
+ ) : null}
+ {replyMediaInfo?.type === "video" ? (
+
+
+ {isReplyThumbnailClicked[index] ? (
+ e.target.src = fallbackImgUrl}
+ />
+ ) : (
+ {handleThumbnailClick(index, 'reply')}}
+ style={{cursor: "pointer"}}
+ onError={(e) => e.target.src = fallbackImgUrl}
+ />
+ )}
+
- ) : null}
+ ) : null}
+ {replyMediaInfo?.type === "audio" ? (
+
+ e.target.src = fallbackImgUrl} />
+
+ ) : null}
+
+ ) : null}
{}} key={`r-pm-${index}`} className="quotelink"
ref={el => {
@@ -1666,7 +1716,7 @@ const Thread = () => {
{commentMediaInfo?.type === 'iframe' && (
- {(isMobileThumbnailClicked[index] || !commentMediaInfo.thumbnail) && commentMediaInfo.embedUrl ? (
+ {(isMobileThreadThumbnailClicked[index] || !commentMediaInfo.thumbnail) && commentMediaInfo.embedUrl ? (
) : commentMediaInfo.type === "video" ? (
- {isMobileThumbnailClicked[index] ? (
+ {isMobileThreadThumbnailClicked[index] ? (
{
key={`fti-${index}`}
src={commentMediaInfo.url}
alt="thumbnail"
- onClick={() => {handleThumbnailClick(index, true)}}
+ onClick={() => {handleThumbnailClick(index, 'mobileThread')}}
style={{cursor: "pointer"}}
id="video-thumbnail-mobile"
onError={(e) => e.target.src = fallbackImgUrl}
/>
)}
{commentMediaInfo?.type === "video" || "iframe" ? (
- isMobileThumbnailClicked[index] ? (
+ isMobileThreadThumbnailClicked[index] ? (
{handleThumbnailClick(index, true)}}
+ onClick={() => {handleThumbnailClick(index, 'mobileThread')}}
>Close
) : (
diff --git a/src/utils/getCommentMediaInfo.js b/src/utils/getCommentMediaInfo.js
index fc26ac3a..de1d367a 100644
--- a/src/utils/getCommentMediaInfo.js
+++ b/src/utils/getCommentMediaInfo.js
@@ -55,7 +55,7 @@ const getCommentMediaInfo = (comment) => {
};
}
- const mime = extName(url.pathname.replace('/', ''))[0]?.mime;
+ const mime = extName(url.pathname.toLowerCase().replace('/', ''))[0]?.mime;
if (mime?.startsWith('image')) {
return {