Commit 7d5f6034 authored by Steven's avatar Steven

chore: update compact view

parent f0a521f5
......@@ -161,8 +161,6 @@ message UserSetting {
string memo_visibility = 4;
// The telegram user id of the user.
string telegram_user_id = 5;
// The compact view for a memo.
bool compact_view = 6;
}
message GetUserSettingRequest {
......
......@@ -718,7 +718,6 @@ Used internally for obfuscating the page token.
| appearance | [string](#string) | | The preferred appearance of the user. |
| memo_visibility | [string](#string) | | The default visibility of the memo. |
| telegram_user_id | [string](#string) | | The telegram user id of the user. |
| compact_view | [bool](#bool) | | The compact view for a memo. |
......
This diff is collapsed.
......@@ -289,7 +289,6 @@
| appearance | [string](#string) | | |
| memo_visibility | [string](#string) | | |
| telegram_user_id | [string](#string) | | |
| compact_view | [bool](#bool) | | |
......@@ -311,7 +310,6 @@
| USER_SETTING_APPEARANCE | 3 | The appearance of the user. |
| USER_SETTING_MEMO_VISIBILITY | 4 | The visibility of the memo. |
| USER_SETTING_TELEGRAM_USER_ID | 5 | The telegram user id of the user. |
| USER_SETTING_COMPACT_VIEW | 6 | The compact view for a memo. |
......
This diff is collapsed.
......@@ -16,8 +16,6 @@ enum UserSettingKey {
USER_SETTING_MEMO_VISIBILITY = 4;
// The telegram user id of the user.
USER_SETTING_TELEGRAM_USER_ID = 5;
// The compact view for a memo.
USER_SETTING_COMPACT_VIEW = 6;
}
message UserSetting {
......@@ -29,7 +27,6 @@ message UserSetting {
string appearance = 5;
string memo_visibility = 6;
string telegram_user_id = 7;
bool compact_view = 8;
}
}
......
......@@ -1437,9 +1437,6 @@ paths:
telegramUserId:
type: string
description: The telegram user id of the user.
compactView:
type: boolean
description: The compact view for a memo.
tags:
- UserService
/api/v2/{user.name}:
......@@ -1626,9 +1623,6 @@ definitions:
telegramUserId:
type: string
description: The telegram user id of the user.
compactView:
type: boolean
description: The compact view for a memo.
apiv2Webhook:
type: object
properties:
......
......@@ -219,7 +219,6 @@ func getDefaultUserSetting() *apiv2pb.UserSetting {
Locale: "en",
Appearance: "system",
MemoVisibility: "PRIVATE",
CompactView: false,
}
}
......@@ -245,8 +244,6 @@ func (s *APIV2Service) GetUserSetting(ctx context.Context, _ *apiv2pb.GetUserSet
userSettingMessage.MemoVisibility = setting.GetMemoVisibility()
} else if setting.Key == storepb.UserSettingKey_USER_SETTING_TELEGRAM_USER_ID {
userSettingMessage.TelegramUserId = setting.GetTelegramUserId()
} else if setting.Key == storepb.UserSettingKey_USER_SETTING_COMPACT_VIEW {
userSettingMessage.CompactView = setting.GetCompactView()
}
}
return &apiv2pb.GetUserSettingResponse{
......@@ -305,16 +302,6 @@ func (s *APIV2Service) UpdateUserSetting(ctx context.Context, request *apiv2pb.U
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to upsert user setting: %v", err)
}
} else if field == "compact_view" {
if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{
UserId: user.ID,
Key: storepb.UserSettingKey_USER_SETTING_COMPACT_VIEW,
Value: &storepb.UserSetting_CompactView{
CompactView: request.Setting.CompactView,
},
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to upsert user setting: %v", err)
}
} else {
return nil, status.Errorf(codes.InvalidArgument, "invalid update path: %s", field)
}
......
......@@ -3,7 +3,6 @@ package mysql
import (
"context"
"database/sql"
"strconv"
"strings"
"github.com/pkg/errors"
......@@ -30,8 +29,6 @@ func (d *DB) UpsertUserSetting(ctx context.Context, upsert *storepb.UserSetting)
valueString = upsert.GetMemoVisibility()
} else if upsert.Key == storepb.UserSettingKey_USER_SETTING_TELEGRAM_USER_ID {
valueString = upsert.GetTelegramUserId()
} else if upsert.Key == storepb.UserSettingKey_USER_SETTING_COMPACT_VIEW {
valueString = strconv.FormatBool(upsert.GetCompactView())
} else {
return nil, errors.Errorf("unknown user setting key: %s", upsert.Key.String())
}
......@@ -96,14 +93,6 @@ func (d *DB) ListUserSettings(ctx context.Context, find *store.FindUserSetting)
userSetting.Value = &storepb.UserSetting_TelegramUserId{
TelegramUserId: valueString,
}
} else if userSetting.Key == storepb.UserSettingKey_USER_SETTING_COMPACT_VIEW {
compactView, err := strconv.ParseBool(valueString)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse compact view value: %s", valueString)
}
userSetting.Value = &storepb.UserSetting_CompactView{
CompactView: compactView,
}
} else {
// Skip unknown user setting key.
continue
......
......@@ -3,7 +3,6 @@ package postgres
import (
"context"
"database/sql"
"strconv"
"strings"
"github.com/pkg/errors"
......@@ -37,8 +36,6 @@ func (d *DB) UpsertUserSetting(ctx context.Context, upsert *storepb.UserSetting)
valueString = upsert.GetMemoVisibility()
} else if upsert.Key == storepb.UserSettingKey_USER_SETTING_TELEGRAM_USER_ID {
valueString = upsert.GetTelegramUserId()
} else if upsert.Key == storepb.UserSettingKey_USER_SETTING_COMPACT_VIEW {
valueString = strconv.FormatBool(upsert.GetCompactView())
} else {
return nil, errors.Errorf("unknown user setting key: %s", upsert.Key.String())
}
......@@ -109,14 +106,6 @@ func (d *DB) ListUserSettings(ctx context.Context, find *store.FindUserSetting)
userSetting.Value = &storepb.UserSetting_TelegramUserId{
TelegramUserId: valueString,
}
} else if userSetting.Key == storepb.UserSettingKey_USER_SETTING_COMPACT_VIEW {
compactView, err := strconv.ParseBool(valueString)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse compact view value: %s", valueString)
}
userSetting.Value = &storepb.UserSetting_CompactView{
CompactView: compactView,
}
} else {
// Skip unknown user setting key.
continue
......
......@@ -3,7 +3,6 @@ package sqlite
import (
"context"
"database/sql"
"strconv"
"strings"
"github.com/pkg/errors"
......@@ -37,8 +36,6 @@ func (d *DB) UpsertUserSetting(ctx context.Context, upsert *storepb.UserSetting)
valueString = upsert.GetMemoVisibility()
} else if upsert.Key == storepb.UserSettingKey_USER_SETTING_TELEGRAM_USER_ID {
valueString = upsert.GetTelegramUserId()
} else if upsert.Key == storepb.UserSettingKey_USER_SETTING_COMPACT_VIEW {
valueString = strconv.FormatBool(upsert.GetCompactView())
} else {
return nil, errors.Errorf("unknown user setting key: %s", upsert.Key.String())
}
......@@ -109,14 +106,6 @@ func (d *DB) ListUserSettings(ctx context.Context, find *store.FindUserSetting)
userSetting.Value = &storepb.UserSetting_TelegramUserId{
TelegramUserId: valueString,
}
} else if userSetting.Key == storepb.UserSettingKey_USER_SETTING_COMPACT_VIEW {
compactView, err := strconv.ParseBool(valueString)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse compact view value: %s", valueString)
}
userSetting.Value = &storepb.UserSetting_CompactView{
CompactView: compactView,
}
} else {
// Skip unknown user setting key.
continue
......
......@@ -363,6 +363,7 @@
"syscall/js.finalizeRef": (v_ref) => {
// Note: TinyGo does not support finalizers so this should never be
// called.
console.warn("syscall/js.finalizeRef not implemented");
},
// func stringVal(value string) ref
......
......@@ -28,10 +28,13 @@ const CodeBlock: React.FC<Props> = ({ language, content }: Props) => {
let highlightedCode = content;
try {
const temp = hljs.highlight(content, {
language: formatedLanguage,
}).value;
highlightedCode = temp;
const lang = hljs.getLanguage(formatedLanguage);
if (lang) {
const temp = hljs.highlight(content, {
language: formatedLanguage,
}).value;
highlightedCode = temp;
}
} catch (error) {
// Skip error and use default highlighted code.
}
......
......@@ -62,67 +62,66 @@ interface Props {
node: Node;
}
const Renderer: React.FC<Props> = ({ index, node: rawNode }: Props) => {
const { type, node } = rawNode;
switch (type) {
const Renderer: React.FC<Props> = ({ index, node }: Props) => {
switch (node.type) {
case NodeType.LINE_BREAK:
return <LineBreak index={index} />;
case NodeType.PARAGRAPH:
return <Paragraph index={index} {...(node as ParagraphNode)} />;
return <Paragraph index={index} {...(node.value as ParagraphNode)} />;
case NodeType.CODE_BLOCK:
return <CodeBlock index={index} {...(node as CodeBlockNode)} />;
return <CodeBlock index={index} {...(node.value as CodeBlockNode)} />;
case NodeType.HEADING:
return <Heading index={index} {...(node as HeadingNode)} />;
return <Heading index={index} {...(node.value as HeadingNode)} />;
case NodeType.HORIZONTAL_RULE:
return <HorizontalRule index={index} {...(node as HorizontalRuleNode)} />;
return <HorizontalRule index={index} {...(node.value as HorizontalRuleNode)} />;
case NodeType.BLOCKQUOTE:
return <Blockquote index={index} {...(node as BlockquoteNode)} />;
return <Blockquote index={index} {...(node.value as BlockquoteNode)} />;
case NodeType.ORDERED_LIST:
return <OrderedList index={index} {...(node as OrderedListNode)} />;
return <OrderedList index={index} {...(node.value as OrderedListNode)} />;
case NodeType.UNORDERED_LIST:
return <UnorderedList {...(node as UnorderedListNode)} />;
return <UnorderedList {...(node.value as UnorderedListNode)} />;
case NodeType.TASK_LIST:
return <TaskList index={index} {...(node as TaskListNode)} />;
return <TaskList index={index} {...(node.value as TaskListNode)} />;
case NodeType.MATH_BLOCK:
return <Math {...(node as MathNode)} block={true} />;
return <Math {...(node.value as MathNode)} block={true} />;
case NodeType.TABLE:
return <Table {...(node as TableNode)} />;
return <Table {...(node.value as TableNode)} />;
case NodeType.EMBEDDED_CONTENT:
return <EmbeddedContent {...(node as EmbeddedContentNode)} />;
return <EmbeddedContent {...(node.value as EmbeddedContentNode)} />;
case NodeType.TEXT:
return <Text {...(node as TextNode)} />;
return <Text {...(node.value as TextNode)} />;
case NodeType.BOLD:
return <Bold {...(node as BoldNode)} />;
return <Bold {...(node.value as BoldNode)} />;
case NodeType.ITALIC:
return <Italic {...(node as ItalicNode)} />;
return <Italic {...(node.value as ItalicNode)} />;
case NodeType.BOLD_ITALIC:
return <BoldItalic {...(node as BoldItalicNode)} />;
return <BoldItalic {...(node.value as BoldItalicNode)} />;
case NodeType.CODE:
return <Code {...(node as CodeNode)} />;
return <Code {...(node.value as CodeNode)} />;
case NodeType.IMAGE:
return <Image {...(node as ImageNode)} />;
return <Image {...(node.value as ImageNode)} />;
case NodeType.LINK:
return <Link {...(node as LinkNode)} />;
return <Link {...(node.value as LinkNode)} />;
case NodeType.AUTO_LINK:
return <Link {...(node as AutoLinkNode)} />;
return <Link {...(node.value as AutoLinkNode)} />;
case NodeType.TAG:
return <Tag {...(node as TagNode)} />;
return <Tag {...(node.value as TagNode)} />;
case NodeType.STRIKETHROUGH:
return <Strikethrough {...(node as StrikethroughNode)} />;
return <Strikethrough {...(node.value as StrikethroughNode)} />;
case NodeType.MATH:
return <Math {...(node as MathNode)} />;
return <Math {...(node.value as MathNode)} />;
case NodeType.HIGHLIGHT:
return <Highlight {...(node as HighlightNode)} />;
return <Highlight {...(node.value as HighlightNode)} />;
case NodeType.ESCAPING_CHARACTER:
return <EscapingCharacter {...(node as EscapingCharacterNode)} />;
return <EscapingCharacter {...(node.value as EscapingCharacterNode)} />;
case NodeType.SUBSCRIPT:
return <Subscript {...(node as SubscriptNode)} />;
return <Subscript {...(node.value as SubscriptNode)} />;
case NodeType.SUPERSCRIPT:
return <Superscript {...(node as SuperscriptNode)} />;
return <Superscript {...(node.value as SuperscriptNode)} />;
case NodeType.REFERENCED_CONTENT:
return <ReferencedContent {...(node as ReferencedContentNode)} />;
return <ReferencedContent {...(node.value as ReferencedContentNode)} />;
case NodeType.SPOILER:
return <Spoiler {...(node as SpoilerNode)} />;
return <Spoiler {...(node.value as SpoilerNode)} />;
default:
return null;
}
......
......@@ -31,11 +31,11 @@ const TaskList: React.FC<Props> = ({ index, indent, complete, children }: Props)
}
const node = context.nodes[nodeIndex];
if (node.type !== NodeType.TASK_LIST || !node.node) {
if (node.type !== NodeType.TASK_LIST || !node.value) {
return;
}
(node.node as TaskListNode)!.complete = on;
(node.value as TaskListNode)!.complete = on;
const content = window.restore(context.nodes);
await memoStore.updateMemo(
{
......
import { memo, useRef } from "react";
import classNames from "classnames";
import { memo, useEffect, useRef, useState } from "react";
import { Link } from "react-router-dom";
import useCurrentUser from "@/hooks/useCurrentUser";
import { useMemoStore } from "@/store/v1";
import { Node, NodeType } from "@/types/node";
import { useTranslate } from "@/utils/i18n";
import Icon from "../Icon";
import Renderer from "./Renderer";
import { RendererContext } from "./types";
// MAX_DISPLAY_HEIGHT is the maximum height of the memo content to display in compact mode.
const MAX_DISPLAY_HEIGHT = 256;
interface Props {
content: string;
memoId?: number;
compact?: boolean;
readonly?: boolean;
disableFilter?: boolean;
// embeddedMemos is a set of memo resource names that are embedded in the current memo.
......@@ -19,11 +27,28 @@ interface Props {
const MemoContent: React.FC<Props> = (props: Props) => {
const { className, content, memoId, embeddedMemos, onClick } = props;
const t = useTranslate();
const currentUser = useCurrentUser();
const memoStore = useMemoStore();
const memoContentContainerRef = useRef<HTMLDivElement>(null);
const [showCompactMode, setShowCompactMode] = useState<boolean>(false);
const memo = memoId ? memoStore.getMemoById(memoId) : null;
const nodes = window.parse(content);
const allowEdit = !props.readonly && memoId && currentUser?.id === memoStore.getMemoById(memoId)?.creatorId;
const allowEdit = !props.readonly && memo && currentUser?.id === memo.creatorId;
// Initial compact mode.
useEffect(() => {
if (!props.compact) {
return;
}
if (!memoContentContainerRef.current) {
return;
}
if ((memoContentContainerRef.current as HTMLDivElement).getBoundingClientRect().height > MAX_DISPLAY_HEIGHT) {
setShowCompactMode(true);
}
}, []);
const handleMemoContentClick = async (e: React.MouseEvent) => {
if (onClick) {
......@@ -35,34 +60,50 @@ const MemoContent: React.FC<Props> = (props: Props) => {
let skipNextLineBreakFlag = false;
return (
<RendererContext.Provider
value={{
nodes,
memoId,
readonly: !allowEdit,
disableFilter: props.disableFilter,
embeddedMemos: embeddedMemos || new Set(),
}}
>
<div className={`w-full flex flex-col justify-start items-start text-gray-800 dark:text-gray-300 ${className || ""}`}>
<div
ref={memoContentContainerRef}
className="w-full max-w-full word-break text-base leading-6 space-y-1 whitespace-pre-wrap"
onClick={handleMemoContentClick}
>
{nodes.map((node, index) => {
if (prevNode?.type !== NodeType.LINE_BREAK && node.type === NodeType.LINE_BREAK && skipNextLineBreakFlag) {
skipNextLineBreakFlag = false;
return null;
}
<>
<RendererContext.Provider
value={{
nodes,
memoId,
readonly: !allowEdit,
disableFilter: props.disableFilter,
embeddedMemos: embeddedMemos || new Set(),
}}
>
<div className={`w-full flex flex-col justify-start items-start text-gray-800 dark:text-gray-300 ${className || ""}`}>
<div
ref={memoContentContainerRef}
className={classNames(
"w-full max-w-full word-break text-base leading-6 space-y-1 whitespace-pre-wrap",
showCompactMode && "line-clamp-6",
)}
onClick={handleMemoContentClick}
>
{nodes.map((node, index) => {
if (prevNode?.type !== NodeType.LINE_BREAK && node.type === NodeType.LINE_BREAK && skipNextLineBreakFlag) {
skipNextLineBreakFlag = false;
return null;
}
prevNode = node;
skipNextLineBreakFlag = true;
return <Renderer key={`${node.type}-${index}`} index={String(index)} node={node} />;
})}
prevNode = node;
skipNextLineBreakFlag = true;
return <Renderer key={`${node.type}-${index}`} index={String(index)} node={node} />;
})}
</div>
</div>
</RendererContext.Provider>
{memo && showCompactMode && (
<div className="w-full mt-2">
<Link
className="w-auto inline-flex flex-row justify-start items-center text-sm text-blue-600 dark:text-blue-400 hover:underline"
to={`/m/${memo.name}`}
>
<span>{t("memo.show-more")}</span>
<Icon.ChevronRight className="w-4 h-auto" />
</Link>
</div>
</div>
</RendererContext.Provider>
)}
</>
);
};
......
......@@ -42,7 +42,6 @@ const MemoView: React.FC<Props> = (props: Props) => {
const [displayTime, setDisplayTime] = useState<string>(getRelativeTimeString(getTimeStampByDate(memo.displayTime)));
const [creator, setCreator] = useState(userStore.getUserByUsername(extractUsernameFromName(memo.creator)));
const memoContainerRef = useRef<HTMLDivElement>(null);
const [showCompactMode, setShowCompactMode] = useState(false);
const referencedMemos = memo.relations.filter((relation) => relation.type === MemoRelation_Type.REFERENCE);
const commentAmount = memo.relations.filter((relation) => relation.type === MemoRelation_Type.COMMENT).length;
const readonly = memo.creator !== user?.name;
......@@ -55,16 +54,6 @@ const MemoView: React.FC<Props> = (props: Props) => {
})();
}, []);
// Initial compact mode.
useEffect(() => {
if (!memoContainerRef.current) {
return;
}
if ((memoContainerRef.current as HTMLDivElement).getBoundingClientRect().height > 512) {
setShowCompactMode(true);
}
}, []);
// Update display time string.
useEffect(() => {
let intervalFlag: any = -1;
......@@ -163,24 +152,13 @@ const MemoView: React.FC<Props> = (props: Props) => {
</div>
</div>
<MemoContent
className={showCompactMode ? "!line-clamp-6" : ""}
key={`${memo.id}-${memo.updateTime}`}
memoId={memo.id}
content={memo.content}
readonly={readonly}
onClick={handleMemoContentClick}
compact={true}
/>
{showCompactMode && (
<div className="w-full mt-2">
<Link
className="w-auto flex flex-row justify-start items-center text-sm text-blue-600 dark:text-blue-400 hover:underline"
to={`/m/${memo.name}`}
>
<span>{t("memo.show-more")}</span>
<Icon.ChevronRight className="w-4 h-auto" />
</Link>
</div>
)}
<MemoResourceListView resources={memo.resources} />
<MemoRelationListView memo={memo} relations={referencedMemos} />
<MemoReactionistView memo={memo} reactions={memo.reactions} />
......
......@@ -216,7 +216,7 @@ const SystemSection = () => {
<div className="w-full flex flex-col gap-2 pt-2 pb-4">
<p className="font-medium text-gray-700 dark:text-gray-500">{t("common.basic")}</p>
<div className="w-full flex flex-row justify-between items-center">
<div className="normal-text">
<div>
{t("setting.system-section.server-name")}: <span className="font-mono font-bold">{systemStatus.customizedProfile.name}</span>
</div>
<Button onClick={handleUpdateCustomizedProfileButtonClick}>{t("common.edit")}</Button>
......@@ -267,7 +267,7 @@ const SystemSection = () => {
</div>
<div className="space-y-2 border rounded-md py-2 px-3 dark:border-zinc-700">
<div className="w-full flex flex-row justify-between items-center">
<span className="normal-text">{t("setting.system-section.additional-style")}</span>
<span>{t("setting.system-section.additional-style")}</span>
<Button variant="outlined" color="neutral" onClick={handleSaveAdditionalStyle}>
{t("common.save")}
</Button>
......@@ -285,7 +285,7 @@ const SystemSection = () => {
onChange={(event) => handleAdditionalStyleChanged(event.target.value)}
/>
<div className="w-full flex flex-row justify-between items-center">
<span className="normal-text">{t("setting.system-section.additional-script")}</span>
<span>{t("setting.system-section.additional-script")}</span>
<Button variant="outlined" color="neutral" onClick={handleSaveAdditionalScript}>
{t("common.save")}
</Button>
......@@ -317,16 +317,16 @@ const SystemSection = () => {
<Divider className="!my-3" />
<p className="font-medium text-gray-700 dark:text-gray-500">Others</p>
<div className="w-full flex flex-row justify-between items-center">
<span className="normal-text">{t("setting.system-section.disable-public-memos")}</span>
<span>{t("setting.system-section.disable-public-memos")}</span>
<Switch checked={state.disablePublicMemos} onChange={(event) => handleDisablePublicMemosChanged(event.target.checked)} />
</div>
<div className="w-full flex flex-row justify-between items-center">
<span className="normal-text">{t("setting.system-section.display-with-updated-time")}</span>
<span>{t("setting.system-section.display-with-updated-time")}</span>
<Switch checked={state.memoDisplayWithUpdatedTs} onChange={(event) => handleMemoDisplayWithUpdatedTs(event.target.checked)} />
</div>
<div className="w-full flex flex-row justify-between items-center">
<div className="flex flex-row items-center">
<span className="text-sm mr-1">{t("setting.system-section.max-upload-size")}</span>
<span className="mr-1">{t("setting.system-section.max-upload-size")}</span>
<Tooltip title={t("setting.system-section.max-upload-size-hint")} placement="top">
<Icon.HelpCircle className="w-4 h-auto" />
</Tooltip>
......
......@@ -32,7 +32,7 @@ export enum NodeType {
export interface Node {
type: NodeType;
node:
value:
| LineBreakNode
| ParagraphNode
| CodeBlockNode
......
import { Node } from "@/types/node";
import { Node, TagNode } from "@/types/node";
export const TAG_REG = /#([^\s#,]+)/;
......@@ -15,7 +15,7 @@ export const extractTagsFromContent = (content: string) => {
handle(node);
if (node.type === "PARAGRAPH" || node.type === "ORDERED_LIST" || node.type === "UNORDERED_LIST") {
const children = (node.node as any).children;
const children = (node.value as any).children;
if (Array.isArray(children)) {
traverse(children, handle);
}
......@@ -24,8 +24,8 @@ export const extractTagsFromContent = (content: string) => {
};
traverse(nodes, (node) => {
if (node.type === "TAG" && node.node) {
tags.add((node.node as any).content);
if (node.type === "TAG" && node.value) {
tags.add((node.value as TagNode).content);
}
});
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment