Добавлен признак новых сообщений

This commit is contained in:
Pavel Gnedov 2023-06-20 02:57:54 +07:00
parent e79a7cd492
commit 0dc7e0c4ad
6 changed files with 113 additions and 3 deletions

View file

@ -6,20 +6,24 @@ import * as TimePassedNs from '../misc-components/time-passed';
import * as TagsNs from '../misc-components/tags';
import * as IssueHrefNs from '../misc-components/issue-href';
import * as IssueDetailsDialogNs from '../misc-components/issue-details-dialog';
import * as UnreadedFlagNs from '../misc-components/unreaded-flag';
export type Props = {
store: IIssueStore
};
export const IssuesListCard = observer((props: Props): JSX.Element => {
const unreadedStore = UnreadedFlagNs.CreateStoreFromLocalStorage(props.store);
const detailsStore = IssueDetailsDialogNs.Store.create({
issue: props.store,
visible: false
visible: false,
unreadedFlagStore: unreadedStore
});
return (
<div className={Css.listItem} onClick={(e) => {e.stopPropagation(); detailsStore.show();}}>
<IssueDetailsDialogNs.IssueDetailsDialog store={detailsStore}/>
<div>
<UnreadedFlagNs.UnreadedFlag store={unreadedStore}/>
<TimePassedNs.TimePassed params={{ fromIssue: { issue: props.store, keyName: 'timePassedClass' } }}/>
<span className={Css.issueSubject}>
<IssueHrefNs.IssueHref

View file

@ -6,6 +6,7 @@ import { getStyleObjectFromString } from '../utils/style';
import * as TimePassedNs from '../misc-components/time-passed';
import * as TagsNs from '../misc-components/tags';
import * as IssueDetailsDialogNs from '../misc-components/issue-details-dialog';
import * as UnreadedFlagNs from '../misc-components/unreaded-flag';
export type Props = {
store: ICardStore
@ -50,14 +51,17 @@ export const KanbanCard = observer((props: Props) => {
keyName: 'timePassedClass'
}
}
const unreadedStore = UnreadedFlagNs.CreateStoreFromLocalStorage(props.store.issue);
const detailsStore = IssueDetailsDialogNs.Store.create({
issue: props.store.issue,
visible: false,
})
unreadedFlagStore: unreadedStore
});
return (
<div className={KanbanCardCss.kanbanCard} onClick={(e) => {e.stopPropagation(); detailsStore.show();}}>
<IssueDetailsDialogNs.IssueDetailsDialog store={detailsStore} />
<div className={KanbanCardCss.kanbanCardTitle}>
<UnreadedFlagNs.UnreadedFlag store={unreadedStore}/>
<TimePassedNs.TimePassed params={timePassedParams}/>
<a href={props.store.issue.url.url}>{props.store.issue.tracker.name} #{props.store.issue.id} - {props.store.issue.subject}</a>
</div>

View file

@ -4,10 +4,13 @@ import { observer } from 'mobx-react-lite';
import { Instance, types } from 'mobx-state-tree';
import * as IssueHrefNs from '../misc-components/issue-href';
import Css from './issue-details-dialog.module.css';
import * as UnreadedFlagNs from '../misc-components/unreaded-flag';
import { SetIssueReadingTimestamp } from '../utils/unreaded-provider';
export const Store = types.model({
visible: types.boolean,
issue: types.frozen<RedmineTypes.ExtendedIssue>()
issue: types.frozen<RedmineTypes.ExtendedIssue>(),
unreadedFlagStore: types.maybe(UnreadedFlagNs.Store)
}).actions((self) => {
return {
hide: () => {
@ -17,6 +20,11 @@ export const Store = types.model({
show: () => {
console.debug(`Issue details dialog show: issue_id=${self.issue.id}`); // DEBUG
self.visible = true;
if (self.unreadedFlagStore) {
self.unreadedFlagStore.read();
} else {
SetIssueReadingTimestamp(self.issue.id);
}
}
};
}).views((self) => {

View file

@ -0,0 +1,16 @@
.circle {
height: 6px;
width: 6px;
background-color: #707070;
border: 2px solid #707070;
border-radius: 50%;
display: inline-block;
}
.circle.unreaded {
border: 2px solid #0000FF;
}
.circle.forMe {
background-color: #FF0000;
}

View file

@ -0,0 +1,64 @@
import React from 'react';
import Css from './unreaded-flag.module.css';
import { observer } from 'mobx-react-lite';
import { Instance, types } from 'mobx-state-tree';
import { RedmineTypes } from '../redmine-types';
import { GetIssueReadingTimestamp, SetIssueReadingTimestamp } from '../utils/unreaded-provider';
export const Store = types.model({
issue: types.frozen<RedmineTypes.ExtendedIssue>(),
readingTimestamp: types.number
}).actions((self) => {
return {
read: () => {
self.readingTimestamp = SetIssueReadingTimestamp(self.issue.id);
}
};
}).views((self) => {
return {
getUpdatedTimestap(): number {
if (self.issue.journals) {
let lastComment: RedmineTypes.Journal | undefined;
for (let i = self.issue.journals.length - 1; i >= 0; i--) {
const journal = self.issue.journals[i];
if (journal.notes) {
lastComment = journal;
break;
}
}
if (lastComment) {
return (new Date(lastComment.created_on)).getTime();
}
}
return 0;
},
getClassName(): string {
let className = Css.circle;
const updatedTimestamp = this.getUpdatedTimestap();
if (self.readingTimestamp < updatedTimestamp) {
className += ` ${Css.unreaded}`;
}
console.debug(`Unreaded flag getClassName: issueId=${self.issue.id}; readingTimestamp=${self.readingTimestamp}; updatedTimestamp=${updatedTimestamp}; className=${className}`); // DEBUG
return className;
}
};
});
export function CreateStoreFromLocalStorage(issue: RedmineTypes.ExtendedIssue) {
const timestamp = GetIssueReadingTimestamp(issue.id);
return Store.create({
issue: issue,
readingTimestamp: timestamp
});
}
export type Props = {
store: Instance<typeof Store>
}
export const UnreadedFlag = observer((props: Props): JSX.Element => {
const className = props.store.getClassName();
return (
<span className={className} onClick={(e) => {e.stopPropagation(); props.store.read();}}></span>
);
})

View file

@ -0,0 +1,14 @@
export function GetIssueReadingTimestamp(issueId: number): number {
const value = window.localStorage.getItem(getKey(issueId));
return value ? Number(value) : 0;
}
export function SetIssueReadingTimestamp(issueId: number): number {
const now = (new Date()).getTime();
window.localStorage.setItem(getKey(issueId), String(now));
return now;
}
function getKey(issueId: number): string {
return `issue_read_${issueId}`;
}