Skip to content

Commit

Permalink
Load bleeps and their replies
Browse files Browse the repository at this point in the history
asdfsad
  • Loading branch information
Caleb-T-Owens committed Nov 20, 2024
1 parent 3eb57d8 commit ef72e55
Show file tree
Hide file tree
Showing 10 changed files with 225 additions and 19 deletions.
34 changes: 34 additions & 0 deletions apps/desktop/src/lib/navigation/FeedButton.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<script lang="ts">
import DomainButton from '$lib/navigation/DomainButton.svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
interface Props {
href: string;
isNavCollapsed: boolean;
}
const { href, isNavCollapsed }: Props = $props();
const label = 'Feeds';
</script>

<DomainButton
isSelected={$page.url.pathname === href}
{isNavCollapsed}
tooltipLabel={label}
onmousedown={async () => await goto(href)}
>
<img class="icon" src="/images/domain-icons/working-branches.svg" alt="" />
{#if !isNavCollapsed}
<span class="text-14 text-semibold" class:collapsed-txt={isNavCollapsed}>{label}</span>
{/if}
</DomainButton>

<style lang="postcss">
.icon {
border-radius: var(--radius-s);
height: 20px;
width: 20px;
flex-shrink: 0;
}
</style>
2 changes: 2 additions & 0 deletions apps/desktop/src/lib/navigation/Navigation.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import { ModeService } from '$lib/modes/service';
import CloudSeriesButton from '$lib/navigation/CloudSeriesButton.svelte';
import EditButton from '$lib/navigation/EditButton.svelte';
import FeedButton from '$lib/navigation/FeedButton.svelte';
import { platformName } from '$lib/platform/platform';
import { SETTINGS, type Settings } from '$lib/settings/userSettings';
import { createKeybind } from '$lib/utils/hotkeys';
Expand Down Expand Up @@ -125,6 +126,7 @@

{#if $cloudEnabled}
<CloudSeriesButton href={`/${projectId}/series`} isNavCollapsed={$isNavCollapsed} />
<FeedButton href={`/${projectId}/feed`} isNavCollapsed={$isNavCollapsed} />
{/if}
</div>
</div>
Expand Down
41 changes: 41 additions & 0 deletions apps/desktop/src/lib/posts/Post.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script lang="ts">
import { getContext } from '@gitbutler/shared/context';
import Interest from '@gitbutler/shared/redux/interest/Interest.svelte';
import { FeedService } from '@gitbutler/shared/redux/posts/service';
import { postSelectors, postRepliesSelectors } from '@gitbutler/shared/redux/posts/slice';
import { useStore } from '@gitbutler/shared/redux/utils';
type Props = {
postId: string;
};
const { postId }: Props = $props();
const feedService = getContext(FeedService);
const store = useStore();
const postWithRepliesInterest = $derived(feedService.getPostWithRepliesInterest(postId));
const post = $derived(postSelectors.selectById($store, postId));
const postReplies = $derived(postRepliesSelectors.selectById($store, postId));
let postCardRef = $state<HTMLDivElement | undefined>(undefined);
$effect(() => console.log(post));
$effect(() => console.log(postReplies));
</script>

<Interest interest={postWithRepliesInterest} reference={postCardRef} onlyInView />

{#if post}
<div class="card card__content" bind:this={postCardRef}>
<p>{post.uuid}</p>
<p>{post.content}</p>
{#if postReplies}
<p>There is {postReplies.replyIds.length} replies</p>
{:else}
<p>Loading replies count...</p>
{/if}
</div>
{:else}
<p>Loading...</p>
{/if}
25 changes: 24 additions & 1 deletion apps/desktop/src/routes/[projectId]/feed/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,17 +1,40 @@
<script lang="ts">
import Post from '$lib/posts/Post.svelte';
import ScrollableContainer from '$lib/scroll/ScrollableContainer.svelte';
import { getContext } from '@gitbutler/shared/context';
import Interest from '@gitbutler/shared/redux/interest/Interest.svelte';
import { FeedService } from '@gitbutler/shared/redux/posts/service';
import { feedSelectors } from '@gitbutler/shared/redux/posts/slice';
import { useStore } from '@gitbutler/shared/redux/utils';
import Button from '@gitbutler/ui/Button.svelte';
const store = useStore();
const feedService = getContext(FeedService);
const feedInterest = feedService.getFeedInterest();
const feed = $derived(feedSelectors.selectById($store, 'all'));
$inspect(feed);
$effect(() => console.log(feed));
let newPostContent = $state('');
function createPost() {
feedService.createPost(newPostContent);
newPostContent = '';
}
</script>

<Interest interest={feedInterest} />
<ScrollableContainer>
<div>
<input type="text" bind:value={newPostContent} />
<Button onclick={createPost}>Create</Button>
</div>

<div>
{#if feed}
{#each feed.postIds as postId (postId)}
<Post {postId} />
{/each}
{/if}
</div>
</ScrollableContainer>
40 changes: 34 additions & 6 deletions packages/shared/src/lib/redux/interest/Interest.svelte
Original file line number Diff line number Diff line change
@@ -1,17 +1,45 @@
<script lang="ts">
import type { Interest } from '$lib/redux/interest';
import type { Interest } from '$lib/redux/interest/intrestStore';
type Props = {
interest: Interest;
// ref?: HTMLElement;
reference?: HTMLElement;
onlyInView?: boolean;
};
const { interest }: Props = $props();
const { interest, reference: ref, onlyInView }: Props = $props();
let inView = $state(false);
$effect(() => {
if (ref && onlyInView) {
inView = false;
const observer = new IntersectionObserver(
(entries) => {
inView = entries[0].isIntersecting;
},
{
root: null
}
);
observer.observe(ref);
return () => {
inView = false;
observer.disconnect();
};
}
});
$effect(() => {
const unsubscribe = interest._subscribe();
const shouldSubscribe = !onlyInView || inView;
if (interest && shouldSubscribe) {
const unsubscribe = interest._subscribe();
// It is vitally important that we return the unsubscribe function
return unsubscribe;
// It is vitally important that we return the unsubscribe function
return unsubscribe;
}
});
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export interface Interest {
interface Subscription<Arguments> {
args: Arguments;
counter: number;
interval: ReturnType<typeof setInterval>;
interval?: ReturnType<typeof setInterval>;
lastCalled: number;
}

Expand All @@ -16,7 +16,7 @@ export class InterestStore<Arguments> {

constructor(private readonly frequency: number) {}

createInterest(args: Arguments, callback: (args: Arguments) => void): Interest {
createInterest(args: Arguments, callback: () => void): Interest {
return {
_subscribe: () => {
let subscription = this.subscriptions.find((subscription) =>
Expand All @@ -27,23 +27,36 @@ export class InterestStore<Arguments> {
args,
counter: 0,
lastCalled: 0,
interval: setInterval(() => {
callback(args);
}, this.frequency)
interval: undefined
} as Subscription<Arguments>;
}

this.subscriptions.push(subscription);

// Fetch data immediately on first subscription
if (subscription.counter === 0) {
callback(args);
if (Date.now() - subscription.lastCalled > this.frequency) {
subscription.lastCalled = Date.now();
callback();
}

subscription.interval = setInterval(() => {
subscription.lastCalled = Date.now();
callback();
}, this.frequency);
}

++subscription.counter;

let unsubscribed = false;

// Unsubscribe function
return () => {
if (unsubscribed) {
return;
}
unsubscribed = true;

--subscription.counter;
if (subscription.counter <= 0) {
clearInterval(subscription.interval);
Expand Down
31 changes: 27 additions & 4 deletions packages/shared/src/lib/redux/posts/service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { InterestStore } from '$lib/redux/interest';
import { upsertFeed, upsertPost, upsertPosts } from '$lib/redux/posts/slice';
import { apiToPost, type ApiPost, type Post } from '$lib/redux/posts/types';
import { InterestStore } from '$lib/redux/interest/intrestStore';
import { upsertFeed, upsertPost, upsertPostReplies, upsertPosts } from '$lib/redux/posts/slice';
import {
apiToPost,
type ApiPost,
type ApiPostWithReplies,
type Post
} from '$lib/redux/posts/types';
import type { HttpClient } from '$lib/httpClient';
import type { AppDispatch } from '$lib/redux/store';

export class FeedService {
private readonly feedInterests = new InterestStore<undefined>(5 * 60 * 1000);
private readonly postWithRepliesIntrestes = new InterestStore<{ postId: string }>(5 * 60 * 1000);

constructor(
private readonly httpClient: HttpClient,
Expand All @@ -22,7 +28,7 @@ export class FeedService {
}

async createPost(content: string): Promise<Post> {
const apiPost = await this.httpClient.post<ApiPost>('post', { body: { content } });
const apiPost = await this.httpClient.post<ApiPost>('feed/new', { body: { content } });
const post = apiToPost(apiPost);
this.dispatch(upsertPost(post));

Expand All @@ -33,4 +39,21 @@ export class FeedService {

return post;
}

getPostWithRepliesInterest(postId: string) {
return this.postWithRepliesIntrestes.createInterest({ postId }, async () => {
const apiPostWithReplies = await this.httpClient.get<ApiPostWithReplies>(
`feed/post/${postId}`
);
const post = apiToPost(apiPostWithReplies);
const posts = [post, ...apiPostWithReplies.replies.map(apiToPost)];
this.dispatch(upsertPosts(posts));
this.dispatch(
upsertPostReplies({
postId,
replyIds: apiPostWithReplies.replies.map((reply) => reply.uuid)
})
);
});
}
}
34 changes: 33 additions & 1 deletion packages/shared/src/lib/redux/posts/slice.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
import type { Feed, Post } from '$lib/redux/posts/types';
import type { Feed, Post, PostReplies } from '$lib/redux/posts/types';
import type { RootState } from '$lib/redux/store';

const postAdapter = createEntityAdapter({
Expand Down Expand Up @@ -36,6 +36,7 @@ export const {
upsertPosts
} = postSlice.actions;

// Feeds
const feedAdapter = createEntityAdapter({
selectId: (feed: Feed) => feed.identifier,
sortComparer: (a: Feed, b: Feed) => a.identifier.localeCompare(b.identifier)
Expand All @@ -62,3 +63,34 @@ export const feedReducer = feedSlice.reducer;

export const feedSelectors = feedAdapter.getSelectors((state: RootState) => state.feed);
export const { addFeed, updateFeed, removeFeed, upsertFeed } = feedSlice.actions;

// Replies
const postRepliesAdapter = createEntityAdapter({
selectId: (postReplies: PostReplies) => postReplies.postId,
sortComparer: (a: PostReplies, b: PostReplies) => a.postId.localeCompare(b.postId)
});

const postRepliesSlice = createSlice({
name: 'postReplies',
initialState: postRepliesAdapter.getInitialState(),
reducers: {
addPostReplies: postRepliesAdapter.addOne,
updatePostReplies: postRepliesAdapter.updateOne,
removePostReplies: postRepliesAdapter.removeOne,
upsertPostReplies: postRepliesAdapter.upsertOne,
addReply: (state, action) => {
const feed = state.entities[action.payload.postId];
if (feed) {
feed.replyIds.unshift(action.payload.postId);
}
}
}
});

export const postRepliesReducer = postRepliesSlice.reducer;

export const postRepliesSelectors = postRepliesAdapter.getSelectors(
(state: RootState) => state.postReplies
);
export const { addPostReplies, updatePostReplies, removePostReplies, upsertPostReplies } =
postRepliesSlice.actions;
9 changes: 9 additions & 0 deletions packages/shared/src/lib/redux/posts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export type ApiPost = {
// TODO target:
};

export type ApiPostWithReplies = ApiPost & {
replies: ApiPost[];
};

export type Post = {
uuid: string;
content: string;
Expand All @@ -31,3 +35,8 @@ export type Feed = {
identifier: string;
postIds: string[];
};

export type PostReplies = {
postId: string;
replyIds: string[];
};
3 changes: 2 additions & 1 deletion packages/shared/src/lib/redux/store.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { exampleReducer } from '$lib/redux/example';
import { feedReducer, postReducer } from '$lib/redux/posts/slice';
import { feedReducer, postReducer, postRepliesReducer } from '$lib/redux/posts/slice';
import { configureStore } from '@reduxjs/toolkit';

/**
Expand All @@ -12,6 +12,7 @@ export const _store = configureStore({
reducer: {
example: exampleReducer,
post: postReducer,
postReplies: postRepliesReducer,
feed: feedReducer
}
});
Expand Down

0 comments on commit ef72e55

Please sign in to comment.