Introduce the group activity section

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
chapril-v3
Thomas Citharel 2 years ago
parent d0567f783d
commit 3fe64a4389
No known key found for this signature in database
GPG Key ID: A061B9DDE0CA0773

@ -261,7 +261,7 @@ config :mobilizon, :anonymous,
config :mobilizon, Oban,
repo: Mobilizon.Storage.Repo,
log: false,
queues: [default: 10, search: 5, mailers: 10, background: 5],
queues: [default: 10, search: 5, mailers: 10, background: 5, activity: 5],
plugins: [
{Oban.Plugins.Cron,
crontab: [

@ -0,0 +1,119 @@
<template>
<div class="activity-item">
<b-icon :icon="'chat'" :type="iconColor" />
<div class="subject">
<i18n :path="translation" tag="p">
<router-link
v-if="activity.object"
slot="discussion"
:to="{
name: RouteName.DISCUSSION,
params: { slug: subjectParams.discussion_slug },
}"
>{{ subjectParams.discussion_title }}</router-link
>
<b v-else slot="discussion">{{ subjectParams.discussion_title }}</b>
<router-link
v-if="activity.object && subjectParams.old_discussion_title"
slot="old_discussion"
:to="{
name: RouteName.DISCUSSION,
params: { slug: subjectParams.discussion_slug },
}"
>{{ subjectParams.old_discussion_title }}</router-link
>
<b
v-else-if="subjectParams.old_discussion_title"
slot="old_discussion"
>{{ subjectParams.old_discussion_title }}</b
>
<popover-actor-card
:actor="activity.author"
:inline="true"
slot="profile"
>
<b>
{{
$t("@{username}", {
username: usernameWithDomain(activity.author),
})
}}</b
></popover-actor-card
></i18n
>
<small class="has-text-grey activity-date">{{
activity.insertedAt | formatTimeString
}}</small>
</div>
</div>
</template>
<script lang="ts">
import { usernameWithDomain } from "@/types/actor";
import { ActivityDiscussionSubject } from "@/types/enums";
import { Component } from "vue-property-decorator";
import RouteName from "../../router/name";
import PopoverActorCard from "../Account/PopoverActorCard.vue";
import ActivityMixin from "../../mixins/activity";
import { mixins } from "vue-class-component";
@Component({
components: {
PopoverActorCard,
},
})
export default class DiscussionActivityItem extends mixins(ActivityMixin) {
usernameWithDomain = usernameWithDomain;
RouteName = RouteName;
ActivityDiscussionSubject = ActivityDiscussionSubject;
get translation(): string | undefined {
switch (this.activity.subject) {
case ActivityDiscussionSubject.DISCUSSION_CREATED:
if (this.isAuthorCurrentActor) {
return "You created the discussion {discussion}.";
}
return "{profile} created the discussion {discussion}.";
case ActivityDiscussionSubject.DISCUSSION_REPLIED:
if (this.isAuthorCurrentActor) {
return "You replied to the discussion {discussion}.";
}
return "{profile} replied to the discussion {discussion}.";
case ActivityDiscussionSubject.DISCUSSION_RENAMED:
if (this.isAuthorCurrentActor) {
return "You renamed the discussion from {old_discussion} to {discussion}.";
}
return "{profile} renamed the discussion from {old_discussion} to {discussion}.";
case ActivityDiscussionSubject.DISCUSSION_ARCHIVED:
if (this.isAuthorCurrentActor) {
return "You archived the discussion {discussion}.";
}
return "{profile} archived the discussion {discussion}.";
case ActivityDiscussionSubject.DISCUSSION_DELETED:
if (this.isAuthorCurrentActor) {
return "You deleted the discussion {discussion}.";
}
return "{profile} deleted the discussion {discussion}.";
default:
return undefined;
}
}
get iconColor(): string | undefined {
switch (this.activity.subject) {
case ActivityDiscussionSubject.DISCUSSION_CREATED:
case ActivityDiscussionSubject.DISCUSSION_REPLIED:
return "is-success";
case ActivityDiscussionSubject.DISCUSSION_RENAMED:
case ActivityDiscussionSubject.DISCUSSION_ARCHIVED:
return "is-grey";
case ActivityDiscussionSubject.DISCUSSION_DELETED:
return "is-danger";
default:
return undefined;
}
}
}
</script>
<style lang="scss" scoped>
@import "./activity.scss";
</style>

@ -0,0 +1,93 @@
<template>
<div class="activity-item">
<b-icon :icon="'calendar'" :type="iconColor" />
<div class="subject">
<i18n :path="translation" tag="p">
<router-link
slot="event"
v-if="activity.object"
:to="{
name: RouteName.EVENT,
params: { uuid: subjectParams.event_uuid },
}"
>{{ subjectParams.event_title }}</router-link
>
<b v-else slot="event">{{ subjectParams.event_title }}</b>
<popover-actor-card
:actor="activity.author"
:inline="true"
slot="profile"
>
<b>
{{
$t("@{username}", {
username: usernameWithDomain(activity.author),
})
}}</b
></popover-actor-card
></i18n
>
<small class="has-text-grey activity-date">{{
activity.insertedAt | formatTimeString
}}</small>
</div>
</div>
</template>
<script lang="ts">
import { usernameWithDomain } from "@/types/actor";
import { ActivityEventSubject } from "@/types/enums";
import { mixins } from "vue-class-component";
import { Component } from "vue-property-decorator";
import RouteName from "../../router/name";
import PopoverActorCard from "../Account/PopoverActorCard.vue";
import ActivityMixin from "../../mixins/activity";
@Component({
components: {
PopoverActorCard,
},
})
export default class EventActivityItem extends mixins(ActivityMixin) {
ActivityEventSubject = ActivityEventSubject;
usernameWithDomain = usernameWithDomain;
RouteName = RouteName;
get translation(): string | undefined {
switch (this.activity.subject) {
case ActivityEventSubject.EVENT_CREATED:
if (this.isAuthorCurrentActor) {
return "You created the event {event}.";
}
return "The event {event} was created by {profile}.";
case ActivityEventSubject.EVENT_UPDATED:
if (this.isAuthorCurrentActor) {
return "You updated the event {event}.";
}
return "The event {event} was updated by {profile}.";
case ActivityEventSubject.EVENT_DELETED:
if (this.isAuthorCurrentActor) {
return "You deleted the event {event}.";
}
return "The event {event} was deleted by {profile}.";
default:
return undefined;
}
}
get iconColor(): string | undefined {
switch (this.activity.subject) {
case ActivityEventSubject.EVENT_CREATED:
return "is-success";
case ActivityEventSubject.EVENT_UPDATED:
return "is-grey";
case ActivityEventSubject.EVENT_DELETED:
return "is-danger";
default:
return undefined;
}
}
}
</script>
<style lang="scss" scoped>
@import "./activity.scss";
</style>

@ -0,0 +1,183 @@
<template>
<div class="activity-item">
<b-icon :icon="'cog'" :type="iconColor" />
<div class="subject">
<i18n :path="translation" tag="p">
<router-link
v-if="activity.object"
slot="group"
:to="{
name: RouteName.GROUP,
params: { preferredUsername: usernameWithDomain(activity.object) },
}"
>{{ subjectParams.group_name }}</router-link
>
<b v-else slot="post">{{ subjectParams.group_name }}</b>
<popover-actor-card
:actor="activity.author"
:inline="true"
slot="profile"
>
<b>
{{
$t("@{username}", {
username: usernameWithDomain(activity.author),
})
}}</b
></popover-actor-card
></i18n
>
<i18n
:path="detail"
v-for="detail in details"
:key="detail"
tag="p"
class="has-text-grey"
>
<popover-actor-card
:actor="activity.author"
:inline="true"
slot="profile"
>
<b>
{{
$t("@{username}", {
username: usernameWithDomain(activity.author),
})
}}</b
></popover-actor-card
>
<router-link
v-if="activity.object"
slot="group"
:to="{
name: RouteName.GROUP,
params: { preferredUsername: usernameWithDomain(activity.object) },
}"
>{{ subjectParams.group_name }}</router-link
>
<b v-else slot="post">{{ subjectParams.group_name }}</b>
<b v-if="subjectParams.old_group_name" slot="old_group_name">{{
subjectParams.old_group_name
}}</b>
</i18n>
<small class="has-text-grey activity-date">{{
activity.insertedAt | formatTimeString
}}</small>
</div>
</div>
</template>
<script lang="ts">
import { usernameWithDomain } from "@/types/actor";
import { ActivityGroupSubject, GroupVisibility, Openness } from "@/types/enums";
import { Component } from "vue-property-decorator";
import RouteName from "../../router/name";
import PopoverActorCard from "../Account/PopoverActorCard.vue";
import ActivityMixin from "../../mixins/activity";
import { mixins } from "vue-class-component";
@Component({
components: {
PopoverActorCard,
},
})
export default class GroupActivityItem extends mixins(ActivityMixin) {
usernameWithDomain = usernameWithDomain;
RouteName = RouteName;
ActivityGroupSubject = ActivityGroupSubject;
get translation(): string | undefined {
switch (this.activity.subject) {
case ActivityGroupSubject.GROUP_CREATED:
if (this.isAuthorCurrentActor) {
return "You created the group {group}.";
}
return "{profile} created the group {group}.";
case ActivityGroupSubject.GROUP_UPDATED:
if (this.isAuthorCurrentActor) {
return "You updated the group {group}.";
}
return "{profile} updated the group {group}.";
default:
return undefined;
}
}
get iconColor(): string | undefined {
switch (this.activity.subject) {
case ActivityGroupSubject.GROUP_CREATED:
return "is-success";
case ActivityGroupSubject.GROUP_UPDATED:
return "is-grey";
default:
return undefined;
}
}
get details(): string[] {
let details = [];
const changes = this.subjectParams.group_changes.split(",");
if (changes.includes("name") && this.subjectParams.old_group_name) {
details.push("{old_group_name} was renamed to {group}.");
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (changes.includes("visibility") && this.activity.object.visibility) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
switch (this.activity.object.visibility) {
case GroupVisibility.PRIVATE:
details.push("Visibility was set to private.");
break;
case GroupVisibility.PUBLIC:
details.push("Visibility was set to public.");
break;
default:
details.push("Visibility was set to an unknown value.");
break;
}
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (changes.includes("openness") && this.activity.object.openness) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
switch (this.activity.object.openness) {
case Openness.INVITE_ONLY:
details.push("The group can now only be joined with an invite.");
break;
case Openness.OPEN:
details.push("The group can now be joined by anyone.");
break;
default:
details.push("Unknown value for the openness setting.");
break;
}
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (changes.includes("address") && this.activity.object.physicalAddress) {
details.push("The group's physical address was changed.");
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (changes.includes("avatar") && this.activity.object.avatar) {
details.push("The group's avatar was changed.");
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (changes.includes("banner") && this.activity.object.banner) {
details.push("The group's banner was changed.");
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (changes.includes("summary") && this.activity.object.summary) {
details.push("The group's short description was changed.");
}
return details;
}
}
</script>
<style lang="scss" scoped>
@import "./activity.scss";
</style>

@ -0,0 +1,233 @@
<template>
<div class="activity-item">
<b-icon :icon="icon" :type="iconColor" />
<div class="subject">
<i18n :path="translation" tag="p">
<popover-actor-card
v-if="activity.object"
:actor="activity.object.actor"
:inline="true"
slot="member"
>
<b>
{{
$t("@{username}", {
username: usernameWithDomain(activity.object.actor),
})
}}</b
></popover-actor-card
>
<b slot="member" v-else>{{
subjectParams.member_preferred_username
}}</b>
<popover-actor-card
:actor="activity.author"
:inline="true"
slot="profile"
>
<b>
{{
$t("@{username}", {
username: usernameWithDomain(activity.author),
})
}}</b
></popover-actor-card
></i18n
>
<small class="has-text-grey activity-date">{{
activity.insertedAt | formatTimeString
}}</small>
</div>
</div>
</template>
<script lang="ts">
import { usernameWithDomain } from "@/types/actor";
import { ActivityMemberSubject, MemberRole } from "@/types/enums";
import { Component } from "vue-property-decorator";
import RouteName from "../../router/name";
import PopoverActorCard from "../Account/PopoverActorCard.vue";
import ActivityMixin from "../../mixins/activity";
import { mixins } from "vue-class-component";
export const MEMBER_ROLE_VALUE: Record<string, number> = {
[MemberRole.MEMBER]: 20,
[MemberRole.MODERATOR]: 50,
[MemberRole.ADMINISTRATOR]: 90,
[MemberRole.CREATOR]: 100,
};
@Component({
components: {
PopoverActorCard,
},
})
export default class MemberActivityItem extends mixins(ActivityMixin) {
usernameWithDomain = usernameWithDomain;
RouteName = RouteName;
ActivityMemberSubject = ActivityMemberSubject;
get translation(): string | undefined {
switch (this.activity.subject) {
case ActivityMemberSubject.MEMBER_REQUEST:
if (this.isAuthorCurrentActor) {
return "You requested to join the group.";
}
return "{member} requested to join the group.";
case ActivityMemberSubject.MEMBER_INVITED:
if (this.isAuthorCurrentActor) {
return "You invited {member}.";
}
return "{member} was invited by {profile}.";
case ActivityMemberSubject.MEMBER_ADDED:
if (this.isAuthorCurrentActor) {
return "You added the member {member}.";
}
return "{profile} added the member {member}.";
case ActivityMemberSubject.MEMBER_UPDATED:
if (this.subjectParams.member_role && this.subjectParams.old_role) {
return this.roleUpdate;
}
if (this.isAuthorCurrentActor) {
return "You updated the member {member}.";
}
return "{profile} updated the member {member}.";
case ActivityMemberSubject.MEMBER_REMOVED:
if (this.isAuthorCurrentActor) {
return "You excluded member {member}.";
}
return "{profile} excluded member {member}.";
case ActivityMemberSubject.MEMBER_QUIT:
return "{profile} quit the group.";
case ActivityMemberSubject.MEMBER_REJECTED_INVITATION:
return "{member} rejected the invitation to join the group.";
case ActivityMemberSubject.MEMBER_ACCEPTED_INVITATION:
if (this.isAuthorCurrentActor) {
return "You accepted the invitation to join the group.";
}
return "{member} accepted the invitation to join the group.";
default:
return undefined;
}
}
get icon(): string {
switch (this.activity.subject) {
case ActivityMemberSubject.MEMBER_REQUEST:
case ActivityMemberSubject.MEMBER_ADDED:
case ActivityMemberSubject.MEMBER_INVITED:
case ActivityMemberSubject.MEMBER_ACCEPTED_INVITATION:
return "account-multiple-plus";
case ActivityMemberSubject.MEMBER_REMOVED:
case ActivityMemberSubject.MEMBER_REJECTED_INVITATION:
case ActivityMemberSubject.MEMBER_QUIT:
return "account-multiple-minus";
case ActivityMemberSubject.MEMBER_UPDATED:
default:
return "account-multiple";
}
}
get iconColor(): string | undefined {
switch (this.activity.subject) {
case ActivityMemberSubject.MEMBER_ADDED:
case ActivityMemberSubject.MEMBER_INVITED:
case ActivityMemberSubject.MEMBER_JOINED:
case ActivityMemberSubject.MEMBER_APPROVED:
case ActivityMemberSubject.MEMBER_ACCEPTED_INVITATION:
return "is-success";
case ActivityMemberSubject.MEMBER_REQUEST:
case ActivityMemberSubject.MEMBER_UPDATED:
return "is-grey";
case ActivityMemberSubject.MEMBER_REMOVED:
case ActivityMemberSubject.MEMBER_REJECTED_INVITATION:
case ActivityMemberSubject.MEMBER_QUIT:
return "is-danger";
default:
return undefined;
}
}
get roleUpdate(): string | undefined {
if (
Object.keys(MEMBER_ROLE_VALUE).includes(this.subjectParams.member_role) &&
Object.keys(MEMBER_ROLE_VALUE).includes(this.subjectParams.old_role)
) {
if (
MEMBER_ROLE_VALUE[this.subjectParams.member_role] >
MEMBER_ROLE_VALUE[this.subjectParams.old_role]
) {
switch (this.subjectParams.member_role) {
case MemberRole.MODERATOR:
if (this.isAuthorCurrentActor) {
return "You promoted {member} to moderator.";
}
if (this.isObjectMemberCurrentActor) {
return "You were promoted to moderator by {profile}.";
}
return "{profile} promoted {member} to moderator.";
case MemberRole.ADMINISTRATOR:
if (this.isAuthorCurrentActor) {
return "You promoted {member} to administrator.";
}
if (this.isObjectMemberCurrentActor) {
return "You were promoted to administrator by {profile}.";
}
return "{profile} promoted {member} to administrator.";
default:
if (this.isAuthorCurrentActor) {
return "You promoted the member {member} to an unknown role.";
}
if (this.isObjectMemberCurrentActor) {
return "You were promoted to an unknown role by {profile}.";
}
return "{profile} promoted {member} to an unknown role.";
}
} else {
switch (this.subjectParams.member_role) {
case MemberRole.MODERATOR:
if (this.isAuthorCurrentActor) {
return "You demoted {member} to moderator.";
}
if (this.isObjectMemberCurrentActor) {
return "You were demoted to moderator by {profile}.";
}
return "{profile} demoted {member} to moderator.";
case MemberRole.MEMBER:
if (this.isAuthorCurrentActor) {
return "You demoted {member} to simple member.";
}
if (this.isObjectMemberCurrentActor) {
return "You were demoted to simple member by {profile}.";
}
return "{profile} demoted {member} to simple member.";
default:
if (this.isAuthorCurrentActor) {
return "You demoted the member {member} to an unknown role.";
}
if (this.isObjectMemberCurrentActor) {
return "You were demoted to an unknown role by {profile}.";
}
return "{profile} demoted {member} to an unknown role.";
}
}
} else {
if (this.isAuthorCurrentActor) {
return "You updated the member {member}.";
}
return "{profile} updated the member {member}";
}
}
get isObjectMemberCurrentActor(): boolean {
return (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.activity?.object?.actor?.id === this.currentActor?.id &&
this.currentActor?.id !== undefined
);
}
}
</script>
<style lang="scss" scoped>
@import "./activity.scss";
</style>

@ -0,0 +1,93 @@
<template>
<div class="activity-item">
<b-icon :icon="'bullhorn'" :type="iconColor" />
<div class="subject">
<i18n :path="translation" tag="p">
<router-link
v-if="activity.object"
slot="post"
:to="{
name: RouteName.POST,
params: { slug: subjectParams.post_slug },
}"
>{{ subjectParams.post_title }}</router-link
>
<b v-else slot="post">{{ subjectParams.post_title }}</b>
<popover-actor-card
:actor="activity.author"
:inline="true"
slot="profile"
>
<b>
{{
$t("@{username}", {
username: usernameWithDomain(activity.author),
})
}}</b
></popover-actor-card
></i18n
>
<small class="has-text-grey activity-date">{{
activity.insertedAt | formatTimeString
}}</small>
</div>
</div>
</template>
<script lang="ts">
import { usernameWithDomain } from "@/types/actor";
import { ActivityPostSubject } from "@/types/enums";
import { Component } from "vue-property-decorator";
import RouteName from "../../router/name";
import PopoverActorCard from "../Account/PopoverActorCard.vue";
import ActivityMixin from "../../mixins/activity";
import { mixins } from "vue-class-component";
@Component({
components: {
PopoverActorCard,
},
})
export default class PostActivityItem extends mixins(ActivityMixin) {
usernameWithDomain = usernameWithDomain;
RouteName = RouteName;
ActivityPostSubject = ActivityPostSubject;
get translation(): string | undefined {
switch (this.activity.subject) {
case ActivityPostSubject.POST_CREATED:
if (this.isAuthorCurrentActor) {
return "You created the post {post}.";
}
return "The post {post} was created by {profile}.";
case ActivityPostSubject.POST_UPDATED:
if (this.isAuthorCurrentActor) {
return "You updated the post {post}.";
}
return "The post {post} was updated by {profile}.";
case ActivityPostSubject.POST_DELETED:
if (this.isAuthorCurrentActor) {
return "You deleted the post {post}.";
}
return "The post {post} was deleted by {profile}.";
default:
return undefined;
}
}
get iconColor(): string | undefined {
switch (this.activity.subject) {
case ActivityPostSubject.POST_CREATED:
return "is-success";
case ActivityPostSubject.POST_UPDATED:
return "is-grey";
case ActivityPostSubject.POST_DELETED:
return "is-danger";
default:
return undefined;
}
}
}
</script>
<style lang="scss" scoped>
@import "./activity.scss";
</style>

@ -0,0 +1,189 @@
<template>
<div class="activity-item">
<b-icon :icon="'link'" :type="iconColor" />
<div class="subject">
<i18n :path="translation" tag="p">
<router-link v-if="activity.object" slot="resource" :to="path">{{
subjectParams.resource_title
}}</router-link>
<b v-else slot="resource">{{ subjectParams.resource_title }}</b>
<router-link v-if="activity.object" slot="new_path" :to="path">{{
parentDirectory
}}</router-link>
<b v-else slot="new_path">{{ parentDirectory }}</b>
<router-link
v-if="activity.object && subjectParams.old_resource_title"
slot="old_resource_title"
:to="path"
>{{ subjectParams.old_resource_title }}</router-link
>
<b
v-else-if="subjectParams.old_resource_title"
slot="old_resource_title"
>{{ subjectParams.old_resource_title }}</b
>
<popover-actor-card
:actor="activity.author"
:inline="true"
slot="profile"
>
<b>
{{
$t("@{username}", {
username: usernameWithDomain(activity.author),
})
}}</b
></popover-actor-card
></i18n
>
<small class="has-text-grey activity-date">{{
activity.insertedAt | formatTimeString
}}</small>
</div>
</div>
</template>
<script lang="ts">
import { usernameWithDomain } from "@/types/actor";
import { ActivityResourceSubject } from "@/types/enums";
import { Component } from "vue-property-decorator";
import RouteName from "../../router/name";
import PopoverActorCard from "../Account/PopoverActorCard.vue";
import ActivityMixin from "../../mixins/activity";
import { mixins } from "vue-class-component";
import { Location } from "vue-router";
@Component({
components: {
PopoverActorCard,
},
})
export default class ResourceActivityItem extends mixins(ActivityMixin) {
usernameWithDomain = usernameWithDomain;
RouteName = RouteName;
get translation(): string | undefined {
switch (this.activity.subject) {
case ActivityResourceSubject.RESOURCE_CREATED:
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (this.activity?.object?.type === "folder") {
if (this.isAuthorCurrentActor) {
return "You created the folder {resource}.";
}
return "{profile} created the folder {resource}.";
}
if (this.isAuthorCurrentActor) {
return "You created the resource {resource}.";
}
return "{profile} created the resource {resource}.";
case ActivityResourceSubject.RESOURCE_MOVED:
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (this.activity?.object?.type === "folder") {
if (this.parentDirectory === null) {
if (this.isAuthorCurrentActor) {
return "You moved the folder {resource} to the root folder.";
}
return "{profile} moved the folder {resource} to the root folder.";
}
if (this.isAuthorCurrentActor) {
return "You moved the folder {resource} into {new_path}.";
}
return "{profile} moved the folder {resource} into {new_path}.";
}
if (this.parentDirectory === null) {
if (this.isAuthorCurrentActor) {
return "You moved the resource {resource} to the root folder.";
}
return "{profile} moved the resource {resource} to the root folder.";
}
if (this.isAuthorCurrentActor) {
return "You moved the resource {resource} into {new_path}.";
}
return "{profile} moved the resource {resource} into {new_path}.";
case ActivityResourceSubject.RESOURCE_UPDATED:
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (this.activity?.object?.type === "folder") {
if (this.isAuthorCurrentActor) {
return "You renamed the folder from {old_resource_title} to {resource}.";
}
return "{profile} renamed the folder from {old_resource_title} to {resource}.";
}
if (this.isAuthorCurrentActor) {
return "You renamed the resource from {old_resource_title} to {resource}.";
}
return "{profile} renamed the resource from {old_resource_title} to {resource}.";
case ActivityResourceSubject.RESOURCE_DELETED:
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (this.activity?.object?.type === "folder") {
if (this.isAuthorCurrentActor) {
return "You deleted the folder {resource}.";
}
return "{profile} deleted the folder {resource}.";
}
if (this.isAuthorCurrentActor) {
return "You deleted the resource {resource}.";
}
return "{profile} deleted the resource {resource}.";
default:
return undefined;
}
}
get iconColor(): string | undefined {
switch (this.activity.subject) {
case ActivityResourceSubject.RESOURCE_CREATED:
return "is-success";
case ActivityResourceSubject.RESOURCE_MOVED:
case ActivityResourceSubject.RESOURCE_UPDATED:
return "is-grey";
case ActivityResourceSubject.RESOURCE_DELETED:
return "is-danger";
default:
return undefined;
}
}
get path(): Location {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
let path = this.parentPath(this.activity?.object?.path);
if (path === "") {
return {
name: RouteName.RESOURCE_FOLDER_ROOT,
params: {
preferredUsername: usernameWithDomain(this.activity.group),
},
};
}
return {
name: RouteName.RESOURCE_FOLDER,
params: {
path,
preferredUsername: usernameWithDomain(this.activity.group),
},
};
}
get parentDirectory(): string | undefined | null {
if (this.subjectParams.resource_path) {
const parentPath = this.parentPath(this.subjectParams.resource_path);
const directory = parentPath.split("/");
return directory.pop();
}
return null;
}
parentPath(parent: string): string {
let path = parent.split("/");
path.pop();
return path.join("/").replace(/^\//, "");
}
}
</script>
<style lang="scss" scoped>
@import "./activity.scss";
</style>

@ -0,0 +1,30 @@
<template>
<div class="activity-item">
<span>
<b-skeleton circle width="32px" height="32px"></b-skeleton>
</span>
<div class="subject">
<div class="content">
<p>
<b-skeleton active></b-skeleton>
<b-skeleton active class="datetime"></b-skeleton>
</p>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
@import "./activity.scss";
div.activity-item {
flex: 1;
.subject {
flex: 1;
max-width: 600px;
.content p > div:last-child {
max-width: 50px;
}
}
}
</style>

@ -0,0 +1,21 @@
.activity-item {
display: flex;
span.icon {
width: 2em;
height: 2em;
box-sizing: border-box;
border-radius: 50%;
background: #fff;
border: 2px solid;
z-index: 2;
flex-shrink: 0;
&.has-text-grey {
border-color: $grey;
}
}
.subject {
padding: 0.25rem 0 0 0.5rem;
}
}

@ -58,8 +58,14 @@ export default class ResourceItem extends Vue {
mapServiceTypeToIcon = mapServiceTypeToIcon;
get urlHostname(): string {
return new URL(this.resource.resourceUrl).hostname.replace(/^(www\.)/, "");
get urlHostname(): string | undefined {
if (this.resource?.resourceUrl) {
return new URL(this.resource.resourceUrl).hostname.replace(
/^(www\.)/,
""
);
}
return undefined;
}
}
</script>

@ -26,33 +26,35 @@
</span>
{{ $t("Parent folder") }}
</a>
<a
class="panel-block"
v-for="element in resource.children.elements"
:class="{
clickable:
element.type === 'folder' && element.id !== initialResource.id,
}"
:key="element.id"
@click="goDown(element)"
>
<span class="panel-icon">
<b-icon
icon="folder"
size="is-small"
v-if="element.type === 'folder'"
/>
<b-icon icon="link" size="is-small" v-else />
</span>
{{ element.title }}
<span v-if="element.id === initialResource.id">
<em v-if="element.type === 'folder'"> {{ $t("(this folder)") }}</em>
<em v-else> {{ $t("(this link)") }}</em>
</span>
</a>
<template v-if="resource.children">
<a
class="panel-block"
v-for="element in resource.children.elements"
:class="{
clickable:
element.type === 'folder' && element.id !== initialResource.id,
}"
:key="element.id"
@click="goDown(element)"
>
<span class="panel-icon">
<b-icon
icon="folder"
size="is-small"
v-if="element.type === 'folder'"
/>
<b-icon icon="link" size="is-small" v-else />
</span>
{{ element.title }}
<span v-if="element.id === initialResource.id">
<em v-if="element.type === 'folder'"> {{ $t("(this folder)") }}</em>
<em v-else> {{ $t("(this link)") }}</em>
</span>
</a>
</template>
<p
class="panel-block content has-text-grey has-text-centered"
v-if="resource.children.total === 0"
v-if="resource.children && resource.children.total === 0"
>
{{ $t("No resources in this folder") }}
</p>

@ -0,0 +1,28 @@
<template>
<div class="observer" />
</template>
<script lang="ts">
import "intersection-observer";
import { Component, Prop, Vue } from "vue-property-decorator";
@Component
export default class Observer extends Vue {
@Prop({ required: false, default: () => ({}) }) options!: Record<string, any>;
observer!: IntersectionObserver;
mounted(): void {
this.observer = new IntersectionObserver(([entry]) => {
if (entry && entry.isIntersecting) {
this.$emit("intersect");
}
}, this.options);
this.observer.observe(this.$el);
}
destroyed(): void {
this.observer.disconnect();
}
}
</script>

@ -321,3 +321,98 @@ export const REFRESH_PROFILE = gql`
}
}
`;
export const GROUP_TIMELINE = gql`
query GroupTimeline(
$preferredUsername: String!
$type: ActivityType
$page: Int
$limit: Int
) {
group(preferredUsername: $preferredUsername) {
id
preferredUsername
domain
name
activity(type: $type, page: $page, limit: $limit) {
total
elements {
id
insertedAt
subject
subjectParams {
key
value
}
type
author {
id
preferredUsername
name
domain
avatar {
id
url
}
}
group {
id
preferredUsername
}
object {
... on Event {
id
title
}
... on Post {
id
title
}
... on Member {
id
actor {
id
name
preferredUsername
domain
avatar {
id
url
}
}
}
... on Resource {
id
title
path
type
}
... on Discussion {
id
title
slug
}
... on Group {
id
preferredUsername
domain
name
summary
visibility
openness
physicalAddress {
id
}
banner {
id
}
avatar {
id
}
}
}
}
}
}
}
`;

@ -859,5 +859,101 @@
"Last published events": "Last published events",
"On {instance}": "On {instance}",
"Close events": "Close events",
"Within {number} kilometers of {place}": "|Within one kilometer of {place}|Within {number} kilometers of {place}"
"Within {number} kilometers of {place}": "|Within one kilometer of {place}|Within {number} kilometers of {place}",
"@{username}": "@{username}",
"Yesterday": "Yesterday",
"You created the event {event}.": "You created the event {event}.",
"The event {event} was created by {profile}.": "The event {event} was created by {profile}.",
"You updated the event {event}.": "You updated the event {event}.",
"The event {event} was updated by {profile}.": "The event {event} was updated by {profile}.",
"You deleted the event {event}.": "You deleted the event {event}.",
"The event {event} was deleted by {profile}.": "The event {event} was deleted by {profile}.",
"You created the post {post}.": "You created the post {post}.",
"The post {post} was created by {profile}.": "The post {post} was created by {profile}.",
"You updated the post {post}.": "You updated the post {post}.",
"The post {post} was updated by {profile}.": "The post {post} was updated by {profile}.",
"You deleted the post {post}.": "You deleted the post {post}.",
"The post {post} was deleted by {profile}.": "The post {post} was deleted by {profile}.",
"No more activity to display.": "No more activity to display.",
"There is no activity yet. Start doing some things to see activity appear here.": "There is no activity yet. Start doing some things to see activity appear here.",
"{group} activity timeline": "{group} activity timeline",
"You promoted {member} to moderator.": "You promoted {member} to moderator.",
"You promoted {member} to administrator.": "You promoted {member} to administrator.",
"You promoted the member {member} to an unknown role.": "You promoted the member {member} to an unknown role.",
"You demoted {member} to moderator.": "You demoted {member} to moderator.",
"You demoted {member} to simple member.": "You demoted {member} to simple member.",
"You demoted the member {member} to an unknown role.": "You demoted the member {member} to an unknown role.",
"You updated the member {member}.": "You updated the member {member}.",
"{profile} updated the member {member}.": "{profile} updated the member {member}.",
"{profile} promoted {member} to moderator.": "{profile} promoted {member} to moderator.",
"{profile} promoted {member} to administrator.": "{profile} promoted {member} to administrator.",
"{profile} promoted {member} to an unknown role.": "{profile} promoted {member} to an unknown role.",
"{profile} demoted {member} to moderator.": "{profile} demoted {member} to moderator.",
"{profile} demoted {member} to simple member.": "{profile} demoted {member} to simple member.",
"{profile} demoted {member} to an unknown role.": "{profile} demoted {member} to an unknown role.",
"You requested to join the group.": "You requested to join the group.",
"{member} requested to join the group.": "{member} requested to join the group.",
"You invited {member}.": "You invited {member}.",
"{member} was invited by {profile}.": "{member} was invited by {profile}.",
"You added the member {member}.": "You added the member {member}.",
"{profile} added the member {member}.": "{profile} added the member {member}.",
"{member} rejected the invitation to join the group.": "{member} rejected the invitation to join the group.",
"{member} accepted the invitation to join the group.": "{member} accepted the invitation to join the group.",
"You excluded member {member}.": "You excluded member {member}.",
"{profile} excluded member {member}.": "{profile} excluded member {member}.",
"You accepted the invitation to join the group.": "You accepted the invitation to join the group.",
"You were promoted to moderator by {profile}.": "You were promoted to moderator by {profile}.",
"You were promoted to administrator by {profile}.": "You were promoted to administrator by {profile}.",
"You were promoted to an unknown role by {profile}.": "You were promoted to an unknown role by {profile}.",
"You were demoted to moderator by {profile}.": "You were demoted to moderator by {profile}.",
"You were demoted to simple member by {profile}.": "You were demoted to simple member by {profile}.",
"You were demoted to an unknown role by {profile}.": "You were demoted to an unknown role by {profile}.",
"{profile} quit the group.": "{profile} quit the group.",
"You created the folder {resource}.": "You created the folder {resource}.",
"{profile} created the folder {resource}.": "{profile} created the folder {resource}.",
"You created the resource {resource}.": "You created the resource {resource}.",
"{profile} created the resource {resource}.": "{profile} created the resource {resource}.",
"You moved the folder {resource} to the root folder.": "You moved the folder {resource} to the root folder.",
"{profile} moved the folder {resource} to the root folder.": "{profile} moved the folder {resource} to the root folder.",
"You moved the folder {resource} into {new_path}.": "You moved the folder {resource} into {new_path}.",
"{profile} moved the folder {resource} into {new_path}.": "{profile} moved the folder {resource} into {new_path}.",
"You moved the resource {resource} to the root folder.": "You moved the resource {resource} to the root folder.",
"{profile} moved the resource {resource} to the root folder.": "{profile} moved the resource {resource} to the root folder.",
"You moved the resource {resource} into {new_path}.": "You moved the resource {resource} into {new_path}.",
"{profile} moved the resource {resource} into {new_path}.": "{profile} moved the resource {resource} into {new_path}.",
"You renamed the folder from {old_resource_title} to {resource}.": "You renamed the folder from {old_resource_title} to {resource}.",
"{profile} renamed the folder from {old_resource_title} to {resource}.": "{profile} renamed the folder from {old_resource_title} to {resource}.",
"You renamed the resource from {old_resource_title} to {resource}.": "You renamed the resource from {old_resource_title} to {resource}.",
"{profile} renamed the resource from {old_resource_title} to {resource}.": "{profile} renamed the resource from {old_resource_title} to {resource}.",
"You deleted the folder {resource}.": "You deleted the folder {resource}.",
"{profile} deleted the folder {resource}.": "{profile} deleted the folder {resource}.",
"You deleted the resource {resource}.": "You deleted the resource {resource}.",
"{profile} deleted the resource {resource}.": "{profile} deleted the resource {resource}.",
"Activity": "Activity",
"Load more activities": "Load more activities",
"You created the discussion {discussion}.": "You created the discussion {discussion}.",
"{profile} created the discussion {discussion}.": "{profile} created the discussion {discussion}.",
"You replied to the discussion {discussion}.": "You replied to the discussion {discussion}.",
"{profile} replied to the discussion {discussion}.": "{profile} replied to the discussion {discussion}.",
"You renamed the discussion from {old_discussion} to {discussion}.": "You renamed the discussion from {old_discussion} to {discussion}.",
"{profile} renamed the discussion from {old_discussion} to {discussion}.": "{profile} renamed the discussion from {old_discussion} to {discussion}.",
"You archived the discussion {discussion}.": "You archived the discussion {discussion}.",
"{profile} archived the discussion {discussion}.": "{profile} archived the discussion {discussion}.",
"You deleted the discussion {discussion}.": "You deleted the discussion {discussion}.",
"{profile} deleted the discussion {discussion}.": "{profile} deleted the discussion {discussion}.",
"You created the group {group}.": "You created the group {group}.",
"{profile} created the group {group}.": "{profile} created the group {group}.",
"You updated the group {group}.": "You updated the group {group}.",
"{profile} updated the group {group}.": "{profile} updated the group {group}.",
"{old_group_name} was renamed to {group}.": "{old_group_name} was renamed to {group}.",
"Visibility was set to private.": "Visibility was set to private.",
"Visibility was set to public.": "Visibility was set to public.",
"Visibility was set to an unknown value.": "Visibility was set to an unknown value.",
"The group can now only be joined with an invite.": "The group can now only be joined with an invite.",
"The group can now be joined by anyone.": "The group can now be joined by anyone.",
"Unknown value for the openness setting.": "Unknown value for the openness setting.",
"The group's physical address was changed.": "The group's physical address was changed.",
"The group's avatar was changed.": "The group's avatar was changed.",
"The group's banner was changed.": "The group's banner was changed.",
"The group's short description was changed.": "The group's short description was changed."
}

@ -954,5 +954,100 @@
"Last published events": "Derniers événements publiés",
"On {instance}": "Sur {instance}",
"Close events": "Événements proches",
"Within {number} kilometers of {place}": "|Dans un rayon d'un kilomètre de {place}|Dans un rayon de {number} kilomètres de {place}"
"Within {number} kilometers of {place}": "|Dans un rayon d'un kilomètre de {place}|Dans un rayon de {number} kilomètres de {place}",
"Yesterday": "Hier",
"You created the event {event}.": "Vous avez créé l'événement {event}.",
"The event {event} was created by {profile}.": "L'événement {event} a été créé par {profile}.",
"You updated the event {event}.": "Vous avez mis à jour l'événement {event}.",
"The event {event} was updated by {profile}.": "L'événement {event} à été mis à jour par {profile}.",
"You deleted the event {event}.": "Vous avez supprimé l'événement {event}.",
"The event {event} was deleted by {profile}.": "L'événement {event} a été supprimé par {profile}.",
"You created the post {post}.": "Vous avez créé le billet {post}.",
"The post {post} was created by {profile}.": "Le billet {post} a été créé par {profile}.",
"You updated the post {post}.": "Vous avez mis à jour le billet {post}.",
"The post {post} was updated by {profile}.": "Le billet {post} a été mis à jour par {profile}.",
"You deleted the post {post}.": "Vous avez supprimé le billet {post}.",
"The post {post} was deleted by {profile}.": "Le billet {post} a été supprimé par {profile}.",
"No more activity to display.": "Il n'y a plus d'activités à afficher.",
"There is no activity yet. Start doing some things to see activity appear here.": "Il n'y a pas encore d'activité. Commencez par effectuer des actions pour voir des éléments s'afficher ici.",
"{group} activity timeline": "Timeline de l'activité de {group}",
"You promoted {member} to moderator.": "Vous avez promu {member} en tant que modérateur⋅ice.",
"You promoted {member} to administrator.": "Vous avez promu {member} en tant qu'adminstrateur⋅ice.",
"You promoted the member {member} to an unknown role.": "Vous avez promu le ou la membre {member} à un role inconnu.",
"You demoted {member} to moderator.": "Vous avez rétrogradé {member} en tant que modérateur⋅ice.",
"You demoted {member} to simple member.": "Vous avez rétrogradé {member} en tant que simple membre.",
"You demoted the member {member} to an unknown role.": "Vous avez rétrogradé le membre {member} à un role inconnu.",
"You updated the member {member}.": "Vous avez mis à jour le ou la membre {member}.",
"{profile} updated the member {member}.": "{profile} a mis à jour le ou la membre {member}.",
"{profile} promoted {member} to moderator.": "{profile} a promu {member} en tant que modérateur⋅ice.",
"{profile} promoted {member} to administrator.": "{profile} a promu {member} en tant qu'administrateur⋅ice.",
"{profile} promoted {member} to an unknown role.": "{profile} a promu {member} à un role inconnu.",
"{profile} demoted {member} to moderator.": "{profile} a rétrogradé {member} en tant que modérateur⋅ice.",
"{profile} demoted {member} to simple member.": "{profile} a rétrogradé {member} en tant que simple membre.",
"{profile} demoted {member} to an unknown role.": "{profile} a rétrogradé {member} à un role inconnu.",
"You requested to join the group.": "Vous avez demandé à rejoindre le groupe.",
"{member} requested to join the group.": "{member} a demandé à rejoindre le groupe.",
"You invited {member}.": "Vous avez invité {member}.",
"{member} was invited by {profile}.": "{member} a été invité⋅e par {profile}.",
"You added the member {member}.": "Vous avez ajouté le ou la membre {member}.",
"{profile} added the member {member}.": "{profile} a ajouté le ou la membre {member}.",
"{member} rejected the invitation to join the group.": "{member} a refusé l'invitation à se joindre au groupe.",
"{member} accepted the invitation to join the group.": "{member} a accepté l'invitation à se joindre au groupe.",
"You excluded member {member}.": "Vous avez exclu le ou la membre {member}.",
"{profile} excluded member {member}.": "{profile} a exclu le ou la membre {member}.",
"You accepted the invitation to join the group.": "Vous avez accepté l'invitation à vous joindre au groupe.",
"You were promoted to moderator by {profile}.": "Vous avez été promu⋅e modérateur⋅ice par {profile}.",
"You were promoted to administrator by {profile}.": "Vous avez été promu⋅e administrateur⋅ice par {profile}.",
"You were promoted to an unknown role by {profile}.": "Vous avez été promu⋅e à un role inconnu par {profile}.",
"You were demoted to moderator by {profile}.": "Vous avez été rétrogradé⋅e modérateur⋅ice par {profile}.",
"You were demoted to simple member by {profile}.": "Vous avez été rétrogradé⋅e simple membre par {profile}.",
"You were demoted to an unknown role by {profile}.": "Vous avez été rétrogradé⋅e à un role inconnu par {profile}.",
"{profile} quit the group.": "{profile} a quitté le groupe.",
"You created the folder {resource}.": "Vous avez créé le dossier {resource}.",
"{profile} created the folder {resource}.": "{profile} a créé le dossier {resource}.",
"You created the resource {resource}.": "Vous avez créé la ressource {resource}.",
"{profile} created the resource {resource}.": "{profile} a créé la ressource {resource}.",
"You moved the folder {resource} to the root folder.": "Vous avez déplacé le dossier {resource} dans le dossier racine.",
"{profile} moved the folder {resource} to the root folder.": "{profile} a déplacé le dossier {resource} dans le dossier racine.",
"You moved the folder {resource} into {new_path}.": "Vous avez déplacé le dossier {resource} dans {new_path}.",
"{profile} moved the folder {resource} into {new_path}.": "{profile} a déplacé le dossier {resource} dans {new_path}.",
"You moved the resource {resource} to the root folder.": "Vous avez déplacé la ressource {resource} dans le dossier racine.",
"{profile} moved the resource {resource} to the root folder.": "{profile} a déplacé la ressource {resource} dans le dossier racine.",
"You moved the resource {resource} into {new_path}.": "Vous avez déplacé la ressource {resource} dans {new_path}.",
"{profile} moved the resource {resource} into {new_path}.": "{profile} a déplacé la ressource {resource} dans {new_path}.",
"You renamed the folder from {old_resource_title} to {resource}.": "Vous avez renommé le dossier {old_resource_title} en {resource}.",
"{profile} renamed the folder from {old_resource_title} to {resource}.": "{profile} a renommé le dossier {old_resource_title} en {resource}.",
"You renamed the resource from {old_resource_title} to {resource}.": "Vous avez renommé la ressource {old_resource_title} en {resource}.",
"{profile} renamed the resource from {old_resource_title} to {resource}.": "{profile} a renommé la ressource {old_resource_title} en {resource}.",
"You deleted the folder {resource}.": "Vous avez supprimé le dossier {resource}.",
"{profile} deleted the folder {resource}.": "{profile} a supprimé le dossier {resource}.",
"You deleted the resource {resource}.": "Vous avez supprimé la ressource {resource}.",
"{profile} deleted the resource {resource}.": "{profile} a supprimé la ressource {resource}.",
"Activity": "Activité",
"Load more activities": "Charger plus d'activités",
"You created the discussion {discussion}.": "Vous avez créé la discussion {discussion}.",
"{profile} created the discussion {discussion}.": "{profile} a créé la discussion {discussion}.",
"You replied to the discussion {discussion}.": "Vous avez répondu à la discussion {discussion}.",
"{profile} replied to the discussion {discussion}.": "{profile} a répondu à la discussion {discussion}.",
"You renamed the discussion from {old_discussion} to {discussion}.": "Vous avez renommé la discussion {old_discussion} en {discussion}.",
"{profile} renamed the discussion from {old_discussion} to {discussion}.": "{profile} a renommé la discussion {old_discussion} en {discussion}.",
"You archived the discussion {discussion}.": "Vous avez archivé la discussion {discussion}.",
"{profile} archived the discussion {discussion}.": "{profile} a archivé la discussion {discussion}.",
"You deleted the discussion {discussion}.": "Vous avez supprimé la discussion {discussion}.",
"{profile} deleted the discussion {discussion}.": "{profile} a supprimé la discussion {discussion}.",
"You created the group {group}.": "Vous avez créé le groupe {group}.",
"{profile} created the group {group}.": "{profile} a créé le groupe {group}.",
"You updated the group {group}.": "Vous avez mis à jour le groupe {group}.",
"{profile} updated the group {group}.": "{profile} a mis à jour le groupe {group}.",
"{old_group_name} was renamed to {group}.": "{old_group_name} a été renommé en {group}.",
"Visibility was set to private.": "La visibilité a été définie à privée.",
"Visibility was set to public.": "La visibilité a été définie à public.",
"Visibility was set to an unknown value.": "La visibilité a été définie à une valeur inconnue.",
"The group can now only be joined with an invite.": "Le groupe peut maintenant être rejoint uniquement sur invitation.",
"The group can now be joined by anyone.": "Le groupe peut maintenant être rejoint par n'importe qui.",
"Unknown value for the openness setting.": "Valeur inconnue pour le paramètre d'ouverture.",
"The group's physical address was changed.": "L'adresse physique du groupe a été changée.",
"The group's avatar was changed.": "L'avatar du groupe a été changé.",
"The group's banner was changed.": "La bannière du groupe a été changée.",
"The group's short description was changed.": "La description courte du groupe a été changée."
}

@ -0,0 +1,31 @@
import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
import { IActivity } from "@/types/activity.model";
import { IActor } from "@/types/actor";
import { Component, Prop, Vue } from "vue-property-decorator";
@Component({
apollo: {
currentActor: CURRENT_ACTOR_CLIENT,
},
})
export default class ActivityMixin extends Vue {
@Prop({ required: true, type: Object }) activity!: IActivity;
currentActor!: IActor;
get subjectParams(): Record<string, string> {
return this.activity.subjectParams.reduce(
(acc: Record<string, string>, { key, value }) => {
acc[key] = value;
return acc;
},
{}
);
}
get isAuthorCurrentActor(): boolean {
return (
this.activity.author.id === this.currentActor.id &&
this.currentActor.id !== undefined
);
}
}

@ -18,6 +18,7 @@ export enum GroupsRouteName {