Browse Source

Merge branch 'olympia-forum-mappings-part-2' into olympiaCouncilMappings

ondratra 3 years ago
parent
commit
94ca90ebdb
100 changed files with 3664 additions and 900 deletions
  1. 1 1
      README.md
  2. 10 10
      analyses/fee-analysis/main_notebook.ipynb
  3. 2 2
      cli/package.json
  4. 1 1
      cli/src/Api.ts
  5. 2 2
      cli/src/base/ApiCommandBase.ts
  6. 4 5
      cli/src/base/ContentDirectoryCommandBase.ts
  7. 1 1
      cli/src/commands/api/inspect.ts
  8. 1 1
      cli/src/commands/content/curatorGroup.ts
  9. 1 1
      cli/src/commands/content/curatorGroups.ts
  10. 1 1
      cli/src/commands/content/removeCuratorFromGroup.ts
  11. 3 2
      cli/src/commands/working-groups/fillOpening.ts
  12. 2 2
      docker-compose.yml
  13. 192 0
      metadata-protobuf/compiled/index.d.ts
  14. 436 0
      metadata-protobuf/compiled/index.js
  15. 17 0
      metadata-protobuf/doc/index.md
  16. 3 2
      metadata-protobuf/package.json
  17. 12 0
      metadata-protobuf/proto/Forum.proto
  18. 6 0
      metadata-protobuf/proto/ProposalsDiscussion.proto
  19. 1 0
      metadata-protobuf/src/consts.ts
  20. 13 0
      metadata-protobuf/test/forum-tags.ts
  21. 9 12
      package.json
  22. 2 3
      pioneer/packages/joy-election/src/VoteForm.tsx
  23. 1 1
      pioneer/packages/react-api/package.json
  24. 5 11
      query-node/manifest.yml
  25. 8 1
      query-node/mappings/common.ts
  26. 172 36
      query-node/mappings/forum.ts
  27. 5 9
      query-node/mappings/membership.ts
  28. 1 1
      query-node/mappings/package.json
  29. 24 21
      query-node/mappings/proposals.ts
  30. 176 14
      query-node/mappings/proposalsDiscussion.ts
  31. 28 2
      query-node/schemas/forum.graphql
  32. 2 17
      query-node/schemas/forumEvents.graphql
  33. 8 2
      query-node/schemas/proposalDiscussion.graphql
  34. 0 24
      query-node/schemas/proposalDiscussionEvents.graphql
  35. 3 0
      query-node/schemas/proposals.graphql
  36. 12 12
      runtime-modules/forum/src/benchmarking.rs
  37. 23 17
      runtime-modules/forum/src/lib.rs
  38. 11 13
      runtime-modules/forum/src/mock.rs
  39. 7 7
      runtime-modules/forum/src/tests.rs
  40. 1 1
      runtime/src/weights/forum.rs
  41. 1 1
      storage-node/package.json
  42. 1 1
      storage-node/packages/runtime-api/package.json
  43. 6 5
      tests/integration-tests/package.json
  44. 39 3
      tests/integration-tests/src/Api.ts
  45. 102 19
      tests/integration-tests/src/QueryNodeApi.ts
  46. 1 1
      tests/integration-tests/src/Scenario.ts
  47. 2 0
      tests/integration-tests/src/consts.ts
  48. 5 4
      tests/integration-tests/src/fixtures/forum/AddPostsFixture.ts
  49. 37 16
      tests/integration-tests/src/fixtures/forum/CreateThreadsFixture.ts
  50. 14 11
      tests/integration-tests/src/fixtures/forum/DeletePostsFixture.ts
  51. 4 3
      tests/integration-tests/src/fixtures/forum/DeleteThreadsFixture.ts
  52. 1 1
      tests/integration-tests/src/fixtures/forum/InitializeForumFixture.ts
  53. 1 0
      tests/integration-tests/src/fixtures/forum/ModeratePostsFixture.ts
  54. 4 3
      tests/integration-tests/src/fixtures/forum/ModerateThreadsFixture.ts
  55. 3 3
      tests/integration-tests/src/fixtures/forum/MoveThreadsFixture.ts
  56. 0 103
      tests/integration-tests/src/fixtures/forum/UpdateThreadTitlesFixture.ts
  57. 135 0
      tests/integration-tests/src/fixtures/forum/UpdateThreadsMetadataFixture.ts
  58. 3 3
      tests/integration-tests/src/fixtures/forum/VoteOnPollFixture.ts
  59. 1 1
      tests/integration-tests/src/fixtures/forum/index.ts
  60. 1 0
      tests/integration-tests/src/fixtures/proposals/CancelProposalsFixture.ts
  61. 2 0
      tests/integration-tests/src/fixtures/proposals/CreateProposalsFixture.ts
  62. 4 0
      tests/integration-tests/src/fixtures/proposals/DecideOnProposalStatusFixture.ts
  63. 1 0
      tests/integration-tests/src/fixtures/proposals/ExpireProposalsFixture.ts
  64. 103 0
      tests/integration-tests/src/fixtures/proposalsDiscussion/ChangeThreadsModeFixture.ts
  65. 123 0
      tests/integration-tests/src/fixtures/proposalsDiscussion/CreatePostsFixture.ts
  66. 84 0
      tests/integration-tests/src/fixtures/proposalsDiscussion/DeletePostsFixture.ts
  67. 81 0
      tests/integration-tests/src/fixtures/proposalsDiscussion/UpdatePostsFixture.ts
  68. 4 0
      tests/integration-tests/src/fixtures/proposalsDiscussion/index.ts
  69. 2 2
      tests/integration-tests/src/fixtures/workingGroups/FillOpeningsFixture.ts
  70. 1 1
      tests/integration-tests/src/flows/forum/polls.ts
  71. 64 0
      tests/integration-tests/src/flows/forum/threadTags.ts
  72. 8 8
      tests/integration-tests/src/flows/forum/threads.ts
  73. 2 2
      tests/integration-tests/src/flows/proposals/expireProposal.ts
  74. 127 0
      tests/integration-tests/src/flows/proposalsDiscussion/index.ts
  75. 390 26
      tests/integration-tests/src/graphql/generated/queries.ts
  76. 347 317
      tests/integration-tests/src/graphql/generated/schema.ts
  77. 12 5
      tests/integration-tests/src/graphql/queries/forum.graphql
  78. 6 4
      tests/integration-tests/src/graphql/queries/forumEvents.graphql
  79. 7 0
      tests/integration-tests/src/graphql/queries/proposals.graphql
  80. 76 0
      tests/integration-tests/src/graphql/queries/proposalsDiscussion.graphql
  81. 82 0
      tests/integration-tests/src/graphql/queries/proposalsDiscussionEvents.graphql
  82. 2 0
      tests/integration-tests/src/scenarios/forum.ts
  83. 5 1
      tests/integration-tests/src/scenarios/full.ts
  84. 9 3
      tests/integration-tests/src/scenarios/proposals.ts
  85. 8 0
      tests/integration-tests/src/scenarios/proposalsDiscussion.ts
  86. 14 2
      tests/integration-tests/src/types.ts
  87. 4 0
      tests/integration-tests/src/utils.ts
  88. 1 1
      types/augment-codec/all.ts
  89. 176 7
      types/augment-codec/augment-api-consts.ts
  90. 11 17
      types/augment-codec/augment-api-events.ts
  91. 16 16
      types/augment-codec/augment-api-query.ts
  92. 31 5
      types/augment-codec/augment-api-rpc.ts
  93. 9 9
      types/augment-codec/augment-api-tx.ts
  94. 0 1
      types/augment-codec/augment-types.ts
  95. 23 9
      types/augment/all/defs.json
  96. 27 6
      types/augment/all/types.ts
  97. 176 7
      types/augment/augment-api-consts.ts
  98. 11 17
      types/augment/augment-api-events.ts
  99. 16 16
      types/augment/augment-api-query.ts
  100. 31 5
      types/augment/augment-api-rpc.ts

+ 1 - 1
README.md

@@ -12,7 +12,7 @@ functionality to support the [various roles](https://www.joystream.org/roles) th
 The following tools are required for building, testing and contributing to this repo:
 
 - [Rust](https://www.rust-lang.org/tools/install) toolchain - _required_
-- [nodejs](https://nodejs.org/) v12.x - _required_
+- [nodejs](https://nodejs.org/) v14.x - _required_
 - [yarn classic](https://classic.yarnpkg.com/en/docs/install) package manager v1.22.x- _required_
 - [docker](https://www.docker.com/get-started) and docker-compose - _optional_
 - [ansible](https://www.ansible.com/) - _optional_

+ 10 - 10
analyses/fee-analysis/main_notebook.ipynb

@@ -1087,7 +1087,7 @@
         "delete_category_lead",
         "delete_category_moderator",
         "create_thread",
-        "edit_thread_title",
+        "edit_thread_metadata",
         "update_thread_archival_status_lead",
         "update_thread_archival_status_moderator",
         "delete_thread_lead",
@@ -1473,7 +1473,7 @@
         "delete_category_lead",
         "delete_category_moderator",
         "create_thread",
-        "edit_thread_title",
+        "edit_thread_metadata",
         "update_thread_archival_status_lead",
         "update_thread_archival_status_moderator",
         "delete_thread_lead",
@@ -1828,7 +1828,7 @@
         "delete_category_lead",
         "delete_category_moderator",
         "create_thread",
-        "edit_thread_title",
+        "edit_thread_metadata",
         "update_thread_archival_status_lead",
         "update_thread_archival_status_moderator",
         "delete_thread_lead",
@@ -2399,7 +2399,7 @@
         "delete_category_lead",
         "delete_category_moderator",
         "create_thread",
-        "edit_thread_title",
+        "edit_thread_metadata",
         "update_thread_archival_status_lead",
         "update_thread_archival_status_moderator",
         "delete_thread_lead",
@@ -2698,7 +2698,7 @@
         "delete_category_lead",
         "delete_category_moderator",
         "create_thread",
-        "edit_thread_title",
+        "edit_thread_metadata",
         "update_thread_archival_status_lead",
         "update_thread_archival_status_moderator",
         "delete_thread_lead",
@@ -2952,7 +2952,7 @@
         "delete_category_lead",
         "delete_category_moderator",
         "create_thread",
-        "edit_thread_title",
+        "edit_thread_metadata",
         "update_thread_archival_status_lead",
         "update_thread_archival_status_moderator",
         "delete_thread_lead",
@@ -3415,7 +3415,7 @@
         "delete_category_lead",
         "delete_category_moderator",
         "create_thread",
-        "edit_thread_title",
+        "edit_thread_metadata",
         "update_thread_archival_status_lead",
         "update_thread_archival_status_moderator",
         "delete_thread_lead",
@@ -3706,7 +3706,7 @@
         "delete_category_lead",
         "delete_category_moderator",
         "create_thread",
-        "edit_thread_title",
+        "edit_thread_metadata",
         "update_thread_archival_status_lead",
         "update_thread_archival_status_moderator",
         "delete_thread_lead",
@@ -3897,7 +3897,7 @@
         "delete_category_lead",
         "delete_category_moderator",
         "create_thread",
-        "edit_thread_title",
+        "edit_thread_metadata",
         "update_thread_archival_status_lead",
         "update_thread_archival_status_moderator",
         "delete_thread_lead",
@@ -4176,7 +4176,7 @@
         "delete_category_lead",
         "delete_category_moderator",
         "create_thread",
-        "edit_thread_title",
+        "edit_thread_metadata",
         "update_thread_archival_status_lead",
         "update_thread_archival_status_moderator",
         "delete_thread_lead",

+ 2 - 2
cli/package.json

@@ -19,7 +19,7 @@
     "@oclif/plugin-help": "^3.2.2",
     "@oclif/plugin-not-found": "^1.2.4",
     "@oclif/plugin-warn-if-update-available": "^1.7.0",
-    "@polkadot/api": "4.2.1",
+    "@polkadot/api": "5.3.2",
     "@types/cli-progress": "^3.9.1",
     "@types/fluent-ffmpeg": "^2.1.16",
     "@types/inquirer": "^6.5.0",
@@ -49,7 +49,7 @@
   "devDependencies": {
     "@oclif/dev-cli": "^1.22.2",
     "@oclif/test": "^1.2.5",
-    "@polkadot/ts": "^0.3.62",
+    "@polkadot/ts": "^0.4.4",
     "@types/chai": "^4.2.11",
     "@types/mocha": "^5.2.7",
     "@types/node": "^10.17.18",

+ 1 - 1
cli/src/Api.ts

@@ -42,7 +42,7 @@ import {
   VideoCategory,
 } from '@joystream/types/content'
 import { ContentId, DataObject } from '@joystream/types/storage'
-import { ApolloClient, InMemoryCache, HttpLink, NormalizedCacheObject, DocumentNode } from '@apollo/client'
+import { ApolloClient, InMemoryCache, HttpLink, NormalizedCacheObject, DocumentNode } from '@apollo/client/core'
 import fetch from 'cross-fetch'
 import { Maybe } from './graphql/generated/schema'
 import {

+ 2 - 2
cli/src/base/ApiCommandBase.ts

@@ -435,8 +435,8 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
                 let errorMsg = dispatchError.toString()
                 if (dispatchError.isModule) {
                   try {
-                    const { name, documentation } = this.getOriginalApi().registry.findMetaError(dispatchError.asModule)
-                    errorMsg = `${name} (${documentation})`
+                    const { name, docs } = this.getOriginalApi().registry.findMetaError(dispatchError.asModule)
+                    errorMsg = `${name} (${docs.join(', ')})`
                   } catch (e) {
                     // This probably means we don't have this error in the metadata
                     // In this case - continue (we'll just display dispatchError.toString())

+ 4 - 5
cli/src/base/ContentDirectoryCommandBase.ts

@@ -4,7 +4,6 @@ import { CuratorGroup, CuratorGroupId, ContentActor, Channel } from '@joystream/
 import { Worker } from '@joystream/types/working-group'
 import { CLIError } from '@oclif/errors'
 import { RolesCommandBase } from './WorkingGroupsCommandBase'
-import { createType } from '@joystream/types'
 import { flags } from '@oclif/command'
 import { memberHandle } from '../helpers/display'
 
@@ -111,7 +110,7 @@ export default abstract class ContentDirectoryCommandBase extends RolesCommandBa
       if (!group.active.valueOf()) {
         this.error(`Curator group ${requiredGroupId.toString()} is no longer active`, { exit: ExitCodes.AccessDenied })
       }
-      if (!group.curators.toArray().some((curatorId) => curatorId.eq(curator.workerId))) {
+      if (!Array.from(group.curators).some((curatorId) => curatorId.eq(curator.workerId))) {
         this.error(`You don't belong to required curator group (ID: ${requiredGroupId.toString()})`, {
           exit: ExitCodes.AccessDenied,
         })
@@ -122,7 +121,7 @@ export default abstract class ContentDirectoryCommandBase extends RolesCommandBa
       const availableGroupIds = groups
         .filter(
           ([, group]) =>
-            group.active.valueOf() && group.curators.toArray().some((curatorId) => curatorId.eq(curator.workerId))
+            group.active.valueOf() && Array.from(group.curators).some((curatorId) => curatorId.eq(curator.workerId))
         )
         .map(([id]) => id)
 
@@ -136,7 +135,7 @@ export default abstract class ContentDirectoryCommandBase extends RolesCommandBa
     }
 
     return [
-      createType('ContentActor', { Curator: [groupId, curator.workerId.toNumber()] }),
+      this.createType('ContentActor', { Curator: [groupId, curator.workerId.toNumber()] }),
       curator.roleAccount.toString(),
     ]
   }
@@ -149,7 +148,7 @@ export default abstract class ContentDirectoryCommandBase extends RolesCommandBa
         name:
           `Group ${id.toString()} (` +
           `${group.active.valueOf() ? 'Active' : 'Inactive'}, ` +
-          `${group.curators.toArray().length} member(s)), `,
+          `${Array.from(group.curators).length} member(s)), `,
         value: id.toNumber(),
       }))
   }

+ 1 - 1
cli/src/commands/api/inspect.ts

@@ -86,7 +86,7 @@ export default class ApiInspect extends ApiCommandBase {
   }
 
   getMethodDescription(apiType: ApiType, apiModule: string, apiMethod: string): string {
-    const description: string = this.getMethodMeta(apiType, apiModule, apiMethod).documentation.join(' ')
+    const description: string = this.getMethodMeta(apiType, apiModule, apiMethod).docs.join(' ')
     return description || 'No description available.'
   }
 

+ 1 - 1
cli/src/commands/content/curatorGroup.ts

@@ -17,7 +17,7 @@ export default class CuratorGroupCommand extends ContentDirectoryCommandBase {
     const { id } = this.parse(CuratorGroupCommand).args
     const group = await this.getCuratorGroup(id)
     const members = (await this.getApi().groupMembers(WorkingGroups.Curators)).filter((curator) =>
-      group.curators.toArray().some((groupCurator) => groupCurator.eq(curator.workerId))
+      Array.from(group.curators).some((groupCurator) => groupCurator.eq(curator.workerId))
     )
 
     displayCollapsedRow({

+ 1 - 1
cli/src/commands/content/curatorGroups.ts

@@ -13,7 +13,7 @@ export default class CuratorGroupsCommand extends ContentDirectoryCommandBase {
         groups.map(([id, group]) => ({
           'ID': id.toString(),
           'Status': group.active.valueOf() ? 'Active' : 'Inactive',
-          'Members': group.curators.toArray().length,
+          'Members': Array.from(group.curators).length,
         })),
         5
       )

+ 1 - 1
cli/src/commands/content/removeCuratorFromGroup.ts

@@ -26,7 +26,7 @@ export default class RemoveCuratorFromGroupCommand extends ContentDirectoryComma
     }
 
     const group = await this.getCuratorGroup(groupId)
-    const groupCuratorIds = group.curators.toArray().map((id) => id.toNumber())
+    const groupCuratorIds = Array.from(group.curators).map((id) => id.toNumber())
 
     if (curatorId === undefined) {
       curatorId = await this.promptForCurator('Choose a Curator to remove', groupCuratorIds)

+ 3 - 2
cli/src/commands/working-groups/fillOpening.ts

@@ -1,8 +1,9 @@
 import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
 import { apiModuleByGroup } from '../../Api'
 import chalk from 'chalk'
-import { JoyBTreeSet } from '@joystream/types/common'
 import { ApplicationId } from '@joystream/types/working-group'
+import { BTreeSet } from '@polkadot/types'
+import { registry } from '@joystream/types'
 
 export default class WorkingGroupsFillOpening extends WorkingGroupsCommandBase {
   static description = "Allows filling working group opening that's currently in review. Requires lead access."
@@ -33,7 +34,7 @@ export default class WorkingGroupsFillOpening extends WorkingGroupsCommandBase {
       await this.getDecodedPair(lead.roleAccount.toString()),
       apiModuleByGroup[this.group],
       'fillOpening',
-      [openingId, new (JoyBTreeSet(ApplicationId))(this.getTypesRegistry(), applicationIds)]
+      [openingId, new (BTreeSet.with(ApplicationId))(registry, applicationIds)]
     )
 
     this.log(chalk.green(`Opening ${chalk.magentaBright(openingId.toString())} succesfully filled!`))

+ 2 - 2
docker-compose.yml

@@ -89,7 +89,7 @@ services:
 
   graphql-server-mnt:
     <<: *graphql-server
-    image: node:12
+    image: node:14
     build: .
     volumes:
       - type: bind
@@ -121,7 +121,7 @@ services:
 
   processor-mnt:
     <<: *processor
-    image: node:12
+    image: node:14
     build: .
     volumes:
       - type: bind

+ 192 - 0
metadata-protobuf/compiled/index.d.ts

@@ -513,6 +513,102 @@ export class ForumPostMetadata implements IForumPostMetadata {
     public toJSON(): { [k: string]: any };
 }
 
+/** Properties of a ForumThreadMetadata. */
+export interface IForumThreadMetadata {
+
+    /** ForumThreadMetadata title */
+    title?: (string|null);
+
+    /** ForumThreadMetadata tags */
+    tags?: (string[]|null);
+}
+
+/** Represents a ForumThreadMetadata. */
+export class ForumThreadMetadata implements IForumThreadMetadata {
+
+    /**
+     * Constructs a new ForumThreadMetadata.
+     * @param [properties] Properties to set
+     */
+    constructor(properties?: IForumThreadMetadata);
+
+    /** ForumThreadMetadata title. */
+    public title: string;
+
+    /** ForumThreadMetadata tags. */
+    public tags: string[];
+
+    /**
+     * Creates a new ForumThreadMetadata instance using the specified properties.
+     * @param [properties] Properties to set
+     * @returns ForumThreadMetadata instance
+     */
+    public static create(properties?: IForumThreadMetadata): ForumThreadMetadata;
+
+    /**
+     * Encodes the specified ForumThreadMetadata message. Does not implicitly {@link ForumThreadMetadata.verify|verify} messages.
+     * @param message ForumThreadMetadata message or plain object to encode
+     * @param [writer] Writer to encode to
+     * @returns Writer
+     */
+    public static encode(message: IForumThreadMetadata, writer?: $protobuf.Writer): $protobuf.Writer;
+
+    /**
+     * Encodes the specified ForumThreadMetadata message, length delimited. Does not implicitly {@link ForumThreadMetadata.verify|verify} messages.
+     * @param message ForumThreadMetadata message or plain object to encode
+     * @param [writer] Writer to encode to
+     * @returns Writer
+     */
+    public static encodeDelimited(message: IForumThreadMetadata, writer?: $protobuf.Writer): $protobuf.Writer;
+
+    /**
+     * Decodes a ForumThreadMetadata message from the specified reader or buffer.
+     * @param reader Reader or buffer to decode from
+     * @param [length] Message length if known beforehand
+     * @returns ForumThreadMetadata
+     * @throws {Error} If the payload is not a reader or valid buffer
+     * @throws {$protobuf.util.ProtocolError} If required fields are missing
+     */
+    public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): ForumThreadMetadata;
+
+    /**
+     * Decodes a ForumThreadMetadata message from the specified reader or buffer, length delimited.
+     * @param reader Reader or buffer to decode from
+     * @returns ForumThreadMetadata
+     * @throws {Error} If the payload is not a reader or valid buffer
+     * @throws {$protobuf.util.ProtocolError} If required fields are missing
+     */
+    public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): ForumThreadMetadata;
+
+    /**
+     * Verifies a ForumThreadMetadata message.
+     * @param message Plain object to verify
+     * @returns `null` if valid, otherwise the reason why it is not
+     */
+    public static verify(message: { [k: string]: any }): (string|null);
+
+    /**
+     * Creates a ForumThreadMetadata message from a plain object. Also converts values to their respective internal types.
+     * @param object Plain object
+     * @returns ForumThreadMetadata
+     */
+    public static fromObject(object: { [k: string]: any }): ForumThreadMetadata;
+
+    /**
+     * Creates a plain object from a ForumThreadMetadata message. Also converts values to other types if specified.
+     * @param message ForumThreadMetadata
+     * @param [options] Conversion options
+     * @returns Plain object
+     */
+    public static toObject(message: ForumThreadMetadata, options?: $protobuf.IConversionOptions): { [k: string]: any };
+
+    /**
+     * Converts this ForumThreadMetadata to JSON.
+     * @returns JSON object
+     */
+    public toJSON(): { [k: string]: any };
+}
+
 /** Properties of a MembershipMetadata. */
 export interface IMembershipMetadata {
 
@@ -831,6 +927,102 @@ export class PlaylistMetadata implements IPlaylistMetadata {
     public toJSON(): { [k: string]: any };
 }
 
+/** Properties of a ProposalsDiscussionPostMetadata. */
+export interface IProposalsDiscussionPostMetadata {
+
+    /** ProposalsDiscussionPostMetadata text */
+    text?: (string|null);
+
+    /** ProposalsDiscussionPostMetadata repliesTo */
+    repliesTo?: (number|null);
+}
+
+/** Represents a ProposalsDiscussionPostMetadata. */
+export class ProposalsDiscussionPostMetadata implements IProposalsDiscussionPostMetadata {
+
+    /**
+     * Constructs a new ProposalsDiscussionPostMetadata.
+     * @param [properties] Properties to set
+     */
+    constructor(properties?: IProposalsDiscussionPostMetadata);
+
+    /** ProposalsDiscussionPostMetadata text. */
+    public text: string;
+
+    /** ProposalsDiscussionPostMetadata repliesTo. */
+    public repliesTo: number;
+
+    /**
+     * Creates a new ProposalsDiscussionPostMetadata instance using the specified properties.
+     * @param [properties] Properties to set
+     * @returns ProposalsDiscussionPostMetadata instance
+     */
+    public static create(properties?: IProposalsDiscussionPostMetadata): ProposalsDiscussionPostMetadata;
+
+    /**
+     * Encodes the specified ProposalsDiscussionPostMetadata message. Does not implicitly {@link ProposalsDiscussionPostMetadata.verify|verify} messages.
+     * @param message ProposalsDiscussionPostMetadata message or plain object to encode
+     * @param [writer] Writer to encode to
+     * @returns Writer
+     */
+    public static encode(message: IProposalsDiscussionPostMetadata, writer?: $protobuf.Writer): $protobuf.Writer;
+
+    /**
+     * Encodes the specified ProposalsDiscussionPostMetadata message, length delimited. Does not implicitly {@link ProposalsDiscussionPostMetadata.verify|verify} messages.
+     * @param message ProposalsDiscussionPostMetadata message or plain object to encode
+     * @param [writer] Writer to encode to
+     * @returns Writer
+     */
+    public static encodeDelimited(message: IProposalsDiscussionPostMetadata, writer?: $protobuf.Writer): $protobuf.Writer;
+
+    /**
+     * Decodes a ProposalsDiscussionPostMetadata message from the specified reader or buffer.
+     * @param reader Reader or buffer to decode from
+     * @param [length] Message length if known beforehand
+     * @returns ProposalsDiscussionPostMetadata
+     * @throws {Error} If the payload is not a reader or valid buffer
+     * @throws {$protobuf.util.ProtocolError} If required fields are missing
+     */
+    public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): ProposalsDiscussionPostMetadata;
+
+    /**
+     * Decodes a ProposalsDiscussionPostMetadata message from the specified reader or buffer, length delimited.
+     * @param reader Reader or buffer to decode from
+     * @returns ProposalsDiscussionPostMetadata
+     * @throws {Error} If the payload is not a reader or valid buffer
+     * @throws {$protobuf.util.ProtocolError} If required fields are missing
+     */
+    public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): ProposalsDiscussionPostMetadata;
+
+    /**
+     * Verifies a ProposalsDiscussionPostMetadata message.
+     * @param message Plain object to verify
+     * @returns `null` if valid, otherwise the reason why it is not
+     */
+    public static verify(message: { [k: string]: any }): (string|null);
+
+    /**
+     * Creates a ProposalsDiscussionPostMetadata message from a plain object. Also converts values to their respective internal types.
+     * @param object Plain object
+     * @returns ProposalsDiscussionPostMetadata
+     */
+    public static fromObject(object: { [k: string]: any }): ProposalsDiscussionPostMetadata;
+
+    /**
+     * Creates a plain object from a ProposalsDiscussionPostMetadata message. Also converts values to other types if specified.
+     * @param message ProposalsDiscussionPostMetadata
+     * @param [options] Conversion options
+     * @returns Plain object
+     */
+    public static toObject(message: ProposalsDiscussionPostMetadata, options?: $protobuf.IConversionOptions): { [k: string]: any };
+
+    /**
+     * Converts this ProposalsDiscussionPostMetadata to JSON.
+     * @returns JSON object
+     */
+    public toJSON(): { [k: string]: any };
+}
+
 /** Properties of a SeriesMetadata. */
 export interface ISeriesMetadata {
 

+ 436 - 0
metadata-protobuf/compiled/index.js

@@ -1185,6 +1185,232 @@ $root.ForumPostMetadata = (function() {
     return ForumPostMetadata;
 })();
 
+$root.ForumThreadMetadata = (function() {
+
+    /**
+     * Properties of a ForumThreadMetadata.
+     * @exports IForumThreadMetadata
+     * @interface IForumThreadMetadata
+     * @property {string|null} [title] ForumThreadMetadata title
+     * @property {Array.<string>|null} [tags] ForumThreadMetadata tags
+     */
+
+    /**
+     * Constructs a new ForumThreadMetadata.
+     * @exports ForumThreadMetadata
+     * @classdesc Represents a ForumThreadMetadata.
+     * @implements IForumThreadMetadata
+     * @constructor
+     * @param {IForumThreadMetadata=} [properties] Properties to set
+     */
+    function ForumThreadMetadata(properties) {
+        this.tags = [];
+        if (properties)
+            for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i)
+                if (properties[keys[i]] != null)
+                    this[keys[i]] = properties[keys[i]];
+    }
+
+    /**
+     * ForumThreadMetadata title.
+     * @member {string} title
+     * @memberof ForumThreadMetadata
+     * @instance
+     */
+    ForumThreadMetadata.prototype.title = "";
+
+    /**
+     * ForumThreadMetadata tags.
+     * @member {Array.<string>} tags
+     * @memberof ForumThreadMetadata
+     * @instance
+     */
+    ForumThreadMetadata.prototype.tags = $util.emptyArray;
+
+    /**
+     * Creates a new ForumThreadMetadata instance using the specified properties.
+     * @function create
+     * @memberof ForumThreadMetadata
+     * @static
+     * @param {IForumThreadMetadata=} [properties] Properties to set
+     * @returns {ForumThreadMetadata} ForumThreadMetadata instance
+     */
+    ForumThreadMetadata.create = function create(properties) {
+        return new ForumThreadMetadata(properties);
+    };
+
+    /**
+     * Encodes the specified ForumThreadMetadata message. Does not implicitly {@link ForumThreadMetadata.verify|verify} messages.
+     * @function encode
+     * @memberof ForumThreadMetadata
+     * @static
+     * @param {IForumThreadMetadata} message ForumThreadMetadata message or plain object to encode
+     * @param {$protobuf.Writer} [writer] Writer to encode to
+     * @returns {$protobuf.Writer} Writer
+     */
+    ForumThreadMetadata.encode = function encode(message, writer) {
+        if (!writer)
+            writer = $Writer.create();
+        if (message.title != null && Object.hasOwnProperty.call(message, "title"))
+            writer.uint32(/* id 1, wireType 2 =*/10).string(message.title);
+        if (message.tags != null && message.tags.length)
+            for (var i = 0; i < message.tags.length; ++i)
+                writer.uint32(/* id 2, wireType 2 =*/18).string(message.tags[i]);
+        return writer;
+    };
+
+    /**
+     * Encodes the specified ForumThreadMetadata message, length delimited. Does not implicitly {@link ForumThreadMetadata.verify|verify} messages.
+     * @function encodeDelimited
+     * @memberof ForumThreadMetadata
+     * @static
+     * @param {IForumThreadMetadata} message ForumThreadMetadata message or plain object to encode
+     * @param {$protobuf.Writer} [writer] Writer to encode to
+     * @returns {$protobuf.Writer} Writer
+     */
+    ForumThreadMetadata.encodeDelimited = function encodeDelimited(message, writer) {
+        return this.encode(message, writer).ldelim();
+    };
+
+    /**
+     * Decodes a ForumThreadMetadata message from the specified reader or buffer.
+     * @function decode
+     * @memberof ForumThreadMetadata
+     * @static
+     * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from
+     * @param {number} [length] Message length if known beforehand
+     * @returns {ForumThreadMetadata} ForumThreadMetadata
+     * @throws {Error} If the payload is not a reader or valid buffer
+     * @throws {$protobuf.util.ProtocolError} If required fields are missing
+     */
+    ForumThreadMetadata.decode = function decode(reader, length) {
+        if (!(reader instanceof $Reader))
+            reader = $Reader.create(reader);
+        var end = length === undefined ? reader.len : reader.pos + length, message = new $root.ForumThreadMetadata();
+        while (reader.pos < end) {
+            var tag = reader.uint32();
+            switch (tag >>> 3) {
+            case 1:
+                message.title = reader.string();
+                break;
+            case 2:
+                if (!(message.tags && message.tags.length))
+                    message.tags = [];
+                message.tags.push(reader.string());
+                break;
+            default:
+                reader.skipType(tag & 7);
+                break;
+            }
+        }
+        return message;
+    };
+
+    /**
+     * Decodes a ForumThreadMetadata message from the specified reader or buffer, length delimited.
+     * @function decodeDelimited
+     * @memberof ForumThreadMetadata
+     * @static
+     * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from
+     * @returns {ForumThreadMetadata} ForumThreadMetadata
+     * @throws {Error} If the payload is not a reader or valid buffer
+     * @throws {$protobuf.util.ProtocolError} If required fields are missing
+     */
+    ForumThreadMetadata.decodeDelimited = function decodeDelimited(reader) {
+        if (!(reader instanceof $Reader))
+            reader = new $Reader(reader);
+        return this.decode(reader, reader.uint32());
+    };
+
+    /**
+     * Verifies a ForumThreadMetadata message.
+     * @function verify
+     * @memberof ForumThreadMetadata
+     * @static
+     * @param {Object.<string,*>} message Plain object to verify
+     * @returns {string|null} `null` if valid, otherwise the reason why it is not
+     */
+    ForumThreadMetadata.verify = function verify(message) {
+        if (typeof message !== "object" || message === null)
+            return "object expected";
+        if (message.title != null && message.hasOwnProperty("title"))
+            if (!$util.isString(message.title))
+                return "title: string expected";
+        if (message.tags != null && message.hasOwnProperty("tags")) {
+            if (!Array.isArray(message.tags))
+                return "tags: array expected";
+            for (var i = 0; i < message.tags.length; ++i)
+                if (!$util.isString(message.tags[i]))
+                    return "tags: string[] expected";
+        }
+        return null;
+    };
+
+    /**
+     * Creates a ForumThreadMetadata message from a plain object. Also converts values to their respective internal types.
+     * @function fromObject
+     * @memberof ForumThreadMetadata
+     * @static
+     * @param {Object.<string,*>} object Plain object
+     * @returns {ForumThreadMetadata} ForumThreadMetadata
+     */
+    ForumThreadMetadata.fromObject = function fromObject(object) {
+        if (object instanceof $root.ForumThreadMetadata)
+            return object;
+        var message = new $root.ForumThreadMetadata();
+        if (object.title != null)
+            message.title = String(object.title);
+        if (object.tags) {
+            if (!Array.isArray(object.tags))
+                throw TypeError(".ForumThreadMetadata.tags: array expected");
+            message.tags = [];
+            for (var i = 0; i < object.tags.length; ++i)
+                message.tags[i] = String(object.tags[i]);
+        }
+        return message;
+    };
+
+    /**
+     * Creates a plain object from a ForumThreadMetadata message. Also converts values to other types if specified.
+     * @function toObject
+     * @memberof ForumThreadMetadata
+     * @static
+     * @param {ForumThreadMetadata} message ForumThreadMetadata
+     * @param {$protobuf.IConversionOptions} [options] Conversion options
+     * @returns {Object.<string,*>} Plain object
+     */
+    ForumThreadMetadata.toObject = function toObject(message, options) {
+        if (!options)
+            options = {};
+        var object = {};
+        if (options.arrays || options.defaults)
+            object.tags = [];
+        if (options.defaults)
+            object.title = "";
+        if (message.title != null && message.hasOwnProperty("title"))
+            object.title = message.title;
+        if (message.tags && message.tags.length) {
+            object.tags = [];
+            for (var j = 0; j < message.tags.length; ++j)
+                object.tags[j] = message.tags[j];
+        }
+        return object;
+    };
+
+    /**
+     * Converts this ForumThreadMetadata to JSON.
+     * @function toJSON
+     * @memberof ForumThreadMetadata
+     * @instance
+     * @returns {Object.<string,*>} JSON object
+     */
+    ForumThreadMetadata.prototype.toJSON = function toJSON() {
+        return this.constructor.toObject(this, $protobuf.util.toJSONOptions);
+    };
+
+    return ForumThreadMetadata;
+})();
+
 $root.MembershipMetadata = (function() {
 
     /**
@@ -1956,6 +2182,216 @@ $root.PlaylistMetadata = (function() {
     return PlaylistMetadata;
 })();
 
+$root.ProposalsDiscussionPostMetadata = (function() {
+
+    /**
+     * Properties of a ProposalsDiscussionPostMetadata.
+     * @exports IProposalsDiscussionPostMetadata
+     * @interface IProposalsDiscussionPostMetadata
+     * @property {string|null} [text] ProposalsDiscussionPostMetadata text
+     * @property {number|null} [repliesTo] ProposalsDiscussionPostMetadata repliesTo
+     */
+
+    /**
+     * Constructs a new ProposalsDiscussionPostMetadata.
+     * @exports ProposalsDiscussionPostMetadata
+     * @classdesc Represents a ProposalsDiscussionPostMetadata.
+     * @implements IProposalsDiscussionPostMetadata
+     * @constructor
+     * @param {IProposalsDiscussionPostMetadata=} [properties] Properties to set
+     */
+    function ProposalsDiscussionPostMetadata(properties) {
+        if (properties)
+            for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i)
+                if (properties[keys[i]] != null)
+                    this[keys[i]] = properties[keys[i]];
+    }
+
+    /**
+     * ProposalsDiscussionPostMetadata text.
+     * @member {string} text
+     * @memberof ProposalsDiscussionPostMetadata
+     * @instance
+     */
+    ProposalsDiscussionPostMetadata.prototype.text = "";
+
+    /**
+     * ProposalsDiscussionPostMetadata repliesTo.
+     * @member {number} repliesTo
+     * @memberof ProposalsDiscussionPostMetadata
+     * @instance
+     */
+    ProposalsDiscussionPostMetadata.prototype.repliesTo = 0;
+
+    /**
+     * Creates a new ProposalsDiscussionPostMetadata instance using the specified properties.
+     * @function create
+     * @memberof ProposalsDiscussionPostMetadata
+     * @static
+     * @param {IProposalsDiscussionPostMetadata=} [properties] Properties to set
+     * @returns {ProposalsDiscussionPostMetadata} ProposalsDiscussionPostMetadata instance
+     */
+    ProposalsDiscussionPostMetadata.create = function create(properties) {
+        return new ProposalsDiscussionPostMetadata(properties);
+    };
+
+    /**
+     * Encodes the specified ProposalsDiscussionPostMetadata message. Does not implicitly {@link ProposalsDiscussionPostMetadata.verify|verify} messages.
+     * @function encode
+     * @memberof ProposalsDiscussionPostMetadata
+     * @static
+     * @param {IProposalsDiscussionPostMetadata} message ProposalsDiscussionPostMetadata message or plain object to encode
+     * @param {$protobuf.Writer} [writer] Writer to encode to
+     * @returns {$protobuf.Writer} Writer
+     */
+    ProposalsDiscussionPostMetadata.encode = function encode(message, writer) {
+        if (!writer)
+            writer = $Writer.create();
+        if (message.text != null && Object.hasOwnProperty.call(message, "text"))
+            writer.uint32(/* id 1, wireType 2 =*/10).string(message.text);
+        if (message.repliesTo != null && Object.hasOwnProperty.call(message, "repliesTo"))
+            writer.uint32(/* id 2, wireType 0 =*/16).uint32(message.repliesTo);
+        return writer;
+    };
+
+    /**
+     * Encodes the specified ProposalsDiscussionPostMetadata message, length delimited. Does not implicitly {@link ProposalsDiscussionPostMetadata.verify|verify} messages.
+     * @function encodeDelimited
+     * @memberof ProposalsDiscussionPostMetadata
+     * @static
+     * @param {IProposalsDiscussionPostMetadata} message ProposalsDiscussionPostMetadata message or plain object to encode
+     * @param {$protobuf.Writer} [writer] Writer to encode to
+     * @returns {$protobuf.Writer} Writer
+     */
+    ProposalsDiscussionPostMetadata.encodeDelimited = function encodeDelimited(message, writer) {
+        return this.encode(message, writer).ldelim();
+    };
+
+    /**
+     * Decodes a ProposalsDiscussionPostMetadata message from the specified reader or buffer.
+     * @function decode
+     * @memberof ProposalsDiscussionPostMetadata
+     * @static
+     * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from
+     * @param {number} [length] Message length if known beforehand
+     * @returns {ProposalsDiscussionPostMetadata} ProposalsDiscussionPostMetadata
+     * @throws {Error} If the payload is not a reader or valid buffer
+     * @throws {$protobuf.util.ProtocolError} If required fields are missing
+     */
+    ProposalsDiscussionPostMetadata.decode = function decode(reader, length) {
+        if (!(reader instanceof $Reader))
+            reader = $Reader.create(reader);
+        var end = length === undefined ? reader.len : reader.pos + length, message = new $root.ProposalsDiscussionPostMetadata();
+        while (reader.pos < end) {
+            var tag = reader.uint32();
+            switch (tag >>> 3) {
+            case 1:
+                message.text = reader.string();
+                break;
+            case 2:
+                message.repliesTo = reader.uint32();
+                break;
+            default:
+                reader.skipType(tag & 7);
+                break;
+            }
+        }
+        return message;
+    };
+
+    /**
+     * Decodes a ProposalsDiscussionPostMetadata message from the specified reader or buffer, length delimited.
+     * @function decodeDelimited
+     * @memberof ProposalsDiscussionPostMetadata
+     * @static
+     * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from
+     * @returns {ProposalsDiscussionPostMetadata} ProposalsDiscussionPostMetadata
+     * @throws {Error} If the payload is not a reader or valid buffer
+     * @throws {$protobuf.util.ProtocolError} If required fields are missing
+     */
+    ProposalsDiscussionPostMetadata.decodeDelimited = function decodeDelimited(reader) {
+        if (!(reader instanceof $Reader))
+            reader = new $Reader(reader);
+        return this.decode(reader, reader.uint32());
+    };
+
+    /**
+     * Verifies a ProposalsDiscussionPostMetadata message.
+     * @function verify
+     * @memberof ProposalsDiscussionPostMetadata
+     * @static
+     * @param {Object.<string,*>} message Plain object to verify
+     * @returns {string|null} `null` if valid, otherwise the reason why it is not
+     */
+    ProposalsDiscussionPostMetadata.verify = function verify(message) {
+        if (typeof message !== "object" || message === null)
+            return "object expected";
+        if (message.text != null && message.hasOwnProperty("text"))
+            if (!$util.isString(message.text))
+                return "text: string expected";
+        if (message.repliesTo != null && message.hasOwnProperty("repliesTo"))
+            if (!$util.isInteger(message.repliesTo))
+                return "repliesTo: integer expected";
+        return null;
+    };
+
+    /**
+     * Creates a ProposalsDiscussionPostMetadata message from a plain object. Also converts values to their respective internal types.
+     * @function fromObject
+     * @memberof ProposalsDiscussionPostMetadata
+     * @static
+     * @param {Object.<string,*>} object Plain object
+     * @returns {ProposalsDiscussionPostMetadata} ProposalsDiscussionPostMetadata
+     */
+    ProposalsDiscussionPostMetadata.fromObject = function fromObject(object) {
+        if (object instanceof $root.ProposalsDiscussionPostMetadata)
+            return object;
+        var message = new $root.ProposalsDiscussionPostMetadata();
+        if (object.text != null)
+            message.text = String(object.text);
+        if (object.repliesTo != null)
+            message.repliesTo = object.repliesTo >>> 0;
+        return message;
+    };
+
+    /**
+     * Creates a plain object from a ProposalsDiscussionPostMetadata message. Also converts values to other types if specified.
+     * @function toObject
+     * @memberof ProposalsDiscussionPostMetadata
+     * @static
+     * @param {ProposalsDiscussionPostMetadata} message ProposalsDiscussionPostMetadata
+     * @param {$protobuf.IConversionOptions} [options] Conversion options
+     * @returns {Object.<string,*>} Plain object
+     */
+    ProposalsDiscussionPostMetadata.toObject = function toObject(message, options) {
+        if (!options)
+            options = {};
+        var object = {};
+        if (options.defaults) {
+            object.text = "";
+            object.repliesTo = 0;
+        }
+        if (message.text != null && message.hasOwnProperty("text"))
+            object.text = message.text;
+        if (message.repliesTo != null && message.hasOwnProperty("repliesTo"))
+            object.repliesTo = message.repliesTo;
+        return object;
+    };
+
+    /**
+     * Converts this ProposalsDiscussionPostMetadata to JSON.
+     * @function toJSON
+     * @memberof ProposalsDiscussionPostMetadata
+     * @instance
+     * @returns {Object.<string,*>} JSON object
+     */
+    ProposalsDiscussionPostMetadata.prototype.toJSON = function toJSON() {
+        return this.constructor.toObject(this, $protobuf.util.toJSONOptions);
+    };
+
+    return ProposalsDiscussionPostMetadata;
+})();
+
 $root.SeriesMetadata = (function() {
 
     /**

+ 17 - 0
metadata-protobuf/doc/index.md

@@ -13,6 +13,7 @@
 - [proto/Forum.proto](#proto/Forum.proto)
     - [ForumPostMetadata](#.ForumPostMetadata)
     - [ForumPostReaction](#.ForumPostReaction)
+    - [ForumThreadMetadata](#.ForumThreadMetadata)
   
     - [ForumPostReaction.Reaction](#.ForumPostReaction.Reaction)
   
@@ -171,6 +172,22 @@ The enum must be wrapped inside &#34;message&#34;, otherwide it breaks protobufj
 
 
 
+
+<a name=".ForumThreadMetadata"></a>
+
+### ForumThreadMetadata
+
+
+
+| Field | Type | Label | Description |
+| ----- | ---- | ----- | ----------- |
+| title | [string](#string) | optional | Thread title |
+| tags | [string](#string) | repeated | Tags accociated with the thread. Any update overrides all current tags. Only the first {MAX_TAGS_PER_FORUM_THREAD} (const exposed via @joystream/metadata-protobuf/consts) tags are taken into account. In order to unset current tags, [&#39;&#39;] (array with empty string) must be provided as value. |
+
+
+
+
+
  
 
 

+ 3 - 2
metadata-protobuf/package.json

@@ -7,7 +7,8 @@
   "exports": {
     ".": "./lib/index.js",
     "./utils": "./lib/utils.js",
-    "./licenses": "./lib/licenses.js"
+    "./licenses": "./lib/licenses.js",
+    "./consts": "./lib/consts.js"
   },
   "typesVersions": {
     "*": {
@@ -48,7 +49,7 @@
     "mocha": "^8.2.1",
     "prettier": "2.0.2",
     "ts-node": "^8.8.1",
-    "typescript": "^4.1.3",
+    "typescript": "^4.3.5",
     "protobufjs": "^6.11.2"
   }
 }

+ 12 - 0
metadata-protobuf/proto/Forum.proto

@@ -12,3 +12,15 @@ message ForumPostMetadata {
   optional string text = 1; // Post text content (md-formatted)
   optional uint32 repliesTo = 2; // Id of the post that given post replies to (if any)
 }
+
+
+message ForumThreadMetadata {
+  optional string title = 1; // Thread title
+  /**
+   * Tags accociated with the thread.
+   * Any update overrides all current tags.
+   * Only the first {MAX_TAGS_PER_FORUM_THREAD} (const exposed via @joystream/metadata-protobuf/consts) tags are taken into account.
+   * In order to unset current tags, [''] (array with empty string) must be provided as value.
+   */
+  repeated string tags = 2;
+}

+ 6 - 0
metadata-protobuf/proto/ProposalsDiscussion.proto

@@ -0,0 +1,6 @@
+syntax = "proto2";
+
+message ProposalsDiscussionPostMetadata {
+  optional string text = 1; // Post text content (md-formatted)
+  optional uint32 repliesTo = 2; // Id of the post that given post replies to (if any)
+}

+ 1 - 0
metadata-protobuf/src/consts.ts

@@ -0,0 +1 @@
+export const MAX_TAGS_PER_FORUM_THREAD = 5

+ 13 - 0
metadata-protobuf/test/forum-tags.ts

@@ -0,0 +1,13 @@
+import { ForumThreadMetadata } from '../src'
+import { assert } from 'chai'
+import { encodeDecode, metaToObject } from '../src/utils'
+
+describe('Forum tags', () => {
+  it('Skip vs unsetting', () => {
+    const messageSkip = new ForumThreadMetadata()
+    const messageUnset = new ForumThreadMetadata({ tags: [''] })
+
+    assert.equal(metaToObject(ForumThreadMetadata, messageSkip).tags, undefined)
+    assert.deepEqual(encodeDecode(ForumThreadMetadata, messageUnset).tags, [''])
+  })
+})

+ 9 - 12
package.json

@@ -20,8 +20,6 @@
     "storage-node/packages/*",
     "devops/eslint-config",
     "devops/prettier-config",
-    "pioneer",
-    "pioneer/packages/*",
     "utils/api-scripts",
     "query-node",
     "query-node/mappings",
@@ -29,18 +27,17 @@
     "metadata-protobuf"
   ],
   "resolutions": {
-    "@polkadot/api": "4.2.1",
-    "@polkadot/api-contract": "4.2.1",
-    "@polkadot/keyring": "^6.0.5",
-    "@polkadot/metadata": "4.2.1",
-    "@polkadot/types": "4.2.1",
-    "@polkadot/util": "^6.0.5",
-    "@polkadot/util-crypto": "^6.0.5",
-    "@polkadot/wasm-crypto": "^4.0.2",
+    "@polkadot/api": "5.3.2",
+    "@polkadot/api-contract": "5.3.2",
+    "@polkadot/types": "5.3.2",
+    "@polkadot/keyring": "^7.1.1",
+    "@polkadot/util": "^7.1.1",
+    "@polkadot/util-crypto": "^7.1.1",
+    "@polkadot/wasm-crypto": "^4.1.2",
     "babel-core": "^7.0.0-bridge.0",
-    "typescript": "3.8.*",
+    "typescript": "^4.3.5",
     "bn.js": "^5.1.2",
-    "rxjs": "^6.6.2",
+    "rxjs": "^7.2.0",
     "typeorm": "^0.2.31",
     "pg": "^8.4.0",
     "chalk": "^4.0.0"

+ 2 - 3
pioneer/packages/joy-election/src/VoteForm.tsx

@@ -1,5 +1,5 @@
 import BN from 'bn.js';
-import uuid from 'uuid/v4';
+import { randomAsHex } from '@polkadot/util-crypto';
 
 import React from 'react';
 import { Message, Table } from 'semantic-ui-react';
@@ -25,9 +25,8 @@ import { saveVote, NewVote } from './myVotesStore';
 import { TxFailedCallback } from '@polkadot/react-components/Status/types';
 import { RouteProps } from 'react-router-dom';
 
-// TODO use a crypto-prooven generator instead of UUID 4.
 function randomSalt () {
-  return uuid().replace(/-/g, '');
+  return randomAsHex();
 }
 
 // AppsProps is needed to get a location from the route.

+ 1 - 1
pioneer/packages/react-api/package.json

@@ -31,7 +31,7 @@
   "homepage": "https://github.com/polkadot-js/ui/tree/master/packages/ui-reactive#readme",
   "dependencies": {
     "@babel/runtime": "^7.10.5",
-    "@polkadot/api": "4.2.1",
+    "@polkadot/api": "5.3.2",
     "@polkadot/extension-dapp": "^0.32.0-beta.10",
     "rxjs-compat": "^6.6.0"
   }

+ 5 - 11
query-node/manifest.yml

@@ -51,7 +51,6 @@ typegen:
     - storageWorkingGroup.NewMissedRewardLevelReached
     # Proposals
     - proposalsCodex.ProposalCreated
-    - proposalsEngine.ProposalCreated
     - proposalsEngine.ProposalStatusUpdated
     - proposalsEngine.ProposalDecisionMade
     - proposalsEngine.ProposalExecuted
@@ -69,8 +68,7 @@ typegen:
     - forum.CategoryDeleted
     - forum.ThreadCreated
     - forum.ThreadModerated
-    # - forum.ThreadUpdated FIXME: Not emitted by the runtime
-    - forum.ThreadTitleUpdated
+    - forum.ThreadMetadataUpdated
     - forum.ThreadDeleted
     - forum.ThreadMoved
     - forum.VoteOnPoll
@@ -136,10 +134,6 @@ typegen:
     - referendum.VoteRevealed
     - referendum.StakeReleased
   calls:
-    # Memberships
-    - members.updateProfile
-    - members.updateAccounts
-    - forum.createThread
     # Content directory
     - content.create_curator_group
     - content.set_curator_group_status
@@ -172,6 +166,8 @@ typegen:
     - data_directory.remove_content
     - data_directory.accept_content
     - data_directory.update_content_uploading_status
+    # Proposals discussion
+    - proposalsDiscussion.addPost
   outDir: ./mappings/generated/types
   customTypes:
     lib: '@joystream/types/augment/all/types'
@@ -486,8 +482,6 @@ mappings:
     # Proposals
     - event: proposalsCodex.ProposalCreated
       handler: proposalsCodex_ProposalCreated
-    - event: proposalsEngine.ProposalCreated
-      handler: proposalsEngine_ProposalCreated
     - event: proposalsEngine.ProposalStatusUpdated
       handler: proposalsEngine_ProposalStatusUpdated
     - event: proposalsEngine.ProposalDecisionMade
@@ -520,8 +514,8 @@ mappings:
       handler: forum_ThreadCreated
     - event: forum.ThreadModerated
       handler: forum_ThreadModerated
-    - event: forum.ThreadTitleUpdated
-      handler: forum_ThreadTitleUpdated
+    - event: forum.ThreadMetadataUpdated
+      handler: forum_ThreadMetadataUpdated
     - event: forum.ThreadDeleted
       handler: forum_ThreadDeleted
     - event: forum.ThreadMoved

+ 8 - 1
query-node/mappings/common.ts

@@ -7,7 +7,7 @@ import {
   StoreContext,
 } from '@dzlzv/hydra-common'
 import { Bytes } from '@polkadot/types'
-import { WorkingGroup, WorkerId, ContentParameters } from '@joystream/types/augment/all'
+import { WorkingGroup, WorkerId, ThreadId, ContentParameters } from '@joystream/types/augment/all'
 import { Worker, Event, Network, DataObject, LiaisonJudgement, DataObjectOwner } from 'query-node/dist/model'
 import { BaseModel } from 'warthog'
 import { ContentParameters as Custom_ContentParameters } from '@joystream/types/storage'
@@ -203,6 +203,13 @@ export function extractSudoCallParameters<DataParams>(rawEvent: SubstrateEvent):
   return callArgs
 }
 
+// FIXME:
+type MappingsMemoryCache = {
+  lastCreatedProposalThreadId?: ThreadId
+}
+
+export const MemoryCache: MappingsMemoryCache = {}
+
 export function genericEventFields(substrateEvent: SubstrateEvent): Partial<BaseModel & Event> {
   const { blockNumber, indexInBlock, extrinsic, blockTimestamp } = substrateEvent
   const eventTime = new Date(blockTimestamp)

+ 172 - 36
query-node/mappings/forum.ts

@@ -2,7 +2,14 @@
 eslint-disable @typescript-eslint/naming-convention
 */
 import { EventContext, StoreContext, DatabaseManager } from '@dzlzv/hydra-common'
-import { bytesToString, deserializeMetadata, genericEventFields, getWorker } from './common'
+import {
+  bytesToString,
+  deserializeMetadata,
+  genericEventFields,
+  getWorker,
+  inconsistentState,
+  perpareString,
+} from './common'
 import {
   CategoryCreatedEvent,
   CategoryStatusActive,
@@ -20,7 +27,7 @@ import {
   ForumPollAlternative,
   ThreadModeratedEvent,
   ThreadStatusModerated,
-  ThreadTitleUpdatedEvent,
+  ThreadMetadataUpdatedEvent,
   ThreadDeletedEvent,
   ThreadStatusLocked,
   ThreadStatusRemoved,
@@ -46,11 +53,20 @@ import {
   PostTextUpdatedEvent,
   PostDeletedEvent,
   PostStatusRemoved,
+  ForumThreadTag,
 } from 'query-node/dist/model'
 import { Forum } from './generated/types'
 import { PostReactionId, PrivilegedActor } from '@joystream/types/augment/all'
-import { ForumPostMetadata, ForumPostReaction as SupportedPostReactions } from '@joystream/metadata-protobuf'
+import {
+  ForumPostMetadata,
+  ForumPostReaction as SupportedPostReactions,
+  ForumThreadMetadata,
+} from '@joystream/metadata-protobuf'
+import { isSet } from '@joystream/metadata-protobuf/utils'
+import { MAX_TAGS_PER_FORUM_THREAD } from '@joystream/metadata-protobuf/consts'
 import { Not, In } from 'typeorm'
+import { Bytes } from '@polkadot/types'
+import _ from 'lodash'
 
 async function getCategory(store: DatabaseManager, categoryId: string, relations?: string[]): Promise<ForumCategory> {
   const category = await store.get(ForumCategory, { where: { id: categoryId }, relations })
@@ -70,8 +86,8 @@ async function getThread(store: DatabaseManager, threadId: string): Promise<Foru
   return thread
 }
 
-async function getPost(store: DatabaseManager, postId: string): Promise<ForumPost> {
-  const post = await store.get(ForumPost, { where: { id: postId } })
+async function getPost(store: DatabaseManager, postId: string, relations?: 'thread'[]): Promise<ForumPost> {
+  const post = await store.get(ForumPost, { where: { id: postId }, relations })
   if (!post) {
     throw new Error(`Forum post not found by id: ${postId.toString()}`)
   }
@@ -108,6 +124,60 @@ async function getActorWorker(store: DatabaseManager, actor: PrivilegedActor): P
   return worker
 }
 
+function normalizeForumTagLabel(label: string): string {
+  // Optionally: normalize to lowercase & ASCII only?
+  return perpareString(label)
+}
+
+function parseThreadMetadata(metaBytes: Bytes) {
+  const meta = deserializeMetadata(ForumThreadMetadata, metaBytes)
+  return {
+    title: meta ? meta.title : bytesToString(metaBytes),
+    tags:
+      meta && isSet(meta.tags)
+        ? _.uniq(meta.tags.slice(0, MAX_TAGS_PER_FORUM_THREAD).map((label) => normalizeForumTagLabel(label))).filter(
+            (v) => v // Filter out empty strings
+          )
+        : undefined,
+  }
+}
+
+async function prepareThreadTagsToSet(
+  { event, store }: StoreContext & EventContext,
+  labels: string[]
+): Promise<ForumThreadTag[]> {
+  const eventTime = new Date(event.blockTimestamp)
+  return Promise.all(
+    labels.map(async (label) => {
+      const forumTag =
+        (await store.get(ForumThreadTag, { where: { id: label } })) ||
+        new ForumThreadTag({
+          id: label,
+          createdAt: eventTime,
+          visibleThreadsCount: 0,
+        })
+      forumTag.updatedAt = eventTime
+      ++forumTag.visibleThreadsCount
+      await store.save<ForumThreadTag>(forumTag)
+      return forumTag
+    })
+  )
+}
+
+async function unsetThreadTags({ event, store }: StoreContext & EventContext, tags: ForumThreadTag[]): Promise<void> {
+  const eventTime = new Date(event.blockTimestamp)
+  await Promise.all(
+    tags.map(async (forumTag) => {
+      --forumTag.visibleThreadsCount
+      if (forumTag.visibleThreadsCount < 0) {
+        inconsistentState('Trying to update forumTag.visibleThreadsCount to a number below 0!')
+      }
+      forumTag.updatedAt = eventTime
+      await store.save<ForumThreadTag>(forumTag)
+    })
+  )
+}
+
 // Get standarized PostReactionResult by PostReactionId
 function parseReaction(reactionId: PostReactionId): typeof PostReactionResult {
   switch (reactionId.toNumber()) {
@@ -201,11 +271,21 @@ export async function forum_CategoryDeleted({ event, store }: EventContext & Sto
   await store.save<ForumCategory>(category)
 }
 
-export async function forum_ThreadCreated({ event, store }: EventContext & StoreContext): Promise<void> {
-  const { forumUserId, categoryId, title, text, poll } = new Forum.CreateThreadCall(event).args
-  const [threadId] = new Forum.ThreadCreatedEvent(event).params
+export async function forum_ThreadCreated(ctx: EventContext & StoreContext): Promise<void> {
+  const { event, store } = ctx
+  const [
+    categoryId,
+    threadId,
+    postId,
+    memberId,
+    threadMetaBytes,
+    postTextBytes,
+    pollInput,
+  ] = new Forum.ThreadCreatedEvent(event).params
   const eventTime = new Date(event.blockTimestamp)
-  const author = new Membership({ id: forumUserId.toString() })
+  const author = new Membership({ id: memberId.toString() })
+
+  const { title, tags } = parseThreadMetadata(threadMetaBytes)
 
   const thread = new ForumThread({
     createdAt: eventTime,
@@ -213,28 +293,31 @@ export async function forum_ThreadCreated({ event, store }: EventContext & Store
     id: threadId.toString(),
     author,
     category: new ForumCategory({ id: categoryId.toString() }),
-    title: bytesToString(title),
+    title: title || '',
     isSticky: false,
     status: new ThreadStatusActive(),
+    isVisible: true,
+    visiblePostsCount: 1,
+    tags: tags ? await prepareThreadTagsToSet(ctx, tags) : [],
   })
   await store.save<ForumThread>(thread)
 
-  if (poll.isSome) {
+  if (pollInput.isSome) {
     const threadPoll = new ForumPoll({
       createdAt: eventTime,
       updatedAt: eventTime,
-      description: bytesToString(poll.unwrap().description_hash), // FIXME: This should be raw description!
-      endTime: new Date(poll.unwrap().end_time.toNumber()),
+      description: bytesToString(pollInput.unwrap().description),
+      endTime: new Date(pollInput.unwrap().end_time.toNumber()),
       thread,
     })
     await store.save<ForumPoll>(threadPoll)
     await Promise.all(
-      poll.unwrap().poll_alternatives.map(async (alt, index) => {
+      pollInput.unwrap().poll_alternatives.map(async (alt, index) => {
         const alternative = new ForumPollAlternative({
           createdAt: eventTime,
           updatedAt: eventTime,
           poll: threadPoll,
-          text: bytesToString(alt.alternative_text_hash), // FIXME: This should be raw text!
+          text: bytesToString(alt),
           index,
         })
 
@@ -246,8 +329,8 @@ export async function forum_ThreadCreated({ event, store }: EventContext & Store
   const threadCreatedEvent = new ThreadCreatedEvent({
     ...genericEventFields(event),
     thread,
-    title: bytesToString(title),
-    text: bytesToString(text),
+    title: title || '',
+    text: bytesToString(postTextBytes),
   })
   await store.save<ThreadCreatedEvent>(threadCreatedEvent)
 
@@ -255,19 +338,24 @@ export async function forum_ThreadCreated({ event, store }: EventContext & Store
   postOrigin.threadCreatedEventId = threadCreatedEvent.id
 
   const initialPost = new ForumPost({
-    // FIXME: The postId is unknown
+    id: postId.toString(),
     createdAt: eventTime,
     updatedAt: eventTime,
     author,
     thread,
-    text: bytesToString(text),
+    text: bytesToString(postTextBytes),
     status: new PostStatusActive(),
+    isVisible: true,
     origin: postOrigin,
   })
   await store.save<ForumPost>(initialPost)
+
+  thread.initialPost = initialPost
+  await store.save<ForumThread>(thread)
 }
 
-export async function forum_ThreadModerated({ event, store }: EventContext & StoreContext): Promise<void> {
+export async function forum_ThreadModerated(ctx: EventContext & StoreContext): Promise<void> {
+  const { event, store } = ctx
   const [threadId, rationaleBytes, privilegedActor] = new Forum.ThreadModeratedEvent(event).params
   const eventTime = new Date(event.blockTimestamp)
   const actorWorker = await getActorWorker(store, privilegedActor)
@@ -287,28 +375,51 @@ export async function forum_ThreadModerated({ event, store }: EventContext & Sto
 
   thread.updatedAt = eventTime
   thread.status = newStatus
+  thread.isVisible = false
+  thread.visiblePostsCount = 0
+  await unsetThreadTags(ctx, thread.tags || [])
   await store.save<ForumThread>(thread)
 }
 
-export async function forum_ThreadTitleUpdated({ event, store }: EventContext & StoreContext): Promise<void> {
-  const [threadId, , , newTitleBytes] = new Forum.ThreadTitleUpdatedEvent(event).params
+export async function forum_ThreadMetadataUpdated(ctx: EventContext & StoreContext): Promise<void> {
+  const { event, store } = ctx
+  const [threadId, , , newMetadataBytes] = new Forum.ThreadMetadataUpdatedEvent(event).params
   const eventTime = new Date(event.blockTimestamp)
   const thread = await getThread(store, threadId.toString())
 
-  const threadTitleUpdatedEvent = new ThreadTitleUpdatedEvent({
-    ...genericEventFields(event),
-    thread,
-    newTitle: bytesToString(newTitleBytes),
-  })
+  const { title: newTitle, tags: newTagIds } = parseThreadMetadata(newMetadataBytes)
+
+  // Only update tags if set
+  if (isSet(newTagIds)) {
+    const currentTagIds = (thread.tags || []).map((t) => t.id)
+    const tagIdsToSet = _.difference(newTagIds, currentTagIds)
+    const tagIdsToUnset = _.difference(currentTagIds, newTagIds)
+    const newTags = await prepareThreadTagsToSet(ctx, tagIdsToSet)
+    await unsetThreadTags(
+      ctx,
+      (thread.tags || []).filter((t) => tagIdsToUnset.includes(t.id))
+    )
+    thread.tags = newTags
+  }
 
-  await store.save<ThreadTitleUpdatedEvent>(threadTitleUpdatedEvent)
+  if (isSet(newTitle)) {
+    thread.title = newTitle
+  }
 
   thread.updatedAt = eventTime
-  thread.title = bytesToString(newTitleBytes)
   await store.save<ForumThread>(thread)
+
+  const threadMetadataUpdatedEvent = new ThreadMetadataUpdatedEvent({
+    ...genericEventFields(event),
+    thread,
+    newTitle: newTitle || undefined,
+  })
+
+  await store.save<ThreadMetadataUpdatedEvent>(threadMetadataUpdatedEvent)
 }
 
-export async function forum_ThreadDeleted({ event, store }: EventContext & StoreContext): Promise<void> {
+export async function forum_ThreadDeleted(ctx: EventContext & StoreContext): Promise<void> {
+  const { event, store } = ctx
   const [threadId, , , hide] = new Forum.ThreadDeletedEvent(event).params
   const eventTime = new Date(event.blockTimestamp)
   const thread = await getThread(store, threadId.toString())
@@ -320,10 +431,15 @@ export async function forum_ThreadDeleted({ event, store }: EventContext & Store
 
   await store.save<ThreadDeletedEvent>(threadDeletedEvent)
 
-  const status = hide.valueOf() ? new ThreadStatusRemoved() : new ThreadStatusLocked()
+  const status = hide.isTrue ? new ThreadStatusRemoved() : new ThreadStatusLocked()
   status.threadDeletedEventId = threadDeletedEvent.id
   thread.status = status
   thread.updatedAt = eventTime
+  if (hide.isTrue) {
+    thread.isVisible = false
+    thread.visiblePostsCount = 0
+    await unsetThreadTags(ctx, thread.tags || [])
+  }
   await store.save<ForumThread>(thread)
 }
 
@@ -366,6 +482,7 @@ export async function forum_PostAdded({ event, store }: EventContext & StoreCont
   const [postId, forumUserId, , threadId, metadataBytes, isEditable] = new Forum.PostAddedEvent(event).params
   const eventTime = new Date(event.blockTimestamp)
 
+  const thread = await getThread(store, threadId.toString())
   const metadata = deserializeMetadata(ForumPostMetadata, metadataBytes)
   const postText = metadata ? metadata.text || '' : bytesToString(metadataBytes)
   const repliesToPost =
@@ -380,8 +497,9 @@ export async function forum_PostAdded({ event, store }: EventContext & StoreCont
     createdAt: eventTime,
     updatedAt: eventTime,
     text: postText,
-    thread: new ForumThread({ id: threadId.toString() }),
+    thread,
     status: postStatus,
+    isVisible: true,
     author: new Membership({ id: forumUserId.toString() }),
     origin: postOrigin,
     repliesTo: repliesToPost || undefined,
@@ -399,6 +517,10 @@ export async function forum_PostAdded({ event, store }: EventContext & StoreCont
   // Update the other side of cross-relationship
   postOrigin.postAddedEventId = postAddedEvent.id
   await store.save<ForumPost>(post)
+
+  ++thread.visiblePostsCount
+  thread.updatedAt = eventTime
+  await store.save<ForumThread>(thread)
 }
 
 export async function forum_CategoryStickyThreadUpdate({ event, store }: EventContext & StoreContext): Promise<void> {
@@ -469,7 +591,7 @@ export async function forum_PostModerated({ event, store }: EventContext & Store
   const [postId, rationaleBytes, privilegedActor] = new Forum.PostModeratedEvent(event).params
   const eventTime = new Date(event.blockTimestamp)
   const actorWorker = await getActorWorker(store, privilegedActor)
-  const post = await getPost(store, postId.toString())
+  const post = await getPost(store, postId.toString(), ['thread'])
 
   const postModeratedEvent = new PostModeratedEvent({
     ...genericEventFields(event),
@@ -485,7 +607,13 @@ export async function forum_PostModerated({ event, store }: EventContext & Store
 
   post.updatedAt = eventTime
   post.status = newStatus
+  post.isVisible = false
   await store.save<ForumPost>(post)
+
+  const { thread } = post
+  --thread.visiblePostsCount
+  thread.updatedAt = eventTime
+  await store.save<ForumThread>(thread)
 }
 
 export async function forum_PostReacted({ event, store }: EventContext & StoreContext): Promise<void> {
@@ -558,14 +686,22 @@ export async function forum_PostDeleted({ event, store }: EventContext & StoreCo
   await store.save<PostDeletedEvent>(postDeletedEvent)
 
   await Promise.all(
-    postsData.map(async ([, , postId, hideFlag]) => {
-      const post = await getPost(store, postId.toString())
-      const newStatus = hideFlag.valueOf() ? new PostStatusRemoved() : new PostStatusLocked()
+    Array.from(postsData.entries()).map(async ([{ post_id: postId }, hideFlag]) => {
+      const post = await getPost(store, postId.toString(), ['thread'])
+      const newStatus = hideFlag.isTrue ? new PostStatusRemoved() : new PostStatusLocked()
       newStatus.postDeletedEventId = postDeletedEvent.id
       post.updatedAt = eventTime
       post.status = newStatus
       post.deletedInEvent = postDeletedEvent
+      post.isVisible = hideFlag.isFalse
       await store.save<ForumPost>(post)
+
+      if (hideFlag.isTrue) {
+        const { thread } = post
+        --thread.visiblePostsCount
+        thread.updatedAt = eventTime
+        await store.save<ForumThread>(thread)
+      }
     })
   )
 }

+ 5 - 9
query-node/mappings/membership.ts

@@ -141,11 +141,8 @@ export async function members_MembershipBought({ store, event }: EventContext &
 }
 
 export async function members_MemberProfileUpdated({ store, event }: EventContext & StoreContext): Promise<void> {
-  const [memberId] = new Members.MemberProfileUpdatedEvent(event).params
-  const { metadata: metadataBytesOpt, handle } = new Members.UpdateProfileCall(event).args
-  const metadata = metadataBytesOpt.isSome
-    ? deserializeMetadata(MembershipMetadata, metadataBytesOpt.unwrap())
-    : undefined
+  const [memberId, newHandle, newMetadata] = new Members.MemberProfileUpdatedEvent(event).params
+  const metadata = newMetadata.isSome ? deserializeMetadata(MembershipMetadata, newMetadata.unwrap()) : undefined
   const member = await getMemberById(store, memberId)
   const eventTime = new Date(event.blockTimestamp)
 
@@ -159,8 +156,8 @@ export async function members_MemberProfileUpdated({ store, event }: EventContex
     member.metadata.updatedAt = eventTime
   }
   // TODO: avatar
-  if (handle.isSome) {
-    member.handle = bytesToString(handle.unwrap())
+  if (newHandle.isSome) {
+    member.handle = bytesToString(newHandle.unwrap())
     member.updatedAt = eventTime
   }
 
@@ -182,8 +179,7 @@ export async function members_MemberProfileUpdated({ store, event }: EventContex
 }
 
 export async function members_MemberAccountsUpdated({ store, event }: EventContext & StoreContext): Promise<void> {
-  const [memberId] = new Members.MemberAccountsUpdatedEvent(event).params
-  const { newRootAccount, newControllerAccount } = new Members.UpdateAccountsCall(event).args
+  const [memberId, newRootAccount, newControllerAccount] = new Members.MemberAccountsUpdatedEvent(event).params
   const member = await getMemberById(store, memberId)
   const eventTime = new Date(event.blockTimestamp)
 

+ 1 - 1
query-node/mappings/package.json

@@ -19,6 +19,6 @@
   },
   "devDependencies": {
     "ts-node": "^9.0.0",
-    "typescript": "^3.8"
+    "typescript": "^4.3.5"
   }
 }

+ 24 - 21
query-node/mappings/proposals.ts

@@ -2,7 +2,7 @@
 eslint-disable @typescript-eslint/naming-convention
 */
 import { SubstrateEvent, DatabaseManager, EventContext, StoreContext } from '@dzlzv/hydra-common'
-import { ProposalDetails as RuntimeProposalDetails, ProposalId } from '@joystream/types/augment/all'
+import { ProposalDetails as RuntimeProposalDetails } from '@joystream/types/augment/all'
 import BN from 'bn.js'
 import {
   Proposal,
@@ -57,21 +57,15 @@ import {
   ProposalCancelledEvent,
   ProposalCreatedEvent,
   RuntimeWasmBytecode,
+  ProposalDiscussionThread,
+  ProposalDiscussionThreadModeOpen,
 } from 'query-node/dist/model'
-import { bytesToString, genericEventFields, getWorkingGroupModuleName, perpareString } from './common'
+import { bytesToString, genericEventFields, getWorkingGroupModuleName, MemoryCache, perpareString } from './common'
 import { ProposalsEngine, ProposalsCodex } from './generated/types'
 import { createWorkingGroupOpeningMetadata } from './workingGroups'
 import { blake2AsHex } from '@polkadot/util-crypto'
 import { Bytes } from '@polkadot/types'
 
-// FIXME: https://github.com/Joystream/joystream/issues/2457
-type ProposalsMappingsMemoryCache = {
-  lastCreatedProposalId: ProposalId | null
-}
-const proposalsMappingsMemoryCache: ProposalsMappingsMemoryCache = {
-  lastCreatedProposalId: null,
-}
-
 async function getProposal(store: DatabaseManager, id: string) {
   const proposal = await store.get(Proposal, { where: { id } })
   if (!proposal) {
@@ -321,24 +315,19 @@ async function parseProposalDetails(
   }
 }
 
-export async function proposalsEngine_ProposalCreated({ event }: EventContext & StoreContext): Promise<void> {
-  const [, proposalId] = new ProposalsEngine.ProposalCreatedEvent(event).params
-
-  // Cache the id
-  proposalsMappingsMemoryCache.lastCreatedProposalId = proposalId
-}
-
 export async function proposalsCodex_ProposalCreated({ store, event }: EventContext & StoreContext): Promise<void> {
-  const [generalProposalParameters, runtimeProposalDetails] = new ProposalsCodex.ProposalCreatedEvent(event).params
+  const [proposalId, generalProposalParameters, runtimeProposalDetails] = new ProposalsCodex.ProposalCreatedEvent(
+    event
+  ).params
   const eventTime = new Date(event.blockTimestamp)
   const proposalDetails = await parseProposalDetails(event, store, runtimeProposalDetails)
 
-  if (!proposalsMappingsMemoryCache.lastCreatedProposalId) {
-    throw new Error('Unexpected state: proposalsMappingsMemoryCache.lastCreatedProposalId is empty')
+  if (!MemoryCache.lastCreatedProposalThreadId) {
+    throw new Error('Unexpected state: MemoryCache.lastCreatedProposalThreadId is empty')
   }
 
   const proposal = new Proposal({
-    id: proposalsMappingsMemoryCache.lastCreatedProposalId.toString(),
+    id: proposalId.toString(),
     createdAt: eventTime,
     updatedAt: eventTime,
     details: proposalDetails,
@@ -349,11 +338,22 @@ export async function proposalsCodex_ProposalCreated({ store, event }: EventCont
     exactExecutionBlock: generalProposalParameters.exact_execution_block.unwrapOr(undefined)?.toNumber(),
     stakingAccount: generalProposalParameters.staking_account_id.toString(),
     status: new ProposalStatusDeciding(),
+    isFinalized: false,
     statusSetAtBlock: event.blockNumber,
     statusSetAtTime: eventTime,
   })
   await store.save<Proposal>(proposal)
 
+  // Thread is always created along with the proposal
+  const proposalThread = new ProposalDiscussionThread({
+    id: MemoryCache.lastCreatedProposalThreadId.toString(),
+    createdAt: eventTime,
+    updatedAt: eventTime,
+    mode: new ProposalDiscussionThreadModeOpen(),
+    proposal,
+  })
+  await store.save<ProposalDiscussionThread>(proposalThread)
+
   const proposalCreatedEvent = new ProposalCreatedEvent({
     ...genericEventFields(event),
     proposal: proposal,
@@ -453,6 +453,7 @@ export async function proposalsEngine_ProposalDecisionMade({
       | ProposalStatusSlashed
       | ProposalStatusVetoed).proposalDecisionMadeEventId = proposalDecisionMadeEvent.id
     proposal.status = decisionStatus
+    proposal.isFinalized = true
     proposal.statusSetAtBlock = event.blockNumber
     proposal.statusSetAtTime = eventTime
     proposal.updatedAt = eventTime
@@ -485,6 +486,7 @@ export async function proposalsEngine_ProposalExecuted({ store, event }: EventCo
 
   newStatus.proposalExecutedEventId = proposalExecutedEvent.id
   proposal.status = newStatus
+  proposal.isFinalized = true
   proposal.statusSetAtBlock = event.blockNumber
   proposal.statusSetAtTime = eventTime
   proposal.updatedAt = eventTime
@@ -533,6 +535,7 @@ export async function proposalsEngine_ProposalCancelled({ store, event }: EventC
   await store.save<ProposalCancelledEvent>(proposalCancelledEvent)
 
   proposal.status = new ProposalStatusCancelled()
+  proposal.isFinalized = true
   proposal.status.cancelledInEventId = proposalCancelledEvent.id
   proposal.statusSetAtBlock = event.blockNumber
   proposal.statusSetAtTime = eventTime

+ 176 - 14
query-node/mappings/proposalsDiscussion.ts

@@ -1,24 +1,186 @@
 /*
 eslint-disable @typescript-eslint/naming-convention
 */
-import { SubstrateEvent, DatabaseManager } from '@dzlzv/hydra-common'
+import { EventContext, StoreContext, DatabaseManager } from '@dzlzv/hydra-common'
+import {
+  Membership,
+  ProposalDiscussionPostStatusActive,
+  ProposalDiscussionPostStatusLocked,
+  ProposalDiscussionPost,
+  ProposalDiscussionThread,
+  ProposalDiscussionPostCreatedEvent,
+  ProposalDiscussionPostUpdatedEvent,
+  ProposalDiscussionThreadModeClosed,
+  ProposalDiscussionWhitelist,
+  ProposalDiscussionThreadModeOpen,
+  ProposalDiscussionThreadModeChangedEvent,
+  ProposalDiscussionPostDeletedEvent,
+  ProposalDiscussionPostStatusRemoved,
+} from 'query-node/dist/model'
+import { bytesToString, deserializeMetadata, genericEventFields, MemoryCache } from './common'
+import { ProposalsDiscussion } from './generated/types'
+import { ProposalsDiscussionPostMetadata } from '@joystream/metadata-protobuf'
+import { In } from 'typeorm'
 
-export async function proposalsDiscussion_ThreadCreated(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
-  // TODO
+async function getPost(store: DatabaseManager, id: string) {
+  const post = await store.get(ProposalDiscussionPost, { where: { id } })
+  if (!post) {
+    throw new Error(`Proposal discussion post not found by id: ${id}`)
+  }
+
+  return post
 }
-export async function proposalsDiscussion_PostCreated(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
-  // TODO
+
+async function getThread(store: DatabaseManager, id: string) {
+  const thread = await store.get(ProposalDiscussionThread, { where: { id } })
+  if (!thread) {
+    throw new Error(`Proposal discussion thread not found by id: ${id}`)
+  }
+
+  return thread
 }
-export async function proposalsDiscussion_PostUpdated(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
-  // TODO
+
+export async function proposalsDiscussion_ThreadCreated({ event }: EventContext & StoreContext): Promise<void> {
+  const [threadId] = new ProposalsDiscussion.ThreadCreatedEvent(event).params
+  MemoryCache.lastCreatedProposalThreadId = threadId
 }
-export async function proposalsDiscussion_ThreadModeChanged(
-  db: DatabaseManager,
-  event_: SubstrateEvent
-): Promise<void> {
-  // TODO
+
+export async function proposalsDiscussion_PostCreated({ event, store }: EventContext & StoreContext): Promise<void> {
+  // FIXME: extremely ugly and insecure workaround for `batch` and `sudo` calls support.
+  // Ideally this data would be part of the event data
+  let editable: boolean
+
+  if (!event.extrinsic) {
+    throw new Error('Missing extrinsic for proposalsDiscussion.PostCreated event!')
+  } else if (event.extrinsic.section === 'utility' && event.extrinsic.method === 'batch') {
+    // We cannot use new Utility.BatchCall(event).args, because createTypeUnsafe fails on Call
+    // First (and only) argument of utility.batch is "calls"
+    const calls = event.extrinsic.args[0].value as any[]
+    // proposalsDiscussion.addPost call index is currently 0x1f00
+    const call = calls.find((c) => c.callIndex === '0x1f00')
+    if (!call) {
+      throw new Error('Could not find proposalsDiscussion.addPostCall in a batch!')
+    }
+    editable = call.args.editable
+  } else if (
+    event.extrinsic.section === 'sudo' &&
+    (event.extrinsic.method === 'sudo' || event.extrinsic.method === 'sudoAs')
+  ) {
+    // Extract call arg
+    editable = (event.extrinsic.args[0].value as any).args.editable
+  } else {
+    editable = new ProposalsDiscussion.AddPostCall(event).args.editable.valueOf()
+  }
+
+  const [postId, memberId, threadId, metadataBytes] = new ProposalsDiscussion.PostCreatedEvent(event).params
+  const eventTime = new Date(event.blockTimestamp)
+
+  const metadata = deserializeMetadata(ProposalsDiscussionPostMetadata, metadataBytes)
+
+  const repliesTo =
+    typeof metadata?.repliesTo === 'number'
+      ? await store.get(ProposalDiscussionPost, { where: { id: metadata.repliesTo.toString() } })
+      : undefined
+
+  const text = typeof metadata?.text === 'string' ? metadata.text : bytesToString(metadataBytes)
+
+  const post = new ProposalDiscussionPost({
+    id: postId.toString(),
+    createdAt: eventTime,
+    updatedAt: eventTime,
+    author: new Membership({ id: memberId.toString() }),
+    status: editable ? new ProposalDiscussionPostStatusActive() : new ProposalDiscussionPostStatusLocked(),
+    isVisible: true,
+    text,
+    repliesTo,
+    discussionThread: new ProposalDiscussionThread({ id: threadId.toString() }),
+  })
+  await store.save<ProposalDiscussionPost>(post)
+
+  const postCreatedEvent = new ProposalDiscussionPostCreatedEvent({
+    ...genericEventFields(event),
+    post: post,
+    text,
+  })
+  await store.save<ProposalDiscussionPostCreatedEvent>(postCreatedEvent)
 }
 
-export async function proposalsDiscussion_PostDeleted(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
-  // TODO
+export async function proposalsDiscussion_PostUpdated({ event, store }: EventContext & StoreContext): Promise<void> {
+  const [postId, , , newTextBytes] = new ProposalsDiscussion.PostUpdatedEvent(event).params
+
+  const post = await getPost(store, postId.toString())
+  const newText = bytesToString(newTextBytes)
+
+  post.text = newText
+  post.updatedAt = new Date(event.blockTimestamp)
+  await store.save<ProposalDiscussionPost>(post)
+
+  const postUpdatedEvent = new ProposalDiscussionPostUpdatedEvent({
+    ...genericEventFields(event),
+    post,
+    text: newText,
+  })
+  await store.save<ProposalDiscussionPostUpdatedEvent>(postUpdatedEvent)
+}
+
+export async function proposalsDiscussion_ThreadModeChanged({
+  event,
+  store,
+}: EventContext & StoreContext): Promise<void> {
+  const [threadId, threadMode, memberId] = new ProposalsDiscussion.ThreadModeChangedEvent(event).params
+  const eventTime = new Date(event.blockTimestamp)
+
+  const thread = await getThread(store, threadId.toString())
+
+  if (threadMode.isClosed) {
+    const newMode = new ProposalDiscussionThreadModeClosed()
+    const whitelistMemberIds = threadMode.asClosed
+    const members = await store.getMany(Membership, {
+      where: { id: In(whitelistMemberIds.map((id) => id.toString())) },
+    })
+    const whitelist = new ProposalDiscussionWhitelist({
+      createdAt: eventTime,
+      updatedAt: eventTime,
+      members,
+    })
+    await store.save<ProposalDiscussionWhitelist>(whitelist)
+    newMode.whitelistId = whitelist.id
+    thread.mode = newMode
+  } else if (threadMode.isOpen) {
+    const newMode = new ProposalDiscussionThreadModeOpen()
+    thread.mode = newMode
+  } else {
+    throw new Error(`Unrecognized proposal thread mode: ${threadMode.type}`)
+  }
+
+  thread.updatedAt = eventTime
+  await store.save<ProposalDiscussionThread>(thread)
+
+  const threadModeChangedEvent = new ProposalDiscussionThreadModeChangedEvent({
+    ...genericEventFields(event),
+    actor: new Membership({ id: memberId.toString() }),
+    newMode: thread.mode,
+    thread: thread,
+  })
+  await store.save<ProposalDiscussionThreadModeChangedEvent>(threadModeChangedEvent)
+}
+
+export async function proposalsDiscussion_PostDeleted({ event, store }: EventContext & StoreContext): Promise<void> {
+  const [memberId, , postId, hide] = new ProposalsDiscussion.PostDeletedEvent(event).params
+  const eventTime = new Date(event.blockTimestamp)
+  const post = await getPost(store, postId.toString())
+
+  const postDeletedEvent = new ProposalDiscussionPostDeletedEvent({
+    ...genericEventFields(event),
+    post,
+    actor: new Membership({ id: memberId.toString() }),
+  })
+  await store.save<ProposalDiscussionPostDeletedEvent>(postDeletedEvent)
+
+  const newStatus = hide.isTrue ? new ProposalDiscussionPostStatusRemoved() : new ProposalDiscussionPostStatusLocked()
+  newStatus.deletedInEventId = postDeletedEvent.id
+  post.isVisible = hide.isFalse
+  post.status = newStatus
+  post.updatedAt = eventTime
+  await store.save<ProposalDiscussionPost>(post)
 }

+ 28 - 2
query-node/schemas/forum.graphql

@@ -83,6 +83,12 @@ type ForumThread @entity {
   "All posts in the thread"
   posts: [ForumPost!] @derivedFrom(field: "thread")
 
+  "The intial post created along with the thread"
+  initialPost: ForumPost
+
+  "Number of non-deleted posts in the thread"
+  visiblePostsCount: Int!
+
   "Optional poll associated with the thread"
   poll: ForumPoll @derivedFrom(field: "thread")
 
@@ -95,8 +101,11 @@ type ForumThread @entity {
   "Current thread status"
   status: ThreadStatus!
 
-  "Theread title update events"
-  titleUpdates: [ThreadTitleUpdatedEvent!] @derivedFrom(field: "thread")
+  "True if the thread is either Active or Locked"
+  isVisible: Boolean!
+
+  "Theread metadata update events"
+  metadataUpdates: [ThreadMetadataUpdatedEvent!] @derivedFrom(field: "thread")
 
   # Required to create Many-to-Many relation
   "The events the thred was made sticky in"
@@ -104,6 +113,20 @@ type ForumThread @entity {
 
   "List of events that moved the thread to a different category"
   movedInEvents: [ThreadMovedEvent!] @derivedFrom(field: "thread")
+
+  "Assigned thread tags"
+  tags: [ForumThreadTag!]
+}
+
+type ForumThreadTag @entity {
+  "Tag id (and simultaneously - tag label)"
+  id: ID!
+
+  "Forum threads assigned to the tag"
+  threads: [ForumThread!] @derivedFrom(field: "tags")
+
+  "Number of non-removed threads currently assigned to the tag"
+  visibleThreadsCount: Int!
 }
 
 type ForumPoll @entity {
@@ -212,6 +235,9 @@ type ForumPost @entity {
   "Current post status"
   status: PostStatus!
 
+  "True if the post is either Active or Locked"
+  isVisible: Boolean!
+
   "The origin of the post (either thread creation event or regular PostAdded event)"
   origin: PostOrigin!
 

+ 2 - 17
query-node/schemas/forumEvents.graphql

@@ -143,22 +143,7 @@ type ThreadModeratedEvent @entity {
   actor: Worker!
 }
 
-# FIXME: Not emitted by the runtime
-# type ThreadUpdatedEvent @entity {
-#   "Generic event data"
-#   event: Event!
-
-#   "The thread beeing updated"
-#   thread: ForumThread!
-
-#   "The new archival status of the thread (true = archived)"
-#   newArchivalStatus: Boolean!
-
-#   "Actor responsible for the update"
-#   actor: Worker!
-# }
-
-type ThreadTitleUpdatedEvent @entity {
+type ThreadMetadataUpdatedEvent @entity {
   ### GENERIC DATA ###
 
   "(network}-{blockNumber}-{indexInBlock}"
@@ -182,7 +167,7 @@ type ThreadTitleUpdatedEvent @entity {
   thread: ForumThread!
 
   "New title of the thread"
-  newTitle: String!
+  newTitle: String
 
   # Only author can update the thread title, so no actor information required
 }

+ 8 - 2
query-node/schemas/proposalDiscussion.graphql

@@ -26,10 +26,13 @@ type ProposalDiscussionThread @entity {
   proposal: Proposal!
 
   "List of posts in the the thread"
-  posts: [ProposalDiscussionPost!] @derivedFrom(field: "thread")
+  posts: [ProposalDiscussionPost!] @derivedFrom(field: "discussionThread")
 
   "Current thread mode"
   mode: ProposalDiscussionThreadMode!
+
+  "List of related thread mode change events"
+  modeChanges: [ProposalDiscussionThreadModeChangedEvent!] @derivedFrom(field: "thread")
 }
 
 "The post is visible and editable"
@@ -59,7 +62,7 @@ type ProposalDiscussionPost @entity {
   id: ID!
 
   "Proposal discussion thread the post was created in"
-  thread: ProposalDiscussionThread!
+  discussionThread: ProposalDiscussionThread!
 
   "The author of the post"
   author: Membership!
@@ -67,6 +70,9 @@ type ProposalDiscussionPost @entity {
   "Current post status"
   status: ProposalDiscussionPostStatus!
 
+  "True if the post is either Active or Locked"
+  isVisible: Boolean!
+
   "Post's md-formatted text"
   text: String!
 

+ 0 - 24
query-node/schemas/proposalDiscussionEvents.graphql

@@ -1,27 +1,3 @@
-type ProposalDiscussionThreadCreatedEvent implements Event @entity {
-  ### GENERIC DATA ###
-
-  "(network}-{blockNumber}-{indexInBlock}"
-  id: ID!
-
-  "Hash of the extrinsic which caused the event to be emitted"
-  inExtrinsic: String
-
-  "Blocknumber of the block in which the event was emitted."
-  inBlock: Int!
-
-  "Network the block was produced in"
-  network: Network!
-
-  "Index of event in block from which it was emitted."
-  indexInBlock: Int!
-
-  ### SPECIFIC DATA ###
-
-  "The created thread"
-  thread: ProposalDiscussionThread!
-}
-
 type ProposalDiscussionPostCreatedEvent implements Event @entity {
   ### GENERIC DATA ###
 

+ 3 - 0
query-node/schemas/proposals.graphql

@@ -138,6 +138,9 @@ type Proposal @entity {
   "Current proposal status"
   status: ProposalStatus!
 
+  "If true then the proposal status is final and will not change form this point"
+  isFinalized: Boolean
+
   # Additional fileds to avoid the need for complex filtering through status variant relations:
 
   "Number of the block the current status was set at"

+ 12 - 12
runtime-modules/forum/src/benchmarking.rs

@@ -920,7 +920,7 @@ benchmarks! {
         let (category_id, _) = generate_categories_tree::<T>(caller_id.clone(), i, None);
         let mut category = Module::<T>::category_by_id(category_id);
 
-        let title = vec![0u8].repeat(j as usize);
+        let metadata = vec![0u8].repeat(j as usize);
 
         let text = vec![0u8].repeat(k as usize);
 
@@ -937,7 +937,7 @@ benchmarks! {
         let next_post_id = Module::<T>::next_post_id();
         let initial_balance = Balances::<T>::usable_balance(&caller_id);
 
-    }: _ (RawOrigin::Signed(caller_id.clone()), forum_user_id.saturated_into(), category_id, title.clone(), text.clone(), poll_input.clone())
+    }: _ (RawOrigin::Signed(caller_id.clone()), forum_user_id.saturated_into(), category_id, metadata.clone(), text.clone(), poll_input.clone())
     verify {
 
         assert_eq!(
@@ -961,7 +961,7 @@ benchmarks! {
         // Ensure new thread created successfully
         let new_thread = Thread {
             category_id,
-            title_hash: T::calculate_hash(&title),
+            metadata_hash: T::calculate_hash(&metadata),
             author_id: forum_user_id.saturated_into(),
             poll: poll_input.clone().map(<Module<T>>::from_poll_input),
             cleanup_pay_off: T::ThreadDeposit::get(),
@@ -985,14 +985,14 @@ benchmarks! {
                 next_thread_id,
                 next_post_id,
                 forum_user_id.saturated_into(),
-                title,
+                metadata,
                 text,
                 poll_input,
             ).into()
         );
     }
 
-    edit_thread_title {
+    edit_thread_metadata {
         let forum_user_id = 0;
 
         let caller_id =
@@ -1012,19 +1012,19 @@ benchmarks! {
         );
         let mut thread = Module::<T>::thread_by_id(category_id, thread_id);
 
-        let text = vec![0u8].repeat(j as usize);
+        let new_metadata = vec![0u8].repeat(j as usize);
 
-    }: _ (RawOrigin::Signed(caller_id), forum_user_id.saturated_into(), category_id, thread_id, text.clone())
+    }: _ (RawOrigin::Signed(caller_id), forum_user_id.saturated_into(), category_id, thread_id, new_metadata.clone())
     verify {
-        thread.title_hash = T::calculate_hash(&text);
+        thread.metadata_hash = T::calculate_hash(&new_metadata);
         assert_eq!(Module::<T>::thread_by_id(category_id, thread_id), thread);
 
         assert_last_event::<T>(
-            RawEvent::ThreadTitleUpdated(
+            RawEvent::ThreadMetadataUpdated(
                 thread_id,
                 forum_user_id.saturated_into(),
                 category_id,
-                text
+                new_metadata
             ).into()
         );
     }
@@ -1907,9 +1907,9 @@ mod tests {
     }
 
     #[test]
-    fn test_edit_thread_title() {
+    fn test_edit_thread_metadata() {
         with_test_externalities(|| {
-            assert_ok!(test_benchmark_edit_thread_title::<Runtime>());
+            assert_ok!(test_benchmark_edit_thread_metadata::<Runtime>());
         });
     }
 

+ 23 - 17
runtime-modules/forum/src/lib.rs

@@ -82,7 +82,7 @@ pub trait WeightInfo {
     fn delete_category_lead(i: u32) -> Weight;
     fn delete_category_moderator(i: u32) -> Weight;
     fn create_thread(j: u32, k: u32, i: u32) -> Weight;
-    fn edit_thread_title(i: u32, j: u32) -> Weight;
+    fn edit_thread_metadata(i: u32, j: u32) -> Weight;
     fn delete_thread(i: u32) -> Weight;
     fn move_thread_to_category_lead(i: u32) -> Weight;
     fn move_thread_to_category_moderator(i: u32) -> Weight;
@@ -266,8 +266,8 @@ pub struct Post<ForumUserId, ThreadId, Hash, Balance, BlockNumber> {
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Default, Clone, PartialEq, Debug, Eq)]
 pub struct Thread<ForumUserId, CategoryId, Moment, Hash, Balance> {
-    /// Title hash
-    pub title_hash: Hash,
+    /// Metadata hash
+    pub metadata_hash: Hash,
 
     /// Category in which this thread lives
     pub category_id: CategoryId,
@@ -540,8 +540,8 @@ decl_event!(
         /// The second argument reflects the new archival status of the thread.
         ThreadUpdated(ThreadId, bool, PrivilegedActor, CategoryId),
 
-        /// A thread with given id was moderated.
-        ThreadTitleUpdated(ThreadId, ForumUserId, CategoryId, Vec<u8>),
+        /// A thread metadata given id was updated.
+        ThreadMetadataUpdated(ThreadId, ForumUserId, CategoryId, Vec<u8>),
 
         /// A thread was deleted.
         ThreadDeleted(ThreadId, ForumUserId, CategoryId, bool),
@@ -902,7 +902,7 @@ decl_module! {
         ///    - O(W)
         /// # </weight>
         #[weight = WeightInfoForum::<T>::create_thread(
-            title.len().saturated_into(),
+            metadata.len().saturated_into(),
             text.len().saturated_into(),
             T::MaxCategoryDepth::get() as u32,
         )]
@@ -910,7 +910,7 @@ decl_module! {
             origin,
             forum_user_id: ForumUserId<T>,
             category_id: T::CategoryId,
-            title: Vec<u8>,
+            metadata: Vec<u8>,
             text: Vec<u8>,
             poll_input: Option<PollInput<T::Moment>>,
         ) -> DispatchResult {
@@ -951,7 +951,7 @@ decl_module! {
             // Build a new thread
             let new_thread = Thread {
                 category_id,
-                title_hash: T::calculate_hash(&title),
+                metadata_hash: T::calculate_hash(&metadata),
                 author_id: forum_user_id,
                 poll,
                 cleanup_pay_off: T::ThreadDeposit::get(),
@@ -985,7 +985,7 @@ decl_module! {
                     new_thread_id,
                     initial_post_id,
                     forum_user_id,
-                    title,
+                    metadata,
                     text,
                     poll_input,
                 )
@@ -1005,11 +1005,17 @@ decl_module! {
         /// - DB:
         ///    - O(W)
         /// # </weight>
-        #[weight = WeightInfoForum::<T>::edit_thread_title(
+        #[weight = WeightInfoForum::<T>::edit_thread_metadata(
             T::MaxCategoryDepth::get() as u32,
-            new_title.len().saturated_into(),
+            new_metadata.len().saturated_into(),
         )]
-        fn edit_thread_title(origin, forum_user_id: ForumUserId<T>, category_id: T::CategoryId, thread_id: T::ThreadId, new_title: Vec<u8>) -> DispatchResult {
+        fn edit_thread_metadata(
+            origin,
+            forum_user_id: ForumUserId<T>,
+            category_id: T::CategoryId,
+            thread_id: T::ThreadId,
+            new_metadata: Vec<u8>
+        ) -> DispatchResult {
             // Ensure data migration is done
             Self::ensure_data_migration_done()?;
 
@@ -1021,17 +1027,17 @@ decl_module! {
             // == MUTATION SAFE ==
             //
 
-            // Update thread title
-            let title_hash = T::calculate_hash(&new_title);
-            <ThreadById<T>>::mutate(thread.category_id, thread_id, |thread| thread.title_hash = title_hash);
+            // Update thread metadata
+            let metadata_hash = T::calculate_hash(&new_metadata);
+            <ThreadById<T>>::mutate(thread.category_id, thread_id, |thread| thread.metadata_hash = metadata_hash);
 
             // Store the event
             Self::deposit_event(
-                RawEvent::ThreadTitleUpdated(
+                RawEvent::ThreadMetadataUpdated(
                     thread_id,
                     forum_user_id,
                     category_id,
-                    new_title,
+                    new_metadata,
                 )
             );
 

+ 11 - 13
runtime-modules/forum/src/mock.rs

@@ -463,7 +463,7 @@ impl WeightInfo for () {
     fn create_thread(_: u32, _: u32, _: u32) -> Weight {
         0
     }
-    fn edit_thread_title(_: u32, _: u32) -> Weight {
+    fn edit_thread_metadata(_: u32, _: u32) -> Weight {
         0
     }
     fn delete_thread(_: u32) -> Weight {
@@ -585,9 +585,8 @@ pub fn good_thread_text() -> Vec<u8> {
     b"The first post in this thread".to_vec()
 }
 
-/// Get a new title ofr the  good  thread
-pub fn good_thread_new_title() -> Vec<u8> {
-    b"Brand new thread title".to_vec()
+pub fn good_thread_new_metadata() -> Vec<u8> {
+    b"Brand new thread metadata".to_vec()
 }
 
 /// Get a good post text
@@ -729,37 +728,36 @@ pub fn create_thread_mock(
     thread_id
 }
 
-/// Create edit thread title mock
-pub fn edit_thread_title_mock(
+pub fn edit_thread_metadata_mock(
     origin: OriginType,
     forum_user_id: ForumUserId<Runtime>,
     category_id: <Runtime as Trait>::CategoryId,
     thread_id: <Runtime as Trait>::PostId,
-    new_title: Vec<u8>,
+    new_metadata: Vec<u8>,
     result: DispatchResult,
 ) -> <Runtime as Trait>::PostId {
     assert_eq!(
-        TestForumModule::edit_thread_title(
+        TestForumModule::edit_thread_metadata(
             mock_origin(origin),
             forum_user_id,
             category_id,
             thread_id,
-            new_title.clone(),
+            new_metadata.clone(),
         ),
         result
     );
     if result.is_ok() {
         assert_eq!(
-            TestForumModule::thread_by_id(category_id, thread_id).title_hash,
-            Runtime::calculate_hash(new_title.as_slice()),
+            TestForumModule::thread_by_id(category_id, thread_id).metadata_hash,
+            Runtime::calculate_hash(new_metadata.as_slice()),
         );
         assert_eq!(
             System::events().last().unwrap().event,
-            TestEvent::forum_mod(RawEvent::ThreadTitleUpdated(
+            TestEvent::forum_mod(RawEvent::ThreadMetadataUpdated(
                 thread_id,
                 forum_user_id,
                 category_id,
-                new_title.clone()
+                new_metadata
             ))
         );
     }

+ 7 - 7
runtime-modules/forum/src/tests.rs

@@ -404,12 +404,12 @@ fn update_category_archival_status_lock_works() {
         );
 
         // can't update thread
-        edit_thread_title_mock(
+        edit_thread_metadata_mock(
             origin.clone(),
             forum_lead,
             category_id,
             thread_id,
-            good_thread_new_title(),
+            good_thread_new_metadata(),
             Err(Error::<Runtime>::AncestorCategoryImmutable.into()),
         );
     });
@@ -990,7 +990,7 @@ fn create_thread_poll_timestamp() {
 
 #[test]
 // test if author can edit thread's title
-fn edit_thread_title() {
+fn edit_thread_metadata() {
     let forum_users = [NOT_FORUM_LEAD_ORIGIN_ID, NOT_FORUM_LEAD_2_ORIGIN_ID];
     let origins = [NOT_FORUM_LEAD_ORIGIN, NOT_FORUM_LEAD_2_ORIGIN];
 
@@ -1028,22 +1028,22 @@ fn edit_thread_title() {
         );
 
         // check author can edit text
-        edit_thread_title_mock(
+        edit_thread_metadata_mock(
             origins[0].clone(),
             forum_users[0],
             category_id,
             thread_id,
-            good_thread_new_title(),
+            good_thread_new_metadata(),
             Ok(()),
         );
 
         // check non-author is forbidden from editing text
-        edit_thread_title_mock(
+        edit_thread_metadata_mock(
             origins[1].clone(),
             forum_users[1],
             category_id,
             thread_id,
-            good_thread_new_title(),
+            good_thread_new_metadata(),
             Err(Error::<Runtime>::AccountDoesNotMatchThreadAuthor.into()),
         );
     });

+ 1 - 1
runtime/src/weights/forum.rs

@@ -63,7 +63,7 @@ impl forum::WeightInfo for WeightInfo {
             .saturating_add(DbWeight::get().reads((1 as Weight).saturating_mul(i as Weight)))
             .saturating_add(DbWeight::get().writes(7 as Weight))
     }
-    fn edit_thread_title(i: u32, j: u32) -> Weight {
+    fn edit_thread_metadata(i: u32, j: u32) -> Weight {
         (249_076_000 as Weight)
             .saturating_add((65_112_000 as Weight).saturating_mul(i as Weight))
             .saturating_add((145_000 as Weight).saturating_mul(j as Weight))

+ 1 - 1
storage-node/package.json

@@ -46,7 +46,7 @@
     "eslint-plugin-babel": "^5.3.1",
     "eslint-plugin-prettier": "^3.1.4",
     "prettier": "^2.0.5",
-    "typescript": "^3.9.6",
+    "typescript": "^4.3.5",
     "wsrun": "^3.6.5"
   },
   "volta": {

+ 1 - 1
storage-node/packages/runtime-api/package.json

@@ -50,7 +50,7 @@
   "dependencies": {
     "@joystream/storage-utils": "^0.1.0",
     "@joystream/types": "^0.17.0",
-    "@polkadot/api": "4.2.1",
+    "@polkadot/api": "5.3.2",
     "async-lock": "^1.2.0",
     "lodash": "^4.17.11",
     "password-prompt": "^1.1.2"

+ 6 - 5
tests/integration-tests/package.json

@@ -10,13 +10,14 @@
     "lint": "eslint . --quiet --ext .ts",
     "checks": "tsc --noEmit --pretty && prettier ./ --check && yarn lint",
     "format": "prettier ./ --write",
-    "generate:graphql-types": "graphql-codegen"
+    "generate:graphql-types": "graphql-codegen",
+    "generate:all": "yarn generate:graphql-types"
   },
   "dependencies": {
     "@apollo/client": "^3.2.5",
     "@joystream/types": "^0.17.0",
-    "@polkadot/api": "4.2.1",
-    "@polkadot/keyring": "^6.0.5",
+    "@polkadot/api": "5.3.2",
+    "@polkadot/keyring": "^7.1.1",
     "@types/async-lock": "^1.1.2",
     "@types/bn.js": "^4.11.5",
     "@types/lowdb": "^1.0.9",
@@ -28,13 +29,13 @@
     "uuid": "^7.0.3"
   },
   "devDependencies": {
-    "@polkadot/ts": "^0.3.14",
+    "@polkadot/ts": "^0.4.4",
     "@types/chai": "^4.2.11",
     "@types/uuid": "^7.0.2",
     "chai": "^4.2.0",
     "prettier": "2.0.2",
     "ts-node": "^8.8.1",
-    "typescript": "^3.8.3",
+    "typescript": "^4.3.5",
     "@graphql-codegen/cli": "^1.21.4",
     "@graphql-codegen/typescript": "^1.22.0",
     "@graphql-codegen/import-types-preset": "^1.18.1",

+ 39 - 3
tests/integration-tests/src/Api.ts

@@ -31,6 +31,9 @@ import {
   CategoryCreatedEventDetails,
   PostAddedEventDetails,
   ThreadCreatedEventDetails,
+  ProposalsCodexEventName,
+  ProposalDiscussionPostCreatedEventDetails,
+  ProposalsDiscussionEventName,
 } from './types'
 import {
   ApplicationId,
@@ -442,6 +445,17 @@ export class Api {
     return details
   }
 
+  public async retrieveProposalsCodexEventDetails(
+    result: ISubmittableResult,
+    eventName: ProposalsCodexEventName
+  ): Promise<EventDetails> {
+    const details = await this.retrieveEventDetails(result, 'proposalsCodex', eventName)
+    if (!details) {
+      throw new Error(`${eventName} event details not found in result: ${JSON.stringify(result.toHuman())}`)
+    }
+    return details
+  }
+
   public async retrieveForumEventDetails(result: ISubmittableResult, eventName: ForumEventName): Promise<EventDetails> {
     const details = await this.retrieveEventDetails(result, 'forum', eventName)
     if (!details) {
@@ -451,10 +465,31 @@ export class Api {
   }
 
   public async retrieveProposalCreatedEventDetails(result: ISubmittableResult): Promise<ProposalCreatedEventDetails> {
-    const details = await this.retrieveProposalsEngineEventDetails(result, 'ProposalCreated')
+    const details = await this.retrieveProposalsCodexEventDetails(result, 'ProposalCreated')
+    return {
+      ...details,
+      proposalId: details.event.data[0] as ProposalId,
+    }
+  }
+
+  public async retrieveProposalsDiscussionEventDetails(
+    result: ISubmittableResult,
+    eventName: ProposalsDiscussionEventName
+  ): Promise<EventDetails> {
+    const details = await this.retrieveEventDetails(result, 'proposalsDiscussion', eventName)
+    if (!details) {
+      throw new Error(`${eventName} event details not found in result: ${JSON.stringify(result.toHuman())}`)
+    }
+    return details
+  }
+
+  public async retrieveProposalDiscussionPostCreatedEventDetails(
+    result: ISubmittableResult
+  ): Promise<ProposalDiscussionPostCreatedEventDetails> {
+    const details = await this.retrieveProposalsDiscussionEventDetails(result, 'PostCreated')
     return {
       ...details,
-      proposalId: details.event.data[1] as ProposalId,
+      postId: details.event.data[0] as PostId,
     }
   }
 
@@ -551,7 +586,8 @@ export class Api {
     const details = await this.retrieveForumEventDetails(result, 'ThreadCreated')
     return {
       ...details,
-      threadId: details.event.data[0] as ThreadId,
+      threadId: details.event.data[1] as ThreadId,
+      postId: details.event.data[2] as PostId,
     }
   }
 

+ 102 - 19
tests/integration-tests/src/QueryNodeApi.ts

@@ -1,4 +1,4 @@
-import { ApolloClient, DocumentNode, NormalizedCacheObject } from '@apollo/client'
+import { ApolloClient, DocumentNode, NormalizedCacheObject } from '@apollo/client/core'
 import { MemberId, PostId, ThreadId } from '@joystream/types/common'
 import { extendDebug, Debugger } from './Debugger'
 import { ApplicationId, OpeningId, WorkerId } from '@joystream/types/working-group'
@@ -187,10 +187,10 @@ import {
   GetThreadDeletedEventsByEventIdsQuery,
   GetThreadDeletedEventsByEventIdsQueryVariables,
   GetThreadDeletedEventsByEventIds,
-  ForumThreadWithPostsFieldsFragment,
-  GetThreadsWithPostsByIdsQuery,
-  GetThreadsWithPostsByIdsQueryVariables,
-  GetThreadsWithPostsByIds,
+  ForumThreadWithInitialPostFragment,
+  GetThreadsWithInitialPostsByIds,
+  GetThreadsWithInitialPostsByIdsQuery,
+  GetThreadsWithInitialPostsByIdsQueryVariables,
   GetMembershipBoughtEventsByEventIdsQuery,
   GetMembershipBoughtEventsByEventIdsQueryVariables,
   GetMembershipBoughtEventsByEventIds,
@@ -226,10 +226,10 @@ import {
   GetPostAddedEventsByEventIdsQuery,
   GetPostAddedEventsByEventIdsQueryVariables,
   GetPostAddedEventsByEventIds,
-  ThreadTitleUpdatedEventFieldsFragment,
-  GetThreadTitleUpdatedEventsByEventIdsQuery,
-  GetThreadTitleUpdatedEventsByEventIdsQueryVariables,
-  GetThreadTitleUpdatedEventsByEventIds,
+  ThreadMetadataUpdatedEventFieldsFragment,
+  GetThreadMetadataUpdatedEventsByEventIds,
+  GetThreadMetadataUpdatedEventsByEventIdsQuery,
+  GetThreadMetadataUpdatedEventsByEventIdsQueryVariables,
   ThreadMovedEventFieldsFragment,
   GetThreadMovedEventsByEventIdsQuery,
   GetThreadMovedEventsByEventIdsQueryVariables,
@@ -263,6 +263,30 @@ import {
   GetPostDeletedEventsByEventIdsQueryVariables,
   GetPostDeletedEventsByEventIds,
   CategoryArchivalStatusUpdatedEventFieldsFragment,
+  ProposalDiscussionPostCreatedEventFieldsFragment,
+  GetProposalDiscussionPostCreatedEventsQuery,
+  GetProposalDiscussionPostCreatedEventsQueryVariables,
+  GetProposalDiscussionPostCreatedEvents,
+  ProposalDiscussionPostUpdatedEventFieldsFragment,
+  GetProposalDiscussionPostUpdatedEventsQuery,
+  GetProposalDiscussionPostUpdatedEventsQueryVariables,
+  GetProposalDiscussionPostUpdatedEvents,
+  ProposalDiscussionThreadModeChangedEventFieldsFragment,
+  GetProposalDiscussionThreadModeChangedEventsQuery,
+  GetProposalDiscussionThreadModeChangedEventsQueryVariables,
+  GetProposalDiscussionThreadModeChangedEvents,
+  ProposalDiscussionPostDeletedEventFieldsFragment,
+  GetProposalDiscussionPostDeletedEventsQuery,
+  GetProposalDiscussionPostDeletedEventsQueryVariables,
+  GetProposalDiscussionPostDeletedEvents,
+  ProposalDiscussionPostFieldsFragment,
+  GetProposalDiscussionPostsByIdsQuery,
+  GetProposalDiscussionPostsByIdsQueryVariables,
+  GetProposalDiscussionPostsByIds,
+  ProposalDiscussionThreadFieldsFragment,
+  GetProposalDiscussionThreadsByIdsQuery,
+  GetProposalDiscussionThreadsByIdsQueryVariables,
+  GetProposalDiscussionThreadsByIds,
 } from './graphql/generated/queries'
 import { Maybe } from './graphql/generated/schema'
 import { OperationDefinitionNode } from 'graphql'
@@ -834,20 +858,21 @@ export class QueryNodeApi {
     >(GetThreadCreatedEventsByEventIds, { eventIds }, 'threadCreatedEvents')
   }
 
-  public async getThreadTitleUpdatedEvents(events: EventDetails[]): Promise<ThreadTitleUpdatedEventFieldsFragment[]> {
+  public async getThreadMetadataUpdatedEvents(
+    events: EventDetails[]
+  ): Promise<ThreadMetadataUpdatedEventFieldsFragment[]> {
     const eventIds = events.map((e) => this.getQueryNodeEventId(e.blockNumber, e.indexInBlock))
     return this.multipleEntitiesQuery<
-      GetThreadTitleUpdatedEventsByEventIdsQuery,
-      GetThreadTitleUpdatedEventsByEventIdsQueryVariables
-    >(GetThreadTitleUpdatedEventsByEventIds, { eventIds }, 'threadTitleUpdatedEvents')
+      GetThreadMetadataUpdatedEventsByEventIdsQuery,
+      GetThreadMetadataUpdatedEventsByEventIdsQueryVariables
+    >(GetThreadMetadataUpdatedEventsByEventIds, { eventIds }, 'threadMetadataUpdatedEvents')
   }
 
-  public async getThreadsWithPostsByIds(ids: ThreadId[]): Promise<ForumThreadWithPostsFieldsFragment[]> {
-    return this.multipleEntitiesQuery<GetThreadsWithPostsByIdsQuery, GetThreadsWithPostsByIdsQueryVariables>(
-      GetThreadsWithPostsByIds,
-      { ids: ids.map((id) => id.toString()) },
-      'forumThreads'
-    )
+  public async getThreadsWithInitialPostsByIds(ids: ThreadId[]): Promise<ForumThreadWithInitialPostFragment[]> {
+    return this.multipleEntitiesQuery<
+      GetThreadsWithInitialPostsByIdsQuery,
+      GetThreadsWithInitialPostsByIdsQueryVariables
+    >(GetThreadsWithInitialPostsByIds, { ids: ids.map((id) => id.toString()) }, 'forumThreads')
   }
 
   public async getVoteOnPollEvents(events: EventDetails[]): Promise<VoteOnPollEventFieldsFragment[]> {
@@ -955,4 +980,62 @@ export class QueryNodeApi {
       GetPostDeletedEventsByEventIdsQueryVariables
     >(GetPostDeletedEventsByEventIds, { eventIds }, 'postDeletedEvents')
   }
+
+  public async getProposalDiscussionPostCreatedEvents(
+    events: EventDetails[]
+  ): Promise<ProposalDiscussionPostCreatedEventFieldsFragment[]> {
+    const eventIds = events.map((e) => this.getQueryNodeEventId(e.blockNumber, e.indexInBlock))
+    return this.multipleEntitiesQuery<
+      GetProposalDiscussionPostCreatedEventsQuery,
+      GetProposalDiscussionPostCreatedEventsQueryVariables
+    >(GetProposalDiscussionPostCreatedEvents, { eventIds }, 'proposalDiscussionPostCreatedEvents')
+  }
+
+  public async getProposalDiscussionPostUpdatedEvents(
+    events: EventDetails[]
+  ): Promise<ProposalDiscussionPostUpdatedEventFieldsFragment[]> {
+    const eventIds = events.map((e) => this.getQueryNodeEventId(e.blockNumber, e.indexInBlock))
+    return this.multipleEntitiesQuery<
+      GetProposalDiscussionPostUpdatedEventsQuery,
+      GetProposalDiscussionPostUpdatedEventsQueryVariables
+    >(GetProposalDiscussionPostUpdatedEvents, { eventIds }, 'proposalDiscussionPostUpdatedEvents')
+  }
+
+  public async getProposalDiscussionThreadModeChangedEvents(
+    events: EventDetails[]
+  ): Promise<ProposalDiscussionThreadModeChangedEventFieldsFragment[]> {
+    const eventIds = events.map((e) => this.getQueryNodeEventId(e.blockNumber, e.indexInBlock))
+    return this.multipleEntitiesQuery<
+      GetProposalDiscussionThreadModeChangedEventsQuery,
+      GetProposalDiscussionThreadModeChangedEventsQueryVariables
+    >(GetProposalDiscussionThreadModeChangedEvents, { eventIds }, 'proposalDiscussionThreadModeChangedEvents')
+  }
+
+  public async getProposalDiscussionPostDeletedEvents(
+    events: EventDetails[]
+  ): Promise<ProposalDiscussionPostDeletedEventFieldsFragment[]> {
+    const eventIds = events.map((e) => this.getQueryNodeEventId(e.blockNumber, e.indexInBlock))
+    return this.multipleEntitiesQuery<
+      GetProposalDiscussionPostDeletedEventsQuery,
+      GetProposalDiscussionPostDeletedEventsQueryVariables
+    >(GetProposalDiscussionPostDeletedEvents, { eventIds }, 'proposalDiscussionPostDeletedEvents')
+  }
+
+  public async getProposalDiscussionPostsByIds(
+    ids: (PostId | number)[]
+  ): Promise<ProposalDiscussionPostFieldsFragment[]> {
+    return this.multipleEntitiesQuery<
+      GetProposalDiscussionPostsByIdsQuery,
+      GetProposalDiscussionPostsByIdsQueryVariables
+    >(GetProposalDiscussionPostsByIds, { ids: ids.map((id) => id.toString()) }, 'proposalDiscussionPosts')
+  }
+
+  public async getProposalDiscussionThreadsByIds(
+    ids: (PostId | number)[]
+  ): Promise<ProposalDiscussionThreadFieldsFragment[]> {
+    return this.multipleEntitiesQuery<
+      GetProposalDiscussionThreadsByIdsQuery,
+      GetProposalDiscussionThreadsByIdsQueryVariables
+    >(GetProposalDiscussionThreadsByIds, { ids: ids.map((id) => id.toString()) }, 'proposalDiscussionThreads')
+  }
 }

+ 1 - 1
tests/integration-tests/src/Scenario.ts

@@ -2,7 +2,7 @@ import { WsProvider } from '@polkadot/api'
 import { ApiFactory } from './Api'
 import { QueryNodeApi } from './QueryNodeApi'
 import { config } from 'dotenv'
-import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client'
+import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client/core'
 import { extendDebug, Debugger } from './Debugger'
 import { Flow } from './Flow'
 import { Job } from './Job'

+ 2 - 0
tests/integration-tests/src/consts.ts

@@ -15,6 +15,8 @@ export const MIN_UNSTANKING_PERIOD = 43201
 export const LEADER_OPENING_STAKE = new BN(2000)
 export const THREAD_DEPOSIT = new BN(30)
 export const POST_DEPOSIT = new BN(10)
+export const PROPOSALS_POST_DEPOSIT = new BN(2000)
+export const ALL_BYTES = '0x' + Array.from({ length: 256 }, (v, i) => Buffer.from([i]).toString('hex')).join('')
 
 export const lockIdByWorkingGroup: { [K in WorkingGroupModuleName]: string } = {
   storageWorkingGroup: '0x0606060606060606',

+ 5 - 4
tests/integration-tests/src/fixtures/forum/AddPostsFixture.ts

@@ -68,10 +68,10 @@ export class AddPostsFixture extends StandardizedFixture {
     return this.api.retrievePostAddedEventDetails(result)
   }
 
-  protected getPostExpectedText(postParams: PostParams): string {
-    const expectedMetadata = Utils.getDeserializedMetadataFormInput(ForumPostMetadata, postParams.metadata)
-    const metadataBytes = Utils.getMetadataBytesFromInput(ForumPostMetadata, postParams.metadata)
-    return typeof expectedMetadata?.text === 'string' ? expectedMetadata.text : Utils.bytesToString(metadataBytes)
+  protected getPostExpectedText({ metadata: inputMeta }: PostParams): string {
+    const meta = Utils.getDeserializedMetadataFormInput(ForumPostMetadata, inputMeta)
+    const metaBytes = Utils.getMetadataBytesFromInput(ForumPostMetadata, inputMeta)
+    return meta ? meta.text || '' : Utils.bytesToString(metaBytes)
   }
 
   protected assertQueriedPostsAreValid(
@@ -89,6 +89,7 @@ export class AddPostsFixture extends StandardizedFixture {
       assert.equal(qPost.thread.id, postParams.threadId.toString())
       assert.equal(qPost.author.id, postParams.asMember.toString())
       assert.equal(qPost.status.__typename, expectedStatus)
+      assert.equal(qPost.isVisible, true)
       assert.equal(qPost.text, this.getPostExpectedText(postParams))
       assert.equal(
         qPost.repliesTo?.id,

+ 37 - 16
tests/integration-tests/src/fixtures/forum/CreateThreadsFixture.ts

@@ -1,16 +1,18 @@
 import { Api } from '../../Api'
 import { QueryNodeApi } from '../../QueryNodeApi'
-import { ThreadCreatedEventDetails } from '../../types'
+import { MetadataInput, ThreadCreatedEventDetails } from '../../types'
 import { SubmittableExtrinsic } from '@polkadot/api/types'
 import { Utils } from '../../utils'
 import { ISubmittableResult } from '@polkadot/types/types/'
-import { ForumThreadWithPostsFieldsFragment, ThreadCreatedEventFieldsFragment } from '../../graphql/generated/queries'
+import { ForumThreadWithInitialPostFragment, ThreadCreatedEventFieldsFragment } from '../../graphql/generated/queries'
 import { assert } from 'chai'
 import { StandardizedFixture } from '../../Fixture'
-import { CategoryId, Poll } from '@joystream/types/forum'
+import { CategoryId, PollInput } from '@joystream/types/forum'
 import { MemberId, ThreadId } from '@joystream/types/common'
 import { CreateInterface } from '@joystream/types'
 import { POST_DEPOSIT, THREAD_DEPOSIT } from '../../consts'
+import { ForumThreadMetadata, IForumThreadMetadata } from '@joystream/metadata-protobuf'
+import { isSet } from '@joystream/metadata-protobuf/utils'
 
 export type PollParams = {
   description: string
@@ -19,7 +21,7 @@ export type PollParams = {
 }
 
 export type ThreadParams = {
-  title: string
+  metadata: MetadataInput<IForumThreadMetadata>
   text: string
   categoryId: CategoryId
   asMember: MemberId
@@ -58,18 +60,15 @@ export class CreateThreadsFixture extends StandardizedFixture {
     await super.execute()
   }
 
-  protected parsePollParams(pollParams?: PollParams): CreateInterface<Poll> | null {
+  protected parsePollParams(pollParams?: PollParams): CreateInterface<PollInput> | null {
     if (!pollParams) {
       return null
     }
 
     return {
-      description_hash: pollParams.description,
+      description: pollParams.description,
       end_time: pollParams.endTime.getTime(),
-      poll_alternatives: pollParams.alternatives.map((a) => ({
-        alternative_text_hash: a,
-        vote_count: 0,
-      })),
+      poll_alternatives: pollParams.alternatives,
     }
   }
 
@@ -78,7 +77,7 @@ export class CreateThreadsFixture extends StandardizedFixture {
       this.api.tx.forum.createThread(
         params.asMember,
         params.categoryId,
-        params.title,
+        Utils.getMetadataBytesFromInput(ForumThreadMetadata, params.metadata),
         params.text,
         this.parsePollParams(params.poll)
       )
@@ -89,23 +88,33 @@ export class CreateThreadsFixture extends StandardizedFixture {
     return this.api.retrieveThreadCreatedEventDetails(result)
   }
 
+  protected getExpectedThreadTitle({ metadata: inputMeta }: ThreadParams): string {
+    const meta = Utils.getDeserializedMetadataFormInput(ForumThreadMetadata, inputMeta)
+    const metaBytes = Utils.getMetadataBytesFromInput(ForumThreadMetadata, inputMeta)
+    return meta ? meta.title || '' : Utils.bytesToString(metaBytes)
+  }
+
   protected assertQueriedThreadsAreValid(
-    qThreads: ForumThreadWithPostsFieldsFragment[],
+    qThreads: ForumThreadWithInitialPostFragment[],
     qEvents: ThreadCreatedEventFieldsFragment[]
   ): void {
     this.events.map((e, i) => {
       const qThread = qThreads.find((t) => t.id === e.threadId.toString())
       const qEvent = this.findMatchingQueryNodeEvent(e, qEvents)
       const threadParams = this.threadsParams[i]
+      const metadata = Utils.getDeserializedMetadataFormInput(ForumThreadMetadata, threadParams.metadata)
+      const expectedTitle = this.getExpectedThreadTitle(threadParams)
       Utils.assert(qThread, 'Query node: Thread not found')
-      assert.equal(qThread.title, threadParams.title)
+      assert.equal(qThread.title, expectedTitle)
       assert.equal(qThread.category.id, threadParams.categoryId.toString())
       assert.equal(qThread.author.id, threadParams.asMember.toString())
       assert.equal(qThread.status.__typename, 'ThreadStatusActive')
+      assert.equal(qThread.isVisible, true)
       assert.equal(qThread.isSticky, false)
       assert.equal(qThread.createdInEvent.id, qEvent.id)
-      const initialPost = qThread.posts.find((p) => p.origin.__typename === 'PostOriginThreadInitial')
-      Utils.assert(initialPost, "Query node: Thread's initial post not found!")
+      const { initialPost } = qThread
+      Utils.assert(initialPost, "Query node: Thread's initial post is empty!")
+      assert.equal(initialPost.id, e.postId.toString())
       assert.equal(initialPost.text, threadParams.text)
       Utils.assert(initialPost.origin.__typename === 'PostOriginThreadInitial')
       // FIXME: Temporarly not working (https://github.com/Joystream/hydra/issues/396)
@@ -116,13 +125,25 @@ export class CreateThreadsFixture extends StandardizedFixture {
       if (threadParams.poll) {
         Utils.assert(qThread.poll, 'Query node: Thread poll is missing')
         assert.equal(qThread.poll.description, threadParams.poll.description)
+        assert.sameDeepMembers(
+          qThread.poll.pollAlternatives.map((a) => [a.text, a.index]),
+          threadParams.poll.alternatives.map((text, index) => [text, index])
+        )
         assert.equal(new Date(qThread.poll.endTime).getTime(), threadParams.poll.endTime.getTime())
       }
+      if (metadata && isSet(metadata?.tags)) {
+        assert.sameDeepMembers(
+          qThread.tags.map((t) => t.id),
+          metadata.tags
+        )
+      }
     })
   }
 
   protected assertQueryNodeEventIsValid(qEvent: ThreadCreatedEventFieldsFragment, i: number): void {
     assert.equal(qEvent.thread.id, this.events[i].threadId.toString())
+    assert.equal(qEvent.title, this.getExpectedThreadTitle(this.threadsParams[i]))
+    assert.equal(qEvent.text, this.threadsParams[i].text)
   }
 
   async runQueryNodeChecks(): Promise<void> {
@@ -134,7 +155,7 @@ export class CreateThreadsFixture extends StandardizedFixture {
     )
 
     // Query the threads
-    const qThreads = await this.query.getThreadsWithPostsByIds(this.events.map((e) => e.threadId))
+    const qThreads = await this.query.getThreadsWithInitialPostsByIds(this.events.map((e) => e.threadId))
     this.assertQueriedThreadsAreValid(qThreads, qEvents)
   }
 }

+ 14 - 11
tests/integration-tests/src/fixtures/forum/DeletePostsFixture.ts

@@ -7,9 +7,10 @@ import { ISubmittableResult } from '@polkadot/types/types/'
 import { ForumPostFieldsFragment, PostDeletedEventFieldsFragment } from '../../graphql/generated/queries'
 import { assert } from 'chai'
 import { StandardizedFixture } from '../../Fixture'
-import { MemberId, PostId, ThreadId } from '@joystream/types/common'
-import { CategoryId } from '@joystream/types/forum'
+import { MemberId, PostId } from '@joystream/types/common'
+import { ExtendedPostId, PostsToDeleteMap } from '@joystream/types/forum'
 import _ from 'lodash'
+import { registry } from '@joystream/types'
 
 const DEFAULT_RATIONALE = 'State cleanup'
 
@@ -40,16 +41,17 @@ export class DeletePostsFixture extends StandardizedFixture {
   }
 
   protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
-    return this.removals.map((r) =>
-      this.api.tx.forum.deletePosts(
-        r.asMember,
-        r.posts.map(
-          ({ categoryId, threadId, postId, hide }) =>
-            [categoryId, threadId, postId, hide === undefined || hide] as [CategoryId, ThreadId, PostId, boolean]
-        ),
-        r.rationale || DEFAULT_RATIONALE
+    return this.removals.map((r) => {
+      const postsToDeleteEntries = r.posts.map(
+        ({ hide, categoryId, threadId, postId }) =>
+          [
+            this.api.createType('ExtendedPostId', { post_id: postId, thread_id: threadId, category_id: categoryId }),
+            hide === undefined || hide,
+          ] as [ExtendedPostId, boolean]
       )
-    )
+      const postsToDeleteMap = new PostsToDeleteMap(registry, new Map(postsToDeleteEntries))
+      return this.api.tx.forum.deletePosts(r.asMember, postsToDeleteMap, r.rationale || DEFAULT_RATIONALE)
+    })
   }
 
   protected async getEventFromResult(result: ISubmittableResult): Promise<EventDetails> {
@@ -71,6 +73,7 @@ export class DeletePostsFixture extends StandardizedFixture {
         Utils.assert(qPost.status.__typename === expectedStatus, `Invalid post status. Expected: ${expectedStatus}`)
         Utils.assert(qPost.status.postDeletedEvent, 'Query node: Missing PostDeletedEvent ref')
         assert.equal(qPost.status.postDeletedEvent.id, qEvent.id)
+        assert.equal(qPost.isVisible, !hidden)
       })
     })
   }

+ 4 - 3
tests/integration-tests/src/fixtures/forum/DeleteThreadsFixture.ts

@@ -4,7 +4,7 @@ import { EventDetails, MemberContext } from '../../types'
 import { SubmittableExtrinsic } from '@polkadot/api/types'
 import { Utils } from '../../utils'
 import { ISubmittableResult } from '@polkadot/types/types/'
-import { ForumThreadWithPostsFieldsFragment, ThreadDeletedEventFieldsFragment } from '../../graphql/generated/queries'
+import { ForumThreadWithInitialPostFragment, ThreadDeletedEventFieldsFragment } from '../../graphql/generated/queries'
 import { assert } from 'chai'
 import { StandardizedFixture } from '../../Fixture'
 import { CategoryId } from '@joystream/types/forum'
@@ -60,7 +60,7 @@ export class DeleteThreadsFixture extends StandardizedFixture {
   }
 
   protected assertQueriedThreadsAreValid(
-    qThreads: ForumThreadWithPostsFieldsFragment[],
+    qThreads: ForumThreadWithInitialPostFragment[],
     qEvents: ThreadDeletedEventFieldsFragment[]
   ): void {
     this.events.map((e, i) => {
@@ -73,6 +73,7 @@ export class DeleteThreadsFixture extends StandardizedFixture {
       Utils.assert(qThread.status.__typename === expectedStatus, `Invalid thread status. Expected: ${expectedStatus}`)
       Utils.assert(qThread.status.threadDeletedEvent, 'Query node: Missing ThreadDeletedEvent ref')
       assert.equal(qThread.status.threadDeletedEvent.id, qEvent.id)
+      assert.equal(qThread.isVisible, !hidden)
     })
   }
 
@@ -89,7 +90,7 @@ export class DeleteThreadsFixture extends StandardizedFixture {
     )
 
     // Query the threads
-    const qThreads = await this.query.getThreadsWithPostsByIds(this.removals.map((r) => r.threadId))
+    const qThreads = await this.query.getThreadsWithInitialPostsByIds(this.removals.map((r) => r.threadId))
     this.assertQueriedThreadsAreValid(qThreads, qEvents)
   }
 }

+ 1 - 1
tests/integration-tests/src/fixtures/forum/InitializeForumFixture.ts

@@ -177,7 +177,7 @@ export class InitializeForumFixture extends BaseQueryNodeFixture {
             Array.from({ length: threadsPerCategory }, (v, i) => ({
               categoryId,
               asMember: forumMemberIds[i % forumMemberIds.length],
-              title: `Thread ${i} in category ${categoryId.toString()}`,
+              metadata: { value: { title: `Thread ${i} in category ${categoryId.toString()}` } },
               text: `Initialize forum test thread ${i} in category ${categoryId.toString()}`,
             }))
           ))

+ 1 - 0
tests/integration-tests/src/fixtures/forum/ModeratePostsFixture.ts

@@ -61,6 +61,7 @@ export class ModeratePostsFixture extends WithForumWorkersFixture {
       Utils.assert(qPost.status.__typename === 'PostStatusModerated', 'Invalid post status')
       Utils.assert(qPost.status.postModeratedEvent, 'Query node: Missing PostModeratedEvent ref')
       assert.equal(qPost.status.postModeratedEvent.id, qEvent.id)
+      assert.equal(qPost.isVisible, false)
     })
   }
 

+ 4 - 3
tests/integration-tests/src/fixtures/forum/ModerateThreadsFixture.ts

@@ -5,7 +5,7 @@ import { WorkerId } from '@joystream/types/working-group'
 import { SubmittableExtrinsic } from '@polkadot/api/types'
 import { Utils } from '../../utils'
 import { ISubmittableResult } from '@polkadot/types/types/'
-import { ForumThreadWithPostsFieldsFragment, ThreadModeratedEventFieldsFragment } from '../../graphql/generated/queries'
+import { ForumThreadWithInitialPostFragment, ThreadModeratedEventFieldsFragment } from '../../graphql/generated/queries'
 import { assert } from 'chai'
 import { CategoryId } from '@joystream/types/forum'
 import { WithForumWorkersFixture } from './WithForumWorkersFixture'
@@ -48,7 +48,7 @@ export class ModerateThreadsFixture extends WithForumWorkersFixture {
   }
 
   protected assertQueriedThreadsAreValid(
-    qThreads: ForumThreadWithPostsFieldsFragment[],
+    qThreads: ForumThreadWithInitialPostFragment[],
     qEvents: ThreadModeratedEventFieldsFragment[]
   ): void {
     this.events.map((e, i) => {
@@ -59,6 +59,7 @@ export class ModerateThreadsFixture extends WithForumWorkersFixture {
       Utils.assert(qThread.status.__typename === 'ThreadStatusModerated', 'Invalid thread status')
       Utils.assert(qThread.status.threadModeratedEvent, 'Query node: Missing ThreadModeratedEvent ref')
       assert.equal(qThread.status.threadModeratedEvent.id, qEvent.id)
+      assert.equal(qThread.isVisible, false)
     })
   }
 
@@ -78,7 +79,7 @@ export class ModerateThreadsFixture extends WithForumWorkersFixture {
     )
 
     // Query the threads
-    const qThreads = await this.query.getThreadsWithPostsByIds(this.moderations.map((m) => m.threadId))
+    const qThreads = await this.query.getThreadsWithInitialPostsByIds(this.moderations.map((m) => m.threadId))
     this.assertQueriedThreadsAreValid(qThreads, qEvents)
   }
 }

+ 3 - 3
tests/integration-tests/src/fixtures/forum/MoveThreadsFixture.ts

@@ -4,7 +4,7 @@ import { EventDetails } from '../../types'
 import { SubmittableExtrinsic } from '@polkadot/api/types'
 import { Utils } from '../../utils'
 import { ISubmittableResult } from '@polkadot/types/types/'
-import { ForumThreadWithPostsFieldsFragment, ThreadMovedEventFieldsFragment } from '../../graphql/generated/queries'
+import { ForumThreadWithInitialPostFragment, ThreadMovedEventFieldsFragment } from '../../graphql/generated/queries'
 import { assert } from 'chai'
 import { CategoryId } from '@joystream/types/forum'
 import { ThreadId } from '@joystream/types/common'
@@ -47,7 +47,7 @@ export class MoveThreadsFixture extends WithForumWorkersFixture {
   }
 
   protected assertQueriedThreadsAreValid(
-    qThreads: ForumThreadWithPostsFieldsFragment[],
+    qThreads: ForumThreadWithInitialPostFragment[],
     qEvents: ThreadMovedEventFieldsFragment[]
   ): void {
     // Check movedInEvents array
@@ -87,7 +87,7 @@ export class MoveThreadsFixture extends WithForumWorkersFixture {
     )
 
     // Query the threads
-    const qThreads = await this.query.getThreadsWithPostsByIds(this.updates.map((u) => u.threadId))
+    const qThreads = await this.query.getThreadsWithInitialPostsByIds(this.updates.map((u) => u.threadId))
     this.assertQueriedThreadsAreValid(qThreads, qEvents)
   }
 }

+ 0 - 103
tests/integration-tests/src/fixtures/forum/UpdateThreadTitlesFixture.ts

@@ -1,103 +0,0 @@
-import { Api } from '../../Api'
-import { QueryNodeApi } from '../../QueryNodeApi'
-import { EventDetails, MemberContext } from '../../types'
-import { SubmittableExtrinsic } from '@polkadot/api/types'
-import { Utils } from '../../utils'
-import { ISubmittableResult } from '@polkadot/types/types/'
-import {
-  ForumThreadWithPostsFieldsFragment,
-  ThreadTitleUpdatedEventFieldsFragment,
-} from '../../graphql/generated/queries'
-import { assert } from 'chai'
-import { CategoryId } from '@joystream/types/forum'
-import { StandardizedFixture } from '../../Fixture'
-import { ThreadId } from '@joystream/types/common'
-import _ from 'lodash'
-
-export type ThreadTitleUpdate = {
-  categoryId: CategoryId
-  threadId: ThreadId
-  newTitle: string
-}
-
-export class UpdateThreadTitlesFixture extends StandardizedFixture {
-  protected threadAuthors: MemberContext[] = []
-  protected updates: ThreadTitleUpdate[]
-
-  public constructor(api: Api, query: QueryNodeApi, updates: ThreadTitleUpdate[]) {
-    super(api, query)
-    this.updates = updates
-  }
-
-  protected async loadAuthors(): Promise<void> {
-    this.threadAuthors = await Promise.all(
-      this.updates.map(async (u) => {
-        const thread = await this.api.query.forum.threadById(u.categoryId, u.threadId)
-        const member = await this.api.query.members.membershipById(thread.author_id)
-        return { account: member.controller_account.toString(), memberId: thread.author_id }
-      })
-    )
-  }
-
-  protected async getSignerAccountOrAccounts(): Promise<string[]> {
-    return this.threadAuthors.map((a) => a.account)
-  }
-
-  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
-    return this.updates.map((u, i) =>
-      this.api.tx.forum.editThreadTitle(this.threadAuthors[i].memberId, u.categoryId, u.threadId, u.newTitle)
-    )
-  }
-
-  protected async getEventFromResult(result: ISubmittableResult): Promise<EventDetails> {
-    return this.api.retrieveForumEventDetails(result, 'ThreadTitleUpdated')
-  }
-
-  public async execute(): Promise<void> {
-    await this.loadAuthors()
-    await super.execute()
-  }
-
-  protected assertQueriedThreadsAreValid(
-    qThreads: ForumThreadWithPostsFieldsFragment[],
-    qEvents: ThreadTitleUpdatedEventFieldsFragment[]
-  ): void {
-    // Check titleUpdates array
-    this.events.forEach((e, i) => {
-      const update = this.updates[i]
-      const qThread = qThreads.find((t) => t.id === update.threadId.toString())
-      const qEvent = this.findMatchingQueryNodeEvent(e, qEvents)
-      Utils.assert(qThread, 'Query node: Thread not found')
-      assert.include(
-        qThread.titleUpdates.map((u) => u.id),
-        qEvent.id
-      )
-    })
-
-    // Check updated titles (against lastest update per thread)
-    _.uniqBy([...this.updates].reverse(), (v) => v.threadId).map((update) => {
-      const qThread = qThreads.find((t) => t.id === update.threadId.toString())
-      Utils.assert(qThread, 'Query node: Thread not found')
-      assert.equal(qThread.title, update.newTitle)
-    })
-  }
-
-  protected assertQueryNodeEventIsValid(qEvent: ThreadTitleUpdatedEventFieldsFragment, i: number): void {
-    const { threadId, newTitle } = this.updates[i]
-    assert.equal(qEvent.thread.id, threadId.toString())
-    assert.equal(qEvent.newTitle, newTitle)
-  }
-
-  async runQueryNodeChecks(): Promise<void> {
-    await super.runQueryNodeChecks()
-    // Query the events
-    const qEvents = await this.query.tryQueryWithTimeout(
-      () => this.query.getThreadTitleUpdatedEvents(this.events),
-      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
-    )
-
-    // Query the threads
-    const qThreads = await this.query.getThreadsWithPostsByIds(this.updates.map((u) => u.threadId))
-    this.assertQueriedThreadsAreValid(qThreads, qEvents)
-  }
-}

+ 135 - 0
tests/integration-tests/src/fixtures/forum/UpdateThreadsMetadataFixture.ts

@@ -0,0 +1,135 @@
+import { Api } from '../../Api'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { EventDetails, MemberContext, MetadataInput } from '../../types'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { Utils } from '../../utils'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import {
+  ForumThreadWithInitialPostFragment,
+  ThreadMetadataUpdatedEventFieldsFragment,
+} from '../../graphql/generated/queries'
+import { assert } from 'chai'
+import { CategoryId } from '@joystream/types/forum'
+import { StandardizedFixture } from '../../Fixture'
+import { ThreadId } from '@joystream/types/common'
+import _ from 'lodash'
+import { ForumThreadMetadata, IForumThreadMetadata } from '@joystream/metadata-protobuf'
+import { isSet } from '../../../../../metadata-protobuf/lib/utils'
+
+export type ThreadMetadataUpdate = {
+  categoryId: CategoryId
+  threadId: ThreadId
+  newMetadata: MetadataInput<IForumThreadMetadata>
+  preUpdateValues?: {
+    title: string
+    tags: string[]
+  }
+}
+
+export class UpdateThreadsMetadataFixture extends StandardizedFixture {
+  protected threadAuthors: MemberContext[] = []
+  protected updates: ThreadMetadataUpdate[]
+
+  public constructor(api: Api, query: QueryNodeApi, updates: ThreadMetadataUpdate[]) {
+    super(api, query)
+    this.updates = updates
+  }
+
+  protected async loadAuthors(): Promise<void> {
+    this.threadAuthors = await Promise.all(
+      this.updates.map(async (u) => {
+        const thread = await this.api.query.forum.threadById(u.categoryId, u.threadId)
+        const member = await this.api.query.members.membershipById(thread.author_id)
+        return { account: member.controller_account.toString(), memberId: thread.author_id }
+      })
+    )
+  }
+
+  protected async getSignerAccountOrAccounts(): Promise<string[]> {
+    return this.threadAuthors.map((a) => a.account)
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    return this.updates.map((u, i) =>
+      this.api.tx.forum.editThreadMetadata(
+        this.threadAuthors[i].memberId,
+        u.categoryId,
+        u.threadId,
+        Utils.getMetadataBytesFromInput(ForumThreadMetadata, u.newMetadata)
+      )
+    )
+  }
+
+  protected async getEventFromResult(result: ISubmittableResult): Promise<EventDetails> {
+    return this.api.retrieveForumEventDetails(result, 'ThreadMetadataUpdated')
+  }
+
+  public async execute(): Promise<void> {
+    await this.loadAuthors()
+    await super.execute()
+  }
+
+  protected getNewThreadTitle({ newMetadata: inputMeta }: ThreadMetadataUpdate): string | null {
+    const meta = Utils.getDeserializedMetadataFormInput(ForumThreadMetadata, inputMeta)
+    const metaBytes = Utils.getMetadataBytesFromInput(ForumThreadMetadata, inputMeta)
+    return meta ? meta.title || null : Utils.bytesToString(metaBytes)
+  }
+
+  protected getNewThreadTags({ newMetadata: inputMeta }: ThreadMetadataUpdate): string[] | null {
+    const meta = Utils.getDeserializedMetadataFormInput(ForumThreadMetadata, inputMeta)
+    return meta && isSet(meta.tags) ? meta.tags : null
+  }
+
+  protected assertQueriedThreadsAreValid(
+    qThreads: ForumThreadWithInitialPostFragment[],
+    qEvents: ThreadMetadataUpdatedEventFieldsFragment[]
+  ): void {
+    // Check metadataUpdates array
+    this.events.forEach((e, i) => {
+      const update = this.updates[i]
+      const qThread = qThreads.find((t) => t.id === update.threadId.toString())
+      const qEvent = this.findMatchingQueryNodeEvent(e, qEvents)
+      Utils.assert(qThread, 'Query node: Thread not found')
+      assert.include(
+        qThread.metadataUpdates.map((u) => u.id),
+        qEvent.id
+      )
+    })
+
+    // Check updated titles/tags (against lastest update per thread that affected them)
+    _.uniq(this.updates.map((u) => u.threadId)).map((threadId) => {
+      const qThread = qThreads.find((t) => t.id === threadId.toString())
+      Utils.assert(qThread, 'Query node: Thread not found')
+      const threadUpdates = this.updates.filter((u) => u.threadId === threadId)
+      const lastNewTitle = _.last(threadUpdates.map((u) => this.getNewThreadTitle(u)).filter((v) => v !== null))
+      const lastNewTags = _.last(threadUpdates.map((u) => this.getNewThreadTags(u)).filter((v) => v !== null))
+      const expectedTitle = lastNewTitle ?? (threadUpdates[0].preUpdateValues?.title || qThread.createdInEvent.title)
+      const expectedTags = (lastNewTags ?? (threadUpdates[0].preUpdateValues?.tags || [])).filter((v) => v)
+      assert.equal(qThread.title, expectedTitle)
+      assert.sameMembers(
+        qThread.tags.map((t) => t.id),
+        expectedTags
+      )
+    })
+  }
+
+  protected assertQueryNodeEventIsValid(qEvent: ThreadMetadataUpdatedEventFieldsFragment, i: number): void {
+    const update = this.updates[i]
+    const newTitle = this.getNewThreadTitle(update)
+    assert.equal(qEvent.thread.id, update.threadId.toString())
+    assert.equal(qEvent.newTitle, newTitle)
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    // Query the events
+    const qEvents = await this.query.tryQueryWithTimeout(
+      () => this.query.getThreadMetadataUpdatedEvents(this.events),
+      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
+    )
+
+    // Query the threads
+    const qThreads = await this.query.getThreadsWithInitialPostsByIds(this.updates.map((u) => u.threadId))
+    this.assertQueriedThreadsAreValid(qThreads, qEvents)
+  }
+}

+ 3 - 3
tests/integration-tests/src/fixtures/forum/VoteOnPollFixture.ts

@@ -3,7 +3,7 @@ import { QueryNodeApi } from '../../QueryNodeApi'
 import { EventDetails } from '../../types'
 import { SubmittableExtrinsic } from '@polkadot/api/types'
 import { ISubmittableResult } from '@polkadot/types/types/'
-import { ForumThreadWithPostsFieldsFragment, VoteOnPollEventFieldsFragment } from '../../graphql/generated/queries'
+import { ForumThreadWithInitialPostFragment, VoteOnPollEventFieldsFragment } from '../../graphql/generated/queries'
 import { assert } from 'chai'
 import { StandardizedFixture } from '../../Fixture'
 import { CategoryId } from '@joystream/types/forum'
@@ -49,7 +49,7 @@ export class VoteOnPollFixture extends StandardizedFixture {
     assert.equal(qEvent.votingMember.id, this.votes[i].asMember.toString())
   }
 
-  protected assertQueriedThreadsAreValid(qThreads: ForumThreadWithPostsFieldsFragment[]): void {
+  protected assertQueriedThreadsAreValid(qThreads: ForumThreadWithInitialPostFragment[]): void {
     this.votes.forEach(({ asMember, threadId, index }) => {
       const qThread = qThreads.find((t) => t.id === threadId.toString())
       Utils.assert(qThread, 'Query node: Thread not found')
@@ -69,7 +69,7 @@ export class VoteOnPollFixture extends StandardizedFixture {
     )
 
     // Query the threads
-    const qThreads = await this.query.getThreadsWithPostsByIds(this.votes.map((v) => v.threadId))
+    const qThreads = await this.query.getThreadsWithInitialPostsByIds(this.votes.map((v) => v.threadId))
     this.assertQueriedThreadsAreValid(qThreads)
   }
 }

+ 1 - 1
tests/integration-tests/src/fixtures/forum/index.ts

@@ -5,7 +5,7 @@ export { CreateThreadsFixture, ThreadParams } from './CreateThreadsFixture'
 export { DeleteThreadsFixture, ThreadRemovalInput } from './DeleteThreadsFixture'
 export { VoteOnPollFixture, VoteParams } from './VoteOnPollFixture'
 export { AddPostsFixture, PostParams } from './AddPostsFixture'
-export { UpdateThreadTitlesFixture, ThreadTitleUpdate } from './UpdateThreadTitlesFixture'
+export { UpdateThreadsMetadataFixture, ThreadMetadataUpdate } from './UpdateThreadsMetadataFixture'
 export { MoveThreadsFixture, MoveThreadParams } from './MoveThreadsFixture'
 export { SetStickyThreadsFixture, StickyThreadsParams } from './SetStickyThreadsFixture'
 export { UpdateCategoryModeratorsFixture, CategoryModeratorStatusUpdate } from './UpdateCategoryModeratorsFixture'

+ 1 - 0
tests/integration-tests/src/fixtures/proposals/CancelProposalsFixture.ts

@@ -43,6 +43,7 @@ export class CancelProposalsFixture extends StandardizedFixture {
       Utils.assert(qProposal, 'Query node: Proposal not found')
       Utils.assert(qProposal.status.__typename === 'ProposalStatusCancelled', 'Invalid proposal status')
       assert.equal(qProposal.status.cancelledInEvent?.id, qEvent.id)
+      assert.equal(qProposal.isFinalized, true)
     })
   }
 

+ 2 - 0
tests/integration-tests/src/fixtures/proposals/CreateProposalsFixture.ts

@@ -316,6 +316,8 @@ export class CreateProposalsFixture extends StandardizedFixture {
       assert.equal(new Date(qProposal.statusSetAtTime).getTime(), e.blockTimestamp)
       assert.equal(qProposal.createdInEvent.inBlock, e.blockNumber)
       assert.equal(qProposal.createdInEvent.inExtrinsic, this.extrinsics[i].hash.toString())
+      assert.equal(qProposal.isFinalized, false)
+      assert.equal(qProposal.discussionThread.mode.__typename, 'ProposalDiscussionThreadModeOpen')
       this.assertProposalDetailsAreValid(proposalParams, qProposal)
     })
   }

+ 4 - 0
tests/integration-tests/src/fixtures/proposals/DecideOnProposalStatusFixture.ts

@@ -124,12 +124,14 @@ export class DecideOnProposalStatusFixture extends BaseQueryNodeFixture {
       ) {
         Utils.assert(qProposal.status.proposalExecutedEvent?.id, 'Missing proposalExecutedEvent reference')
         assert.equal(qProposal.status.proposalExecutedEvent?.executionStatus.__typename, qProposal.status.__typename)
+        assert.equal(qProposal.isFinalized, true)
       } else if (
         qProposal.status.__typename === 'ProposalStatusDormant' ||
         qProposal.status.__typename === 'ProposalStatusGracing'
       ) {
         Utils.assert(qProposal.status.proposalStatusUpdatedEvent?.id, 'Missing proposalStatusUpdatedEvent reference')
         assert.equal(qProposal.status.proposalStatusUpdatedEvent?.newStatus.__typename, qProposal.status.__typename)
+        assert.equal(qProposal.isFinalized, false)
         assert.include(
           qProposal.proposalStatusUpdates.map((u) => u.id),
           qProposal.status.proposalStatusUpdatedEvent?.id
@@ -137,6 +139,7 @@ export class DecideOnProposalStatusFixture extends BaseQueryNodeFixture {
       } else {
         Utils.assert(qProposal.status.proposalDecisionMadeEvent?.id, 'Missing proposalDecisionMadeEvent reference')
         assert.equal(qProposal.status.proposalDecisionMadeEvent?.decisionStatus.__typename, qProposal.status.__typename)
+        assert.equal(qProposal.isFinalized, true)
       }
     })
   }
@@ -149,6 +152,7 @@ export class DecideOnProposalStatusFixture extends BaseQueryNodeFixture {
       qProposal.status.__typename,
       params.expectExecutionFailure ? 'ProposalStatusExecutionFailed' : 'ProposalStatusExecuted'
     )
+    assert.equal(qProposal.isFinalized, true)
     if (proposal.exactExecutionBlock.isSome) {
       assert.equal(qProposal.statusSetAtBlock, proposal.exactExecutionBlock.unwrap().toNumber())
     } else if (proposal.parameters.gracePeriod.toNumber()) {

+ 1 - 0
tests/integration-tests/src/fixtures/proposals/ExpireProposalsFixture.ts

@@ -37,6 +37,7 @@ export class ExpireProposalsFixture extends BaseQueryNodeFixture {
       )
       Utils.assert(qProposal.status.proposalDecisionMadeEvent, 'Missing proposalDecisionMadeEvent relation')
       assert.equal(qProposal.status.proposalDecisionMadeEvent.decisionStatus.__typename, 'ProposalStatusExpired')
+      assert.equal(qProposal.isFinalized, true)
     })
   }
 

+ 103 - 0
tests/integration-tests/src/fixtures/proposalsDiscussion/ChangeThreadsModeFixture.ts

@@ -0,0 +1,103 @@
+import { Api } from '../../Api'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { EventDetails } from '../../types'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { Utils } from '../../utils'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import {
+  ProposalDiscussionThreadFieldsFragment,
+  ProposalDiscussionThreadModeChangedEventFieldsFragment,
+} from '../../graphql/generated/queries'
+import { assert } from 'chai'
+import { StandardizedFixture } from '../../Fixture'
+import { MemberId, ThreadId } from '@joystream/types/common'
+import { CreateInterface } from '@joystream/types'
+import { ThreadMode } from '@joystream/types/proposals'
+import _ from 'lodash'
+
+export type ThreadModeChangeParams = {
+  threadId: ThreadId | number
+  newMode: CreateInterface<ThreadMode>
+  asMember: MemberId
+}
+
+export class ChangeThreadsModeFixture extends StandardizedFixture {
+  protected threadsModeChangeParams: ThreadModeChangeParams[]
+
+  public constructor(api: Api, query: QueryNodeApi, threadsModeChangeParams: ThreadModeChangeParams[]) {
+    super(api, query)
+    this.threadsModeChangeParams = threadsModeChangeParams
+  }
+
+  protected async getSignerAccountOrAccounts(): Promise<string[]> {
+    return this.api.getMemberSigners(this.threadsModeChangeParams)
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    return this.threadsModeChangeParams.map((params) =>
+      this.api.tx.proposalsDiscussion.changeThreadMode(params.asMember, params.threadId, params.newMode)
+    )
+  }
+
+  protected async getEventFromResult(result: ISubmittableResult): Promise<EventDetails> {
+    return this.api.retrieveProposalsDiscussionEventDetails(result, 'ThreadModeChanged')
+  }
+
+  protected assertQueriedThreadsAreValid(
+    qThreads: ProposalDiscussionThreadFieldsFragment[],
+    qEvents: ProposalDiscussionThreadModeChangedEventFieldsFragment[]
+  ): void {
+    for (const [threadId, changes] of _.entries(
+      _.groupBy(this.threadsModeChangeParams, (p) => p.threadId.toString())
+    )) {
+      const finalUpdate = _.last(changes)
+      const qThread = qThreads.find((t) => t.id === threadId.toString())
+      Utils.assert(qThread, 'Query node: Thread not found!')
+      assert.includeDeepMembers(
+        qThread.modeChanges.map((e) => e.id),
+        qEvents.filter((e) => e.thread.id === qThread.id).map((e) => e.id)
+      )
+      Utils.assert(finalUpdate)
+      const newMode = this.api.createType('ThreadMode', finalUpdate.newMode)
+      if (newMode.isOfType('Closed')) {
+        Utils.assert(
+          qThread.mode.__typename === 'ProposalDiscussionThreadModeClosed',
+          `Invalid thread status ${qThread.mode.__typename}`
+        )
+        Utils.assert(qThread.mode.whitelist, 'Query node: Missing thread.mode.whitelist')
+        assert.sameDeepMembers(
+          qThread.mode.whitelist.members.map((m) => m.id),
+          newMode.asType('Closed').map((memberId) => memberId.toString())
+        )
+      } else if (newMode.isOfType('Open')) {
+        assert.equal(qThread.mode.__typename, 'ProposalDiscussionThreadModeOpen')
+      } else {
+        throw new Error(`Unknown thread mode: ${newMode.type}`)
+      }
+    }
+  }
+
+  protected assertQueryNodeEventIsValid(
+    qEvent: ProposalDiscussionThreadModeChangedEventFieldsFragment,
+    i: number
+  ): void {
+    const params = this.threadsModeChangeParams[i]
+    assert.equal(qEvent.thread.id, params.threadId.toString())
+    assert.equal(qEvent.actor.id, params.asMember.toString())
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    // Query the events
+    const qEvents = await this.query.tryQueryWithTimeout(
+      () => this.query.getProposalDiscussionThreadModeChangedEvents(this.events),
+      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
+    )
+
+    // Query the posts
+    const qThreads = await this.query.getProposalDiscussionThreadsByIds(
+      this.threadsModeChangeParams.map((p) => p.threadId)
+    )
+    this.assertQueriedThreadsAreValid(qThreads, qEvents)
+  }
+}

+ 123 - 0
tests/integration-tests/src/fixtures/proposalsDiscussion/CreatePostsFixture.ts

@@ -0,0 +1,123 @@
+import { Api } from '../../Api'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { MetadataInput, ProposalDiscussionPostCreatedEventDetails } from '../../types'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { Utils } from '../../utils'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import {
+  ProposalDiscussionPostCreatedEventFieldsFragment,
+  ProposalDiscussionPostFieldsFragment,
+} from '../../graphql/generated/queries'
+import { assert } from 'chai'
+import { StandardizedFixture } from '../../Fixture'
+import { MemberId, PostId, ThreadId } from '@joystream/types/common'
+import { PROPOSALS_POST_DEPOSIT } from '../../consts'
+import { ProposalsDiscussionPostMetadata, IProposalsDiscussionPostMetadata } from '@joystream/metadata-protobuf'
+
+export type PostParams = {
+  threadId: ThreadId | number
+  asMember: MemberId
+  editable?: boolean // defaults to true
+  metadata: MetadataInput<IProposalsDiscussionPostMetadata> & { expectReplyFailure?: boolean }
+}
+
+export class CreatePostsFixture extends StandardizedFixture {
+  protected events: ProposalDiscussionPostCreatedEventDetails[] = []
+  protected postsParams: PostParams[]
+
+  public constructor(api: Api, query: QueryNodeApi, postsParams: PostParams[]) {
+    super(api, query)
+    this.postsParams = postsParams
+  }
+
+  public getCreatedPostsIds(): PostId[] {
+    if (!this.events.length) {
+      throw new Error('Trying to get created posts ids before they were created!')
+    }
+    return this.events.map((e) => e.postId)
+  }
+
+  protected async getSignerAccountOrAccounts(): Promise<string[]> {
+    return this.api.getMemberSigners(this.postsParams)
+  }
+
+  public async execute(): Promise<void> {
+    const accounts = await this.getSignerAccountOrAccounts()
+    // Send required funds to accounts (ProposalsPostDeposit)
+    await Promise.all(accounts.map((a) => this.api.treasuryTransferBalance(a, PROPOSALS_POST_DEPOSIT)))
+    await super.execute()
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    return this.postsParams.map((params) =>
+      this.api.tx.proposalsDiscussion.addPost(
+        params.asMember,
+        params.threadId,
+        Utils.getMetadataBytesFromInput(ProposalsDiscussionPostMetadata, params.metadata),
+        params.editable === undefined || params.editable
+      )
+    )
+  }
+
+  protected async getEventFromResult(result: ISubmittableResult): Promise<ProposalDiscussionPostCreatedEventDetails> {
+    return this.api.retrieveProposalDiscussionPostCreatedEventDetails(result)
+  }
+
+  protected getPostExpectedText(postParams: PostParams): string {
+    const expectedMetadata = Utils.getDeserializedMetadataFormInput(
+      ProposalsDiscussionPostMetadata,
+      postParams.metadata
+    )
+    const metadataBytes = Utils.getMetadataBytesFromInput(ProposalsDiscussionPostMetadata, postParams.metadata)
+    return typeof expectedMetadata?.text === 'string' ? expectedMetadata.text : Utils.bytesToString(metadataBytes)
+  }
+
+  protected assertQueriedPostsAreValid(
+    qPosts: ProposalDiscussionPostFieldsFragment[],
+    qEvents: ProposalDiscussionPostCreatedEventFieldsFragment[]
+  ): void {
+    this.events.map((e, i) => {
+      const qPost = qPosts.find((p) => p.id === e.postId.toString())
+      const qEvent = this.findMatchingQueryNodeEvent(e, qEvents)
+      const postParams = this.postsParams[i]
+      const expectedStatus =
+        postParams.editable === undefined || postParams.editable
+          ? 'ProposalDiscussionPostStatusActive'
+          : 'ProposalDiscussionPostStatusLocked'
+      const expectedMetadata = Utils.getDeserializedMetadataFormInput(
+        ProposalsDiscussionPostMetadata,
+        postParams.metadata
+      )
+      Utils.assert(qPost, 'Query node: Post not found')
+      assert.equal(qPost.discussionThread.id, postParams.threadId.toString())
+      assert.equal(qPost.author.id, postParams.asMember.toString())
+      assert.equal(qPost.status.__typename, expectedStatus)
+      assert.equal(qPost.isVisible, true)
+      assert.equal(qPost.text, this.getPostExpectedText(postParams))
+      assert.equal(
+        qPost.repliesTo?.id,
+        postParams.metadata.expectReplyFailure ? undefined : expectedMetadata?.repliesTo?.toString()
+      )
+      assert.equal(qPost.createdInEvent.id, qEvent.id)
+    })
+  }
+
+  protected assertQueryNodeEventIsValid(qEvent: ProposalDiscussionPostCreatedEventFieldsFragment, i: number): void {
+    const params = this.postsParams[i]
+    assert.equal(qEvent.post.id, this.events[i].postId.toString())
+    assert.equal(qEvent.text, this.getPostExpectedText(params))
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    // Query the events
+    const qEvents = await this.query.tryQueryWithTimeout(
+      () => this.query.getProposalDiscussionPostCreatedEvents(this.events),
+      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
+    )
+
+    // Query the posts
+    const qPosts = await this.query.getProposalDiscussionPostsByIds(this.events.map((e) => e.postId))
+    this.assertQueriedPostsAreValid(qPosts, qEvents)
+  }
+}

+ 84 - 0
tests/integration-tests/src/fixtures/proposalsDiscussion/DeletePostsFixture.ts

@@ -0,0 +1,84 @@
+import { Api } from '../../Api'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { EventDetails } from '../../types'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { Utils } from '../../utils'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import {
+  ProposalDiscussionPostDeletedEventFieldsFragment,
+  ProposalDiscussionPostFieldsFragment,
+} from '../../graphql/generated/queries'
+import { assert } from 'chai'
+import { StandardizedFixture } from '../../Fixture'
+import { MemberId, PostId, ThreadId } from '@joystream/types/common'
+
+export type DeletePostParams = {
+  threadId: ThreadId | number
+  postId: PostId | number
+  asMember: MemberId
+  hide?: boolean // defaults to true
+}
+
+export class DeletePostsFixture extends StandardizedFixture {
+  protected deletePostsParams: DeletePostParams[]
+
+  public constructor(api: Api, query: QueryNodeApi, deletePostsParams: DeletePostParams[]) {
+    super(api, query)
+    this.deletePostsParams = deletePostsParams
+  }
+
+  protected async getSignerAccountOrAccounts(): Promise<string[]> {
+    return this.api.getMemberSigners(this.deletePostsParams)
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    return this.deletePostsParams.map((params) =>
+      this.api.tx.proposalsDiscussion.deletePost(
+        params.asMember,
+        params.postId,
+        params.threadId,
+        params.hide === undefined || params.hide
+      )
+    )
+  }
+
+  protected async getEventFromResult(result: ISubmittableResult): Promise<EventDetails> {
+    return this.api.retrieveProposalsDiscussionEventDetails(result, 'PostDeleted')
+  }
+
+  protected assertQueriedPostsAreValid(
+    qPosts: ProposalDiscussionPostFieldsFragment[],
+    qEvents: ProposalDiscussionPostDeletedEventFieldsFragment[]
+  ): void {
+    this.events.map((e, i) => {
+      const params = this.deletePostsParams[i]
+      const qPost = qPosts.find((p) => p.id === params.postId.toString())
+      const qEvent = this.findMatchingQueryNodeEvent(e, qEvents)
+      const hidden = params.hide === undefined || params.hide
+      const expectedStatus = hidden ? 'ProposalDiscussionPostStatusRemoved' : 'ProposalDiscussionPostStatusLocked'
+      Utils.assert(qPost, 'Query node: Post not found')
+      Utils.assert(qPost.status.__typename === expectedStatus, `Invalid post status (${qPost.status.__typename})`)
+      assert.equal(qPost.status.deletedInEvent?.id, qEvent.id)
+      assert.equal(qPost.isVisible, !hidden)
+    })
+  }
+
+  protected assertQueryNodeEventIsValid(qEvent: ProposalDiscussionPostDeletedEventFieldsFragment, i: number): void {
+    const params = this.deletePostsParams[i]
+    assert.equal(qEvent.post.id, params.postId.toString())
+    assert.equal(qEvent.actor.id, params.asMember.toString())
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    // Query the events
+    const qEvents = await this.query.tryQueryWithTimeout(
+      () => this.query.getProposalDiscussionPostDeletedEvents(this.events),
+      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
+    )
+
+    // Query the posts
+    const qPosts = await this.query.getProposalDiscussionPostsByIds(this.deletePostsParams.map((p) => p.postId))
+    this.assertQueriedPostsAreValid(qPosts, qEvents)
+  }
+}

+ 81 - 0
tests/integration-tests/src/fixtures/proposalsDiscussion/UpdatePostsFixture.ts

@@ -0,0 +1,81 @@
+import { Api } from '../../Api'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { EventDetails } from '../../types'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { Utils } from '../../utils'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import {
+  ProposalDiscussionPostFieldsFragment,
+  ProposalDiscussionPostUpdatedEventFieldsFragment,
+} from '../../graphql/generated/queries'
+import { assert } from 'chai'
+import { StandardizedFixture } from '../../Fixture'
+import { MemberId, PostId, ThreadId } from '@joystream/types/common'
+import _ from 'lodash'
+
+export type PostUpdateParams = {
+  threadId: ThreadId | number
+  postId: PostId | number
+  newText: string
+  asMember: MemberId // Cannot retrieve this information from the runtime currently
+}
+
+export class UpdatePostsFixture extends StandardizedFixture {
+  protected postsUpdates: PostUpdateParams[]
+  protected postsAuthors: MemberId[] = []
+
+  public constructor(api: Api, query: QueryNodeApi, postsUpdates: PostUpdateParams[]) {
+    super(api, query)
+    this.postsUpdates = postsUpdates
+  }
+
+  protected async getSignerAccountOrAccounts(): Promise<string[]> {
+    return this.api.getMemberSigners(this.postsUpdates)
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    return this.postsUpdates.map((params) =>
+      this.api.tx.proposalsDiscussion.updatePost(params.threadId, params.postId, params.newText)
+    )
+  }
+
+  protected async getEventFromResult(result: ISubmittableResult): Promise<EventDetails> {
+    return this.api.retrieveProposalsDiscussionEventDetails(result, 'PostUpdated')
+  }
+
+  protected assertQueriedPostsAreValid(
+    qPosts: ProposalDiscussionPostFieldsFragment[],
+    qEvents: ProposalDiscussionPostUpdatedEventFieldsFragment[]
+  ): void {
+    for (const [postId, updates] of _.entries(_.groupBy(this.postsUpdates, (p) => p.postId.toString()))) {
+      const finalUpdate = _.last(updates)
+      const qPost = qPosts.find((p) => p.id === postId.toString())
+      Utils.assert(qPost, 'Query node: Post not found!')
+      assert.includeDeepMembers(
+        qPost.updates.map((e) => e.id),
+        qEvents.filter((e) => e.post.id === qPost.id).map((e) => e.id)
+      )
+      Utils.assert(finalUpdate)
+      assert.equal(qPost.text, Utils.asText(finalUpdate.newText))
+    }
+  }
+
+  protected assertQueryNodeEventIsValid(qEvent: ProposalDiscussionPostUpdatedEventFieldsFragment, i: number): void {
+    const params = this.postsUpdates[i]
+    assert.equal(qEvent.post.id, params.postId.toString())
+    assert.equal(qEvent.text, Utils.asText(params.newText))
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    // Query the events
+    const qEvents = await this.query.tryQueryWithTimeout(
+      () => this.query.getProposalDiscussionPostUpdatedEvents(this.events),
+      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
+    )
+
+    // Query the posts
+    const qPosts = await this.query.getProposalDiscussionPostsByIds(this.postsUpdates.map((u) => u.postId))
+    this.assertQueriedPostsAreValid(qPosts, qEvents)
+  }
+}

+ 4 - 0
tests/integration-tests/src/fixtures/proposalsDiscussion/index.ts

@@ -0,0 +1,4 @@
+export { ChangeThreadsModeFixture, ThreadModeChangeParams } from './ChangeThreadsModeFixture'
+export { CreatePostsFixture, PostParams } from './CreatePostsFixture'
+export { DeletePostsFixture, DeletePostParams } from './DeletePostsFixture'
+export { UpdatePostsFixture, PostUpdateParams } from './UpdatePostsFixture'

+ 2 - 2
tests/integration-tests/src/fixtures/workingGroups/FillOpeningsFixture.ts

@@ -8,7 +8,7 @@ import { Application, ApplicationId, Opening, OpeningId, WorkerId } from '@joyst
 import { SubmittableExtrinsic } from '@polkadot/api/types'
 import { ISubmittableResult } from '@polkadot/types/types/'
 import { Utils } from '../../utils'
-import { JoyBTreeSet } from '@joystream/types/common'
+import { BTreeSet } from '@polkadot/types'
 import { registry } from '@joystream/types'
 import { lockIdByWorkingGroup } from '../../consts'
 import {
@@ -55,7 +55,7 @@ export class FillOpeningsFixture extends BaseWorkingGroupFixture {
 
   protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
     const extrinsics = this.openingIds.map((openingId, i) => {
-      const applicationsSet = new (JoyBTreeSet(ApplicationId))(registry, this.acceptedApplicationsIdsArrays[i])
+      const applicationsSet = new (BTreeSet.with(ApplicationId))(registry, this.acceptedApplicationsIdsArrays[i])
       this.debug(
         'Applications to accept:',
         this.acceptedApplicationsIdsArrays[i].map((id) => id.toNumber())

+ 1 - 1
tests/integration-tests/src/flows/forum/polls.ts

@@ -32,7 +32,7 @@ export default async function polls({ api, query }: FlowProps): Promise<void> {
   const pollThreads: ThreadParams[] = memberIds.map((memberId, i) => ({
     categoryId,
     asMember: memberId,
-    title: `Poll ${i}`,
+    metadata: { value: { title: `Poll ${i}` } },
     text: `Poll ${i} desc`,
     poll: {
       description: `Poll ${i} question?`,

+ 64 - 0
tests/integration-tests/src/flows/forum/threadTags.ts

@@ -0,0 +1,64 @@
+import { FlowProps } from '../../Flow'
+import { extendDebug } from '../../Debugger'
+import { FixtureRunner } from '../../Fixture'
+import {
+  CreateThreadsFixture,
+  ThreadParams,
+  InitializeForumFixture,
+  ThreadMetadataUpdate,
+  UpdateThreadsMetadataFixture,
+} from '../../fixtures/forum'
+import { integrateMeta } from '@joystream/metadata-protobuf/utils'
+import { IForumThreadMetadata } from '@joystream/metadata-protobuf'
+
+export default async function threadTags({ api, query }: FlowProps): Promise<void> {
+  const debug = extendDebug(`flow:threadTags`)
+  debug('Started')
+  api.enableDebugTxLogs()
+
+  // Initialize forum
+  const initializeForumFixture = new InitializeForumFixture(api, query, {
+    numberOfForumMembers: 1,
+    numberOfCategories: 1,
+  })
+  await new FixtureRunner(initializeForumFixture).runWithQueryNodeChecks()
+
+  const [categoryId] = initializeForumFixture.getCreatedCategoryIds()
+  const [memberId] = initializeForumFixture.getCreatedForumMemberIds()
+
+  const originalMeta = { title: 'Test thread with tags', tags: ['tag1', 'tag2', 'tag3'] }
+  const threadParams: ThreadParams = {
+    categoryId,
+    asMember: memberId,
+    text: 'Test thread with tags',
+    metadata: { value: originalMeta },
+  }
+  const createThreadsFixture = new CreateThreadsFixture(api, query, [threadParams])
+  await new FixtureRunner(createThreadsFixture).runWithQueryNodeChecks()
+  const [threadId] = createThreadsFixture.getCreatedThreadsIds()
+
+  const updateMetas: IForumThreadMetadata[] = [
+    { title: 'New title' },
+    { tags: ['newTag1', 'tag2', 'tag3'] },
+    { tags: [''] },
+    { title: 'Final update title', tags: ['finalTag1', 'finalTag2', 'finalTag3'] },
+  ]
+  let previousPreUpdateValues = { ...originalMeta }
+  const updates: ThreadMetadataUpdate[] = updateMetas.map((meta) => {
+    const preUpdateValues = { ...previousPreUpdateValues }
+    integrateMeta(preUpdateValues, meta, ['title', 'tags'])
+    previousPreUpdateValues = { ...preUpdateValues }
+    return {
+      categoryId,
+      threadId,
+      newMetadata: { value: meta },
+      preUpdateValues,
+    }
+  })
+  const updateFixtures = updates.map((u) => new UpdateThreadsMetadataFixture(api, query, [u]))
+  for (const fixutre of updateFixtures) {
+    await new FixtureRunner(fixutre).runWithQueryNodeChecks()
+  }
+
+  debug('Done')
+}

+ 8 - 8
tests/integration-tests/src/flows/forum/threads.ts

@@ -9,8 +9,8 @@ import {
   SetStickyThreadsFixture,
   StickyThreadsParams,
   ThreadRemovalInput,
-  ThreadTitleUpdate,
-  UpdateThreadTitlesFixture,
+  ThreadMetadataUpdate,
+  UpdateThreadsMetadataFixture,
 } from '../../fixtures/forum'
 import { CategoryId } from '@joystream/types/forum'
 
@@ -43,17 +43,17 @@ export default async function threads({ api, query }: FlowProps): Promise<void>
   const setStickyThreadsRunner = new FixtureRunner(setStickyThreadsFixture)
   await setStickyThreadsRunner.run()
 
-  // Update titles
-  let titleUpdates: ThreadTitleUpdate[] = []
+  // Metadata updates
+  let metadataUpdates: ThreadMetadataUpdate[] = []
   initializeForumFixture.getThreadPaths().forEach(
     (threadPath, i) =>
-      (titleUpdates = titleUpdates.concat([
-        { ...threadPath, newTitle: '' },
-        { ...threadPath, newTitle: `Test updated title ${i}` },
+      (metadataUpdates = metadataUpdates.concat([
+        { ...threadPath, newMetadata: { value: { title: '', tags: [] } } },
+        { ...threadPath, newMetadata: { value: { title: `Test updated title ${i}`, tags: [] } } },
       ]))
   )
 
-  const updateThreadTitlesFixture = new UpdateThreadTitlesFixture(api, query, titleUpdates)
+  const updateThreadTitlesFixture = new UpdateThreadsMetadataFixture(api, query, metadataUpdates)
   const updateThreadTitlesRunner = new FixtureRunner(updateThreadTitlesFixture)
   await updateThreadTitlesRunner.run()
 

+ 2 - 2
tests/integration-tests/src/flows/proposals/expireProposal.ts

@@ -29,8 +29,8 @@ export default async function expireProposal({ api, query, lock }: FlowProps): P
   await new FixtureRunner(createProposalFixture).run()
   const [proposalId] = createProposalFixture.getCreatedProposalsIds()
 
-  const approveProposalFixture = new ExpireProposalsFixture(api, query, [proposalId])
-  await new FixtureRunner(approveProposalFixture).runWithQueryNodeChecks()
+  const expireProposalFixture = new ExpireProposalsFixture(api, query, [proposalId])
+  await new FixtureRunner(expireProposalFixture).runWithQueryNodeChecks()
 
   unlock()
 

+ 127 - 0
tests/integration-tests/src/flows/proposalsDiscussion/index.ts

@@ -0,0 +1,127 @@
+import { FlowProps } from '../../Flow'
+import Debugger from 'debug'
+import { FixtureRunner } from '../../Fixture'
+import { BuyMembershipHappyCaseFixture } from '../../fixtures/membership'
+import { CreateProposalsFixture, ExpireProposalsFixture } from '../../fixtures/proposals'
+import {
+  ChangeThreadsModeFixture,
+  CreatePostsFixture,
+  DeletePostParams,
+  DeletePostsFixture,
+  PostParams,
+  PostUpdateParams,
+  ThreadModeChangeParams,
+  UpdatePostsFixture,
+} from '../../fixtures/proposalsDiscussion'
+import { Resource } from '../../Resources'
+import { ThreadId } from '../../../../../types/common'
+import { ALL_BYTES } from '../../consts'
+
+export default async function proposalsDiscussion({ api, query, lock }: FlowProps): Promise<void> {
+  const debug = Debugger('flow:proposals-discussion')
+  debug('Started')
+  api.enableDebugTxLogs()
+
+  const threadsN = 3
+  const accounts = (await api.createKeyPairs(threadsN)).map((kp) => kp.address)
+
+  const buyMembershipsFixture = new BuyMembershipHappyCaseFixture(api, query, accounts)
+  await new FixtureRunner(buyMembershipsFixture).run()
+  const memberIds = buyMembershipsFixture.getCreatedMembers()
+
+  const unlocks = await Promise.all(Array.from({ length: threadsN }, () => lock(Resource.Proposals)))
+  const createProposalFixture = new CreateProposalsFixture(
+    api,
+    query,
+    Array.from({ length: threadsN }, (v, i) => ({
+      type: 'Signal',
+      details: `Discussion test ${i}`,
+      asMember: memberIds[i],
+      title: `Discussion test proposal ${i}`,
+      description: `Proposals discussion test proposal ${i}`,
+    }))
+  )
+  await new FixtureRunner(createProposalFixture).run()
+  const proposalsIds = createProposalFixture.getCreatedProposalsIds()
+  const threadIds = await api.query.proposalsCodex.threadIdByProposalId.multi<ThreadId>(proposalsIds)
+
+  const createPostsParams: PostParams[] = threadIds.reduce(
+    (posts, threadId) =>
+      posts.concat([
+        // Standard case:
+        {
+          threadId,
+          asMember: memberIds[0],
+          metadata: { value: { text: 'Test' } },
+          editable: true,
+        },
+        // Invalid repliesTo case:
+        {
+          threadId,
+          asMember: memberIds[1],
+          metadata: { value: { text: 'Test', repliesTo: 9999 }, expectReplyFailure: true },
+          editable: true,
+        },
+        // ALL_BYTES metadata + non-editable case:
+        {
+          threadId,
+          asMember: memberIds[2],
+          metadata: { value: ALL_BYTES, expectFailure: true }, // expectFailure just means serialization failure, but the value will still be checked
+          editable: false,
+        },
+      ]),
+    [] as PostParams[]
+  )
+  const createPostsFixture = new CreatePostsFixture(api, query, createPostsParams)
+  await new FixtureRunner(createPostsFixture).runWithQueryNodeChecks()
+  const postIds = createPostsFixture.getCreatedPostsIds()
+
+  const threadModeChangesParams: ThreadModeChangeParams[] = [
+    { threadId: threadIds[0], asMember: memberIds[0], newMode: { Closed: memberIds } },
+    { threadId: threadIds[1], asMember: memberIds[1], newMode: { Closed: [memberIds[0]] } },
+    { threadId: threadIds[1], asMember: memberIds[1], newMode: 'Open' },
+  ]
+  const threadModeChanges = new ChangeThreadsModeFixture(api, query, threadModeChangesParams)
+  const threadModeChangesRunner = new FixtureRunner(threadModeChanges)
+  await threadModeChangesRunner.run()
+
+  const createPostRepliesParams: PostParams[] = createPostsParams.map((params, i) => ({
+    threadId: params.threadId,
+    asMember: memberIds[i % memberIds.length],
+    metadata: { value: { text: `Reply to post ${postIds[i].toString()}`, repliesTo: postIds[i].toNumber() } },
+  }))
+  const createRepliesFixture = new CreatePostsFixture(api, query, createPostRepliesParams)
+  const createRepliesRunner = new FixtureRunner(createRepliesFixture)
+  await createRepliesRunner.run()
+
+  const updatePostsParams: PostUpdateParams[] = [
+    { threadId: threadIds[0], postId: postIds[0], asMember: memberIds[0], newText: 'New text' },
+    { threadId: threadIds[0], postId: postIds[1], asMember: memberIds[1], newText: ALL_BYTES },
+  ]
+  const updatePostsFixture = new UpdatePostsFixture(api, query, updatePostsParams)
+  const updatePostsRunner = new FixtureRunner(updatePostsFixture)
+  await updatePostsRunner.run()
+
+  // TODO: Test anyone_can_delete_post (would require waiting PostLifetime)
+
+  const deletePostsParams: DeletePostParams[] = postIds
+    .map((postId, i) => ({ postId, ...createPostsParams[i] }))
+    .filter((p) => p.editable !== false)
+  const deletePostsFixture = new DeletePostsFixture(api, query, deletePostsParams)
+  const deletePostsRunner = new FixtureRunner(deletePostsFixture)
+  await deletePostsRunner.run()
+
+  // Run compound query-node checks
+  await Promise.all([
+    createRepliesRunner.runQueryNodeChecks(),
+    threadModeChangesRunner.runQueryNodeChecks(),
+    updatePostsRunner.runQueryNodeChecks(),
+    deletePostsRunner.runQueryNodeChecks(),
+  ])
+
+  // Wait until proposal expires and release locks
+  await new FixtureRunner(new ExpireProposalsFixture(api, query, proposalsIds)).run()
+  unlocks.forEach((unlock) => unlock())
+
+  debug('Done')
+}

+ 390 - 26
tests/integration-tests/src/graphql/generated/queries.ts

@@ -22,6 +22,7 @@ export type ForumPostFieldsFragment = {
   createdAt: any
   updatedAt?: Types.Maybe<any>
   text: string
+  isVisible: boolean
   author: { id: string }
   thread: { id: string }
   repliesTo?: Types.Maybe<{ id: string }>
@@ -37,28 +38,30 @@ export type ForumPostFieldsFragment = {
   reactions: Array<{ id: string; reaction: Types.PostReaction; member: { id: string } }>
 }
 
-export type ForumThreadWithPostsFieldsFragment = {
+export type ForumThreadWithInitialPostFragment = {
   id: string
   createdAt: any
   updatedAt?: Types.Maybe<any>
   title: string
   isSticky: boolean
+  isVisible: boolean
   author: { id: string }
   category: { id: string }
-  posts: Array<ForumPostFieldsFragment>
+  initialPost?: Types.Maybe<ForumPostFieldsFragment>
   poll?: Types.Maybe<{
     description: string
     endTime: any
     pollAlternatives: Array<{ index: number; text: string; votes: Array<{ votingMember: { id: string } }> }>
   }>
-  createdInEvent: { id: string }
+  createdInEvent: { id: string; title: string; text: string }
   status:
     | { __typename: 'ThreadStatusActive' }
     | { __typename: 'ThreadStatusLocked'; threadDeletedEvent?: Types.Maybe<{ id: string }> }
     | { __typename: 'ThreadStatusModerated'; threadModeratedEvent?: Types.Maybe<{ id: string }> }
     | { __typename: 'ThreadStatusRemoved'; threadDeletedEvent?: Types.Maybe<{ id: string }> }
-  titleUpdates: Array<{ id: string }>
+  metadataUpdates: Array<{ id: string }>
   movedInEvents: Array<{ id: string }>
+  tags: Array<{ id: string }>
 }
 
 export type GetCategoriesByIdsQueryVariables = Types.Exact<{
@@ -67,11 +70,11 @@ export type GetCategoriesByIdsQueryVariables = Types.Exact<{
 
 export type GetCategoriesByIdsQuery = { forumCategories: Array<ForumCategoryFieldsFragment> }
 
-export type GetThreadsWithPostsByIdsQueryVariables = Types.Exact<{
+export type GetThreadsWithInitialPostsByIdsQueryVariables = Types.Exact<{
   ids?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
 }>
 
-export type GetThreadsWithPostsByIdsQuery = { forumThreads: Array<ForumThreadWithPostsFieldsFragment> }
+export type GetThreadsWithInitialPostsByIdsQuery = { forumThreads: Array<ForumThreadWithInitialPostFragment> }
 
 export type GetPostsByIdsQueryVariables = Types.Exact<{
   ids?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
@@ -143,6 +146,8 @@ export type ThreadCreatedEventFieldsFragment = {
   network: Types.Network
   inExtrinsic?: Types.Maybe<string>
   indexInBlock: number
+  title: string
+  text: string
   thread: { id: string }
 }
 
@@ -152,23 +157,23 @@ export type GetThreadCreatedEventsByEventIdsQueryVariables = Types.Exact<{
 
 export type GetThreadCreatedEventsByEventIdsQuery = { threadCreatedEvents: Array<ThreadCreatedEventFieldsFragment> }
 
-export type ThreadTitleUpdatedEventFieldsFragment = {
+export type ThreadMetadataUpdatedEventFieldsFragment = {
   id: string
   createdAt: any
   inBlock: number
   network: Types.Network
   inExtrinsic?: Types.Maybe<string>
   indexInBlock: number
-  newTitle: string
+  newTitle?: Types.Maybe<string>
   thread: { id: string }
 }
 
-export type GetThreadTitleUpdatedEventsByEventIdsQueryVariables = Types.Exact<{
+export type GetThreadMetadataUpdatedEventsByEventIdsQueryVariables = Types.Exact<{
   eventIds?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
 }>
 
-export type GetThreadTitleUpdatedEventsByEventIdsQuery = {
-  threadTitleUpdatedEvents: Array<ThreadTitleUpdatedEventFieldsFragment>
+export type GetThreadMetadataUpdatedEventsByEventIdsQuery = {
+  threadMetadataUpdatedEvents: Array<ThreadMetadataUpdatedEventFieldsFragment>
 }
 
 export type VoteOnPollEventFieldsFragment = {
@@ -985,6 +990,7 @@ export type ProposalFieldsFragment = {
   councilApprovals: number
   statusSetAtBlock: number
   statusSetAtTime: any
+  isFinalized?: Types.Maybe<boolean>
   details:
     | ProposalDetailsFields_SignalProposalDetails_Fragment
     | ProposalDetailsFields_RuntimeUpgradeProposalDetails_Fragment
@@ -1034,6 +1040,12 @@ export type ProposalFieldsFragment = {
     | ProposalStatusFields_ProposalStatusCancelled_Fragment
     | ProposalStatusFields_ProposalStatusCanceledByRuntime_Fragment
   createdInEvent: { id: string; inBlock: number; inExtrinsic?: Types.Maybe<string> }
+  discussionThread: {
+    id: string
+    mode:
+      | ProposalDiscussionThreadModeFields_ProposalDiscussionThreadModeOpen_Fragment
+      | ProposalDiscussionThreadModeFields_ProposalDiscussionThreadModeClosed_Fragment
+  }
 }
 
 export type GetProposalsByIdsQueryVariables = Types.Exact<{
@@ -1042,6 +1054,158 @@ export type GetProposalsByIdsQueryVariables = Types.Exact<{
 
 export type GetProposalsByIdsQuery = { proposals: Array<ProposalFieldsFragment> }
 
+type ProposalDiscussionThreadModeFields_ProposalDiscussionThreadModeOpen_Fragment = {
+  __typename: 'ProposalDiscussionThreadModeOpen'
+}
+
+type ProposalDiscussionThreadModeFields_ProposalDiscussionThreadModeClosed_Fragment = {
+  __typename: 'ProposalDiscussionThreadModeClosed'
+  whitelist?: Types.Maybe<{ members: Array<{ id: string }> }>
+}
+
+export type ProposalDiscussionThreadModeFieldsFragment =
+  | ProposalDiscussionThreadModeFields_ProposalDiscussionThreadModeOpen_Fragment
+  | ProposalDiscussionThreadModeFields_ProposalDiscussionThreadModeClosed_Fragment
+
+type ProposalDiscussionPostStatusFields_ProposalDiscussionPostStatusActive_Fragment = {
+  __typename: 'ProposalDiscussionPostStatusActive'
+}
+
+type ProposalDiscussionPostStatusFields_ProposalDiscussionPostStatusLocked_Fragment = {
+  __typename: 'ProposalDiscussionPostStatusLocked'
+  deletedInEvent?: Types.Maybe<{ id: string }>
+}
+
+type ProposalDiscussionPostStatusFields_ProposalDiscussionPostStatusRemoved_Fragment = {
+  __typename: 'ProposalDiscussionPostStatusRemoved'
+  deletedInEvent?: Types.Maybe<{ id: string }>
+}
+
+export type ProposalDiscussionPostStatusFieldsFragment =
+  | ProposalDiscussionPostStatusFields_ProposalDiscussionPostStatusActive_Fragment
+  | ProposalDiscussionPostStatusFields_ProposalDiscussionPostStatusLocked_Fragment
+  | ProposalDiscussionPostStatusFields_ProposalDiscussionPostStatusRemoved_Fragment
+
+export type ProposalDiscussionThreadFieldsFragment = {
+  id: string
+  proposal: { id: string }
+  posts: Array<{ id: string }>
+  mode:
+    | ProposalDiscussionThreadModeFields_ProposalDiscussionThreadModeOpen_Fragment
+    | ProposalDiscussionThreadModeFields_ProposalDiscussionThreadModeClosed_Fragment
+  modeChanges: Array<{ id: string }>
+}
+
+export type GetProposalDiscussionThreadsByIdsQueryVariables = Types.Exact<{
+  ids?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetProposalDiscussionThreadsByIdsQuery = {
+  proposalDiscussionThreads: Array<ProposalDiscussionThreadFieldsFragment>
+}
+
+export type ProposalDiscussionPostFieldsFragment = {
+  id: string
+  isVisible: boolean
+  text: string
+  discussionThread: { id: string }
+  author: { id: string }
+  status:
+    | ProposalDiscussionPostStatusFields_ProposalDiscussionPostStatusActive_Fragment
+    | ProposalDiscussionPostStatusFields_ProposalDiscussionPostStatusLocked_Fragment
+    | ProposalDiscussionPostStatusFields_ProposalDiscussionPostStatusRemoved_Fragment
+  repliesTo?: Types.Maybe<{ id: string }>
+  updates: Array<{ id: string }>
+  createdInEvent: { id: string }
+}
+
+export type GetProposalDiscussionPostsByIdsQueryVariables = Types.Exact<{
+  ids?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetProposalDiscussionPostsByIdsQuery = {
+  proposalDiscussionPosts: Array<ProposalDiscussionPostFieldsFragment>
+}
+
+export type ProposalDiscussionPostCreatedEventFieldsFragment = {
+  id: string
+  createdAt: any
+  inExtrinsic?: Types.Maybe<string>
+  inBlock: number
+  network: Types.Network
+  indexInBlock: number
+  text: string
+  post: { id: string }
+}
+
+export type GetProposalDiscussionPostCreatedEventsQueryVariables = Types.Exact<{
+  eventIds?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetProposalDiscussionPostCreatedEventsQuery = {
+  proposalDiscussionPostCreatedEvents: Array<ProposalDiscussionPostCreatedEventFieldsFragment>
+}
+
+export type ProposalDiscussionPostUpdatedEventFieldsFragment = {
+  id: string
+  createdAt: any
+  inExtrinsic?: Types.Maybe<string>
+  inBlock: number
+  network: Types.Network
+  indexInBlock: number
+  text: string
+  post: { id: string }
+}
+
+export type GetProposalDiscussionPostUpdatedEventsQueryVariables = Types.Exact<{
+  eventIds?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetProposalDiscussionPostUpdatedEventsQuery = {
+  proposalDiscussionPostUpdatedEvents: Array<ProposalDiscussionPostUpdatedEventFieldsFragment>
+}
+
+export type ProposalDiscussionThreadModeChangedEventFieldsFragment = {
+  id: string
+  createdAt: any
+  inExtrinsic?: Types.Maybe<string>
+  inBlock: number
+  network: Types.Network
+  indexInBlock: number
+  thread: { id: string }
+  newMode:
+    | ProposalDiscussionThreadModeFields_ProposalDiscussionThreadModeOpen_Fragment
+    | ProposalDiscussionThreadModeFields_ProposalDiscussionThreadModeClosed_Fragment
+  actor: { id: string }
+}
+
+export type GetProposalDiscussionThreadModeChangedEventsQueryVariables = Types.Exact<{
+  eventIds?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetProposalDiscussionThreadModeChangedEventsQuery = {
+  proposalDiscussionThreadModeChangedEvents: Array<ProposalDiscussionThreadModeChangedEventFieldsFragment>
+}
+
+export type ProposalDiscussionPostDeletedEventFieldsFragment = {
+  id: string
+  createdAt: any
+  inExtrinsic?: Types.Maybe<string>
+  inBlock: number
+  network: Types.Network
+  indexInBlock: number
+  post: { id: string }
+  actor: { id: string }
+}
+
+export type GetProposalDiscussionPostDeletedEventsQueryVariables = Types.Exact<{
+  eventIds?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetProposalDiscussionPostDeletedEventsQuery = {
+  proposalDiscussionPostDeletedEvents: Array<ProposalDiscussionPostDeletedEventFieldsFragment>
+}
+
 export type ProposalCreatedEventFieldsFragment = {
   id: string
   createdAt: any
@@ -1788,6 +1952,7 @@ export const ForumPostFields = gql`
         }
       }
     }
+    isVisible
     origin {
       __typename
       ... on PostOriginThreadInitial {
@@ -1813,8 +1978,8 @@ export const ForumPostFields = gql`
     }
   }
 `
-export const ForumThreadWithPostsFields = gql`
-  fragment ForumThreadWithPostsFields on ForumThread {
+export const ForumThreadWithInitialPost = gql`
+  fragment ForumThreadWithInitialPost on ForumThread {
     id
     createdAt
     updatedAt
@@ -1825,7 +1990,7 @@ export const ForumThreadWithPostsFields = gql`
       id
     }
     title
-    posts {
+    initialPost {
       ...ForumPostFields
     }
     poll {
@@ -1844,6 +2009,8 @@ export const ForumThreadWithPostsFields = gql`
     isSticky
     createdInEvent {
       id
+      title
+      text
     }
     status {
       __typename
@@ -1863,12 +2030,16 @@ export const ForumThreadWithPostsFields = gql`
         }
       }
     }
-    titleUpdates {
+    isVisible
+    metadataUpdates {
       id
     }
     movedInEvents {
       id
     }
+    tags {
+      id
+    }
   }
   ${ForumPostFields}
 `
@@ -1926,13 +2097,15 @@ export const ThreadCreatedEventFields = gql`
     network
     inExtrinsic
     indexInBlock
+    title
+    text
     thread {
       id
     }
   }
 `
-export const ThreadTitleUpdatedEventFields = gql`
-  fragment ThreadTitleUpdatedEventFields on ThreadTitleUpdatedEvent {
+export const ThreadMetadataUpdatedEventFields = gql`
+  fragment ThreadMetadataUpdatedEventFields on ThreadMetadataUpdatedEvent {
     id
     createdAt
     inBlock
@@ -2615,6 +2788,18 @@ export const ProposalStatusFields = gql`
     }
   }
 `
+export const ProposalDiscussionThreadModeFields = gql`
+  fragment ProposalDiscussionThreadModeFields on ProposalDiscussionThreadMode {
+    __typename
+    ... on ProposalDiscussionThreadModeClosed {
+      whitelist {
+        members {
+          id
+        }
+      }
+    }
+  }
+`
 export const ProposalFields = gql`
   fragment ProposalFields on Proposal {
     id
@@ -2644,14 +2829,145 @@ export const ProposalFields = gql`
     }
     statusSetAtBlock
     statusSetAtTime
+    isFinalized
     createdInEvent {
       id
       inBlock
       inExtrinsic
     }
+    discussionThread {
+      id
+      mode {
+        ...ProposalDiscussionThreadModeFields
+      }
+    }
   }
   ${ProposalDetailsFields}
   ${ProposalStatusFields}
+  ${ProposalDiscussionThreadModeFields}
+`
+export const ProposalDiscussionThreadFields = gql`
+  fragment ProposalDiscussionThreadFields on ProposalDiscussionThread {
+    id
+    proposal {
+      id
+    }
+    posts {
+      id
+    }
+    mode {
+      ...ProposalDiscussionThreadModeFields
+    }
+    modeChanges {
+      id
+    }
+  }
+  ${ProposalDiscussionThreadModeFields}
+`
+export const ProposalDiscussionPostStatusFields = gql`
+  fragment ProposalDiscussionPostStatusFields on ProposalDiscussionPostStatus {
+    __typename
+    ... on ProposalDiscussionPostStatusLocked {
+      deletedInEvent {
+        id
+      }
+    }
+    ... on ProposalDiscussionPostStatusRemoved {
+      deletedInEvent {
+        id
+      }
+    }
+  }
+`
+export const ProposalDiscussionPostFields = gql`
+  fragment ProposalDiscussionPostFields on ProposalDiscussionPost {
+    id
+    discussionThread {
+      id
+    }
+    author {
+      id
+    }
+    status {
+      ...ProposalDiscussionPostStatusFields
+    }
+    isVisible
+    text
+    repliesTo {
+      id
+    }
+    updates {
+      id
+    }
+    createdInEvent {
+      id
+    }
+  }
+  ${ProposalDiscussionPostStatusFields}
+`
+export const ProposalDiscussionPostCreatedEventFields = gql`
+  fragment ProposalDiscussionPostCreatedEventFields on ProposalDiscussionPostCreatedEvent {
+    id
+    createdAt
+    inExtrinsic
+    inBlock
+    network
+    indexInBlock
+    post {
+      id
+    }
+    text
+  }
+`
+export const ProposalDiscussionPostUpdatedEventFields = gql`
+  fragment ProposalDiscussionPostUpdatedEventFields on ProposalDiscussionPostUpdatedEvent {
+    id
+    createdAt
+    inExtrinsic
+    inBlock
+    network
+    indexInBlock
+    post {
+      id
+    }
+    text
+  }
+`
+export const ProposalDiscussionThreadModeChangedEventFields = gql`
+  fragment ProposalDiscussionThreadModeChangedEventFields on ProposalDiscussionThreadModeChangedEvent {
+    id
+    createdAt
+    inExtrinsic
+    inBlock
+    network
+    indexInBlock
+    thread {
+      id
+    }
+    newMode {
+      ...ProposalDiscussionThreadModeFields
+    }
+    actor {
+      id
+    }
+  }
+  ${ProposalDiscussionThreadModeFields}
+`
+export const ProposalDiscussionPostDeletedEventFields = gql`
+  fragment ProposalDiscussionPostDeletedEventFields on ProposalDiscussionPostDeletedEvent {
+    id
+    createdAt
+    inExtrinsic
+    inBlock
+    network
+    indexInBlock
+    post {
+      id
+    }
+    actor {
+      id
+    }
+  }
 `
 export const ProposalCreatedEventFields = gql`
   fragment ProposalCreatedEventFields on ProposalCreatedEvent {
@@ -3308,13 +3624,13 @@ export const GetCategoriesByIds = gql`
   }
   ${ForumCategoryFields}
 `
-export const GetThreadsWithPostsByIds = gql`
-  query getThreadsWithPostsByIds($ids: [ID!]) {
+export const GetThreadsWithInitialPostsByIds = gql`
+  query getThreadsWithInitialPostsByIds($ids: [ID!]) {
     forumThreads(where: { id_in: $ids }) {
-      ...ForumThreadWithPostsFields
+      ...ForumThreadWithInitialPost
     }
   }
-  ${ForumThreadWithPostsFields}
+  ${ForumThreadWithInitialPost}
 `
 export const GetPostsByIds = gql`
   query getPostsByIds($ids: [ID!]) {
@@ -3356,13 +3672,13 @@ export const GetThreadCreatedEventsByEventIds = gql`
   }
   ${ThreadCreatedEventFields}
 `
-export const GetThreadTitleUpdatedEventsByEventIds = gql`
-  query getThreadTitleUpdatedEventsByEventIds($eventIds: [ID!]) {
-    threadTitleUpdatedEvents(where: { id_in: $eventIds }) {
-      ...ThreadTitleUpdatedEventFields
+export const GetThreadMetadataUpdatedEventsByEventIds = gql`
+  query getThreadMetadataUpdatedEventsByEventIds($eventIds: [ID!]) {
+    threadMetadataUpdatedEvents(where: { id_in: $eventIds }) {
+      ...ThreadMetadataUpdatedEventFields
     }
   }
-  ${ThreadTitleUpdatedEventFields}
+  ${ThreadMetadataUpdatedEventFields}
 `
 export const GetVoteOnPollEventsByEventIds = gql`
   query getVoteOnPollEventsByEventIds($eventIds: [ID!]) {
@@ -3588,6 +3904,54 @@ export const GetProposalsByIds = gql`
   }
   ${ProposalFields}
 `
+export const GetProposalDiscussionThreadsByIds = gql`
+  query getProposalDiscussionThreadsByIds($ids: [ID!]) {
+    proposalDiscussionThreads(where: { id_in: $ids }) {
+      ...ProposalDiscussionThreadFields
+    }
+  }
+  ${ProposalDiscussionThreadFields}
+`
+export const GetProposalDiscussionPostsByIds = gql`
+  query getProposalDiscussionPostsByIds($ids: [ID!]) {
+    proposalDiscussionPosts(where: { id_in: $ids }) {
+      ...ProposalDiscussionPostFields
+    }
+  }
+  ${ProposalDiscussionPostFields}
+`
+export const GetProposalDiscussionPostCreatedEvents = gql`
+  query getProposalDiscussionPostCreatedEvents($eventIds: [ID!]) {
+    proposalDiscussionPostCreatedEvents(where: { id_in: $eventIds }) {
+      ...ProposalDiscussionPostCreatedEventFields
+    }
+  }
+  ${ProposalDiscussionPostCreatedEventFields}
+`
+export const GetProposalDiscussionPostUpdatedEvents = gql`
+  query getProposalDiscussionPostUpdatedEvents($eventIds: [ID!]) {
+    proposalDiscussionPostUpdatedEvents(where: { id_in: $eventIds }) {
+      ...ProposalDiscussionPostUpdatedEventFields
+    }
+  }
+  ${ProposalDiscussionPostUpdatedEventFields}
+`
+export const GetProposalDiscussionThreadModeChangedEvents = gql`
+  query getProposalDiscussionThreadModeChangedEvents($eventIds: [ID!]) {
+    proposalDiscussionThreadModeChangedEvents(where: { id_in: $eventIds }) {
+      ...ProposalDiscussionThreadModeChangedEventFields
+    }
+  }
+  ${ProposalDiscussionThreadModeChangedEventFields}
+`
+export const GetProposalDiscussionPostDeletedEvents = gql`
+  query getProposalDiscussionPostDeletedEvents($eventIds: [ID!]) {
+    proposalDiscussionPostDeletedEvents(where: { id_in: $eventIds }) {
+      ...ProposalDiscussionPostDeletedEventFields
+    }
+  }
+  ${ProposalDiscussionPostDeletedEventFields}
+`
 export const GetProposalCreatedEventsByEventIds = gql`
   query getProposalCreatedEventsByEventIds($eventIds: [ID!]) {
     proposalCreatedEvents(where: { id_in: $eventIds }) {

+ 347 - 317
tests/integration-tests/src/graphql/generated/schema.ts

@@ -2913,7 +2913,6 @@ export enum EventTypeOptions {
   ProposalDiscussionPostCreatedEvent = 'ProposalDiscussionPostCreatedEvent',
   ProposalDiscussionPostDeletedEvent = 'ProposalDiscussionPostDeletedEvent',
   ProposalDiscussionPostUpdatedEvent = 'ProposalDiscussionPostUpdatedEvent',
-  ProposalDiscussionThreadCreatedEvent = 'ProposalDiscussionThreadCreatedEvent',
   ProposalDiscussionThreadModeChangedEvent = 'ProposalDiscussionThreadModeChangedEvent',
   ProposalExecutedEvent = 'ProposalExecutedEvent',
   ProposalStatusUpdatedEvent = 'ProposalStatusUpdatedEvent',
@@ -3379,6 +3378,8 @@ export type ForumPost = BaseGraphQlObject & {
   repliesToId?: Maybe<Scalars['String']>
   /** Current post status */
   status: PostStatus
+  /** True if the post is either Active or Locked */
+  isVisible: Scalars['Boolean']
   /** The origin of the post (either thread creation event or regular PostAdded event) */
   origin: PostOrigin
   edits: Array<PostTextUpdatedEvent>
@@ -3386,6 +3387,7 @@ export type ForumPost = BaseGraphQlObject & {
   deletedInEvent?: Maybe<PostDeletedEvent>
   deletedInEventId?: Maybe<Scalars['String']>
   forumpostrepliesTo?: Maybe<Array<ForumPost>>
+  forumthreadinitialPost?: Maybe<Array<ForumThread>>
   postaddedeventpost?: Maybe<Array<PostAddedEvent>>
   postmoderatedeventpost?: Maybe<Array<PostModeratedEvent>>
   postreactedeventpost?: Maybe<Array<PostReactedEvent>>
@@ -3403,6 +3405,7 @@ export type ForumPostCreateInput = {
   text: Scalars['String']
   repliesTo?: Maybe<Scalars['ID']>
   status: Scalars['JSONObject']
+  isVisible: Scalars['Boolean']
   origin: Scalars['JSONObject']
   deletedInEvent?: Maybe<Scalars['ID']>
 }
@@ -3427,6 +3430,8 @@ export enum ForumPostOrderByInput {
   TextDesc = 'text_DESC',
   RepliesToAsc = 'repliesTo_ASC',
   RepliesToDesc = 'repliesTo_DESC',
+  IsVisibleAsc = 'isVisible_ASC',
+  IsVisibleDesc = 'isVisible_DESC',
   DeletedInEventAsc = 'deletedInEvent_ASC',
   DeletedInEventDesc = 'deletedInEvent_DESC',
 }
@@ -3533,6 +3538,7 @@ export type ForumPostUpdateInput = {
   text?: Maybe<Scalars['String']>
   repliesTo?: Maybe<Scalars['ID']>
   status?: Maybe<Scalars['JSONObject']>
+  isVisible?: Maybe<Scalars['Boolean']>
   origin?: Maybe<Scalars['JSONObject']>
   deletedInEvent?: Maybe<Scalars['ID']>
 }
@@ -3574,6 +3580,8 @@ export type ForumPostWhereInput = {
   repliesTo_eq?: Maybe<Scalars['ID']>
   repliesTo_in?: Maybe<Array<Scalars['ID']>>
   status_json?: Maybe<Scalars['JSONObject']>
+  isVisible_eq?: Maybe<Scalars['Boolean']>
+  isVisible_in?: Maybe<Array<Scalars['Boolean']>>
   origin_json?: Maybe<Scalars['JSONObject']>
   deletedInEvent_eq?: Maybe<Scalars['ID']>
   deletedInEvent_in?: Maybe<Array<Scalars['ID']>>
@@ -3590,6 +3598,9 @@ export type ForumPostWhereInput = {
   forumpostrepliesTo_none?: Maybe<ForumPostWhereInput>
   forumpostrepliesTo_some?: Maybe<ForumPostWhereInput>
   forumpostrepliesTo_every?: Maybe<ForumPostWhereInput>
+  forumthreadinitialPost_none?: Maybe<ForumThreadWhereInput>
+  forumthreadinitialPost_some?: Maybe<ForumThreadWhereInput>
+  forumthreadinitialPost_every?: Maybe<ForumThreadWhereInput>
   postaddedeventpost_none?: Maybe<PostAddedEventWhereInput>
   postaddedeventpost_some?: Maybe<PostAddedEventWhereInput>
   postaddedeventpost_every?: Maybe<PostAddedEventWhereInput>
@@ -3623,15 +3634,22 @@ export type ForumThread = BaseGraphQlObject & {
   /** Thread title */
   title: Scalars['String']
   posts: Array<ForumPost>
+  initialPost?: Maybe<ForumPost>
+  initialPostId?: Maybe<Scalars['String']>
+  /** Number of non-deleted posts in the thread */
+  visiblePostsCount: Scalars['Int']
   poll?: Maybe<ForumPoll>
   /** Whether the thread is sticky in the category */
   isSticky: Scalars['Boolean']
   createdInEvent: ThreadCreatedEvent
   /** Current thread status */
   status: ThreadStatus
-  titleUpdates: Array<ThreadTitleUpdatedEvent>
+  /** True if the thread is either Active or Locked */
+  isVisible: Scalars['Boolean']
+  metadataUpdates: Array<ThreadMetadataUpdatedEvent>
   madeStickyInEvents: Array<CategoryStickyThreadUpdateEvent>
   movedInEvents: Array<ThreadMovedEvent>
+  tags: Array<ForumThreadTag>
   threaddeletedeventthread?: Maybe<Array<ThreadDeletedEvent>>
   threadmoderatedeventthread?: Maybe<Array<ThreadModeratedEvent>>
 }
@@ -3646,8 +3664,11 @@ export type ForumThreadCreateInput = {
   author: Scalars['ID']
   category: Scalars['ID']
   title: Scalars['String']
+  initialPost?: Maybe<Scalars['ID']>
+  visiblePostsCount: Scalars['Float']
   isSticky: Scalars['Boolean']
   status: Scalars['JSONObject']
+  isVisible: Scalars['Boolean']
 }
 
 export type ForumThreadEdge = {
@@ -3668,16 +3689,111 @@ export enum ForumThreadOrderByInput {
   CategoryDesc = 'category_DESC',
   TitleAsc = 'title_ASC',
   TitleDesc = 'title_DESC',
+  InitialPostAsc = 'initialPost_ASC',
+  InitialPostDesc = 'initialPost_DESC',
+  VisiblePostsCountAsc = 'visiblePostsCount_ASC',
+  VisiblePostsCountDesc = 'visiblePostsCount_DESC',
   IsStickyAsc = 'isSticky_ASC',
   IsStickyDesc = 'isSticky_DESC',
+  IsVisibleAsc = 'isVisible_ASC',
+  IsVisibleDesc = 'isVisible_DESC',
+}
+
+export type ForumThreadTag = BaseGraphQlObject & {
+  id: Scalars['ID']
+  createdAt: Scalars['DateTime']
+  createdById: Scalars['String']
+  updatedAt?: Maybe<Scalars['DateTime']>
+  updatedById?: Maybe<Scalars['String']>
+  deletedAt?: Maybe<Scalars['DateTime']>
+  deletedById?: Maybe<Scalars['String']>
+  version: Scalars['Int']
+  threads: Array<ForumThread>
+  /** Number of non-removed threads currently assigned to the tag */
+  visibleThreadsCount: Scalars['Int']
+}
+
+export type ForumThreadTagConnection = {
+  totalCount: Scalars['Int']
+  edges: Array<ForumThreadTagEdge>
+  pageInfo: PageInfo
+}
+
+export type ForumThreadTagCreateInput = {
+  visibleThreadsCount: Scalars['Float']
+}
+
+export type ForumThreadTagEdge = {
+  node: ForumThreadTag
+  cursor: Scalars['String']
+}
+
+export enum ForumThreadTagOrderByInput {
+  CreatedAtAsc = 'createdAt_ASC',
+  CreatedAtDesc = 'createdAt_DESC',
+  UpdatedAtAsc = 'updatedAt_ASC',
+  UpdatedAtDesc = 'updatedAt_DESC',
+  DeletedAtAsc = 'deletedAt_ASC',
+  DeletedAtDesc = 'deletedAt_DESC',
+  VisibleThreadsCountAsc = 'visibleThreadsCount_ASC',
+  VisibleThreadsCountDesc = 'visibleThreadsCount_DESC',
+}
+
+export type ForumThreadTagUpdateInput = {
+  visibleThreadsCount?: Maybe<Scalars['Float']>
+}
+
+export type ForumThreadTagWhereInput = {
+  id_eq?: Maybe<Scalars['ID']>
+  id_in?: Maybe<Array<Scalars['ID']>>
+  createdAt_eq?: Maybe<Scalars['DateTime']>
+  createdAt_lt?: Maybe<Scalars['DateTime']>
+  createdAt_lte?: Maybe<Scalars['DateTime']>
+  createdAt_gt?: Maybe<Scalars['DateTime']>
+  createdAt_gte?: Maybe<Scalars['DateTime']>
+  createdById_eq?: Maybe<Scalars['ID']>
+  createdById_in?: Maybe<Array<Scalars['ID']>>
+  updatedAt_eq?: Maybe<Scalars['DateTime']>
+  updatedAt_lt?: Maybe<Scalars['DateTime']>
+  updatedAt_lte?: Maybe<Scalars['DateTime']>
+  updatedAt_gt?: Maybe<Scalars['DateTime']>
+  updatedAt_gte?: Maybe<Scalars['DateTime']>
+  updatedById_eq?: Maybe<Scalars['ID']>
+  updatedById_in?: Maybe<Array<Scalars['ID']>>
+  deletedAt_all?: Maybe<Scalars['Boolean']>
+  deletedAt_eq?: Maybe<Scalars['DateTime']>
+  deletedAt_lt?: Maybe<Scalars['DateTime']>
+  deletedAt_lte?: Maybe<Scalars['DateTime']>
+  deletedAt_gt?: Maybe<Scalars['DateTime']>
+  deletedAt_gte?: Maybe<Scalars['DateTime']>
+  deletedById_eq?: Maybe<Scalars['ID']>
+  deletedById_in?: Maybe<Array<Scalars['ID']>>
+  visibleThreadsCount_eq?: Maybe<Scalars['Int']>
+  visibleThreadsCount_gt?: Maybe<Scalars['Int']>
+  visibleThreadsCount_gte?: Maybe<Scalars['Int']>
+  visibleThreadsCount_lt?: Maybe<Scalars['Int']>
+  visibleThreadsCount_lte?: Maybe<Scalars['Int']>
+  visibleThreadsCount_in?: Maybe<Array<Scalars['Int']>>
+  threads_none?: Maybe<ForumThreadWhereInput>
+  threads_some?: Maybe<ForumThreadWhereInput>
+  threads_every?: Maybe<ForumThreadWhereInput>
+  AND?: Maybe<Array<ForumThreadTagWhereInput>>
+  OR?: Maybe<Array<ForumThreadTagWhereInput>>
+}
+
+export type ForumThreadTagWhereUniqueInput = {
+  id: Scalars['ID']
 }
 
 export type ForumThreadUpdateInput = {
   author?: Maybe<Scalars['ID']>
   category?: Maybe<Scalars['ID']>
   title?: Maybe<Scalars['String']>
+  initialPost?: Maybe<Scalars['ID']>
+  visiblePostsCount?: Maybe<Scalars['Float']>
   isSticky?: Maybe<Scalars['Boolean']>
   status?: Maybe<Scalars['JSONObject']>
+  isVisible?: Maybe<Scalars['Boolean']>
 }
 
 export type ForumThreadWhereInput = {
@@ -3714,25 +3830,39 @@ export type ForumThreadWhereInput = {
   title_startsWith?: Maybe<Scalars['String']>
   title_endsWith?: Maybe<Scalars['String']>
   title_in?: Maybe<Array<Scalars['String']>>
+  initialPost_eq?: Maybe<Scalars['ID']>
+  initialPost_in?: Maybe<Array<Scalars['ID']>>
+  visiblePostsCount_eq?: Maybe<Scalars['Int']>
+  visiblePostsCount_gt?: Maybe<Scalars['Int']>
+  visiblePostsCount_gte?: Maybe<Scalars['Int']>
+  visiblePostsCount_lt?: Maybe<Scalars['Int']>
+  visiblePostsCount_lte?: Maybe<Scalars['Int']>
+  visiblePostsCount_in?: Maybe<Array<Scalars['Int']>>
   isSticky_eq?: Maybe<Scalars['Boolean']>
   isSticky_in?: Maybe<Array<Scalars['Boolean']>>
   status_json?: Maybe<Scalars['JSONObject']>
+  isVisible_eq?: Maybe<Scalars['Boolean']>
+  isVisible_in?: Maybe<Array<Scalars['Boolean']>>
   author?: Maybe<MembershipWhereInput>
   category?: Maybe<ForumCategoryWhereInput>
   posts_none?: Maybe<ForumPostWhereInput>
   posts_some?: Maybe<ForumPostWhereInput>
   posts_every?: Maybe<ForumPostWhereInput>
+  initialPost?: Maybe<ForumPostWhereInput>
   poll?: Maybe<ForumPollWhereInput>
   createdInEvent?: Maybe<ThreadCreatedEventWhereInput>
-  titleUpdates_none?: Maybe<ThreadTitleUpdatedEventWhereInput>
-  titleUpdates_some?: Maybe<ThreadTitleUpdatedEventWhereInput>
-  titleUpdates_every?: Maybe<ThreadTitleUpdatedEventWhereInput>
+  metadataUpdates_none?: Maybe<ThreadMetadataUpdatedEventWhereInput>
+  metadataUpdates_some?: Maybe<ThreadMetadataUpdatedEventWhereInput>
+  metadataUpdates_every?: Maybe<ThreadMetadataUpdatedEventWhereInput>
   madeStickyInEvents_none?: Maybe<CategoryStickyThreadUpdateEventWhereInput>
   madeStickyInEvents_some?: Maybe<CategoryStickyThreadUpdateEventWhereInput>
   madeStickyInEvents_every?: Maybe<CategoryStickyThreadUpdateEventWhereInput>
   movedInEvents_none?: Maybe<ThreadMovedEventWhereInput>
   movedInEvents_some?: Maybe<ThreadMovedEventWhereInput>
   movedInEvents_every?: Maybe<ThreadMovedEventWhereInput>
+  tags_none?: Maybe<ForumThreadTagWhereInput>
+  tags_some?: Maybe<ForumThreadTagWhereInput>
+  tags_every?: Maybe<ForumThreadTagWhereInput>
   threaddeletedeventthread_none?: Maybe<ThreadDeletedEventWhereInput>
   threaddeletedeventthread_some?: Maybe<ThreadDeletedEventWhereInput>
   threaddeletedeventthread_every?: Maybe<ThreadDeletedEventWhereInput>
@@ -8178,6 +8308,8 @@ export type Proposal = BaseGraphQlObject & {
   votes: Array<ProposalVotedEvent>
   /** Current proposal status */
   status: ProposalStatus
+  /** If true then the proposal status is final and will not change form this point */
+  isFinalized?: Maybe<Scalars['Boolean']>
   /** Number of the block the current status was set at */
   statusSetAtBlock: Scalars['Int']
   /** Time the current status was set at (based on block timestamp) */
@@ -8452,6 +8584,7 @@ export type ProposalCreateInput = {
   exactExecutionBlock?: Maybe<Scalars['Float']>
   councilApprovals: Scalars['Float']
   status: Scalars['JSONObject']
+  isFinalized?: Maybe<Scalars['Boolean']>
   statusSetAtBlock: Scalars['Float']
   statusSetAtTime: Scalars['DateTime']
 }
@@ -8632,10 +8765,14 @@ export type ProposalDiscussionPost = BaseGraphQlObject & {
   deletedAt?: Maybe<Scalars['DateTime']>
   deletedById?: Maybe<Scalars['String']>
   version: Scalars['Int']
+  discussionThread: ProposalDiscussionThread
+  discussionThreadId: Scalars['String']
   author: Membership
   authorId: Scalars['String']
   /** Current post status */
   status: ProposalDiscussionPostStatus
+  /** True if the post is either Active or Locked */
+  isVisible: Scalars['Boolean']
   /** Post's md-formatted text */
   text: Scalars['String']
   repliesTo?: Maybe<ProposalDiscussionPost>
@@ -8789,8 +8926,10 @@ export type ProposalDiscussionPostCreatedEventWhereUniqueInput = {
 }
 
 export type ProposalDiscussionPostCreateInput = {
+  discussionThread: Scalars['ID']
   author: Scalars['ID']
   status: Scalars['JSONObject']
+  isVisible: Scalars['Boolean']
   text: Scalars['String']
   repliesTo?: Maybe<Scalars['ID']>
 }
@@ -8941,8 +9080,12 @@ export enum ProposalDiscussionPostOrderByInput {
   UpdatedAtDesc = 'updatedAt_DESC',
   DeletedAtAsc = 'deletedAt_ASC',
   DeletedAtDesc = 'deletedAt_DESC',
+  DiscussionThreadAsc = 'discussionThread_ASC',
+  DiscussionThreadDesc = 'discussionThread_DESC',
   AuthorAsc = 'author_ASC',
   AuthorDesc = 'author_DESC',
+  IsVisibleAsc = 'isVisible_ASC',
+  IsVisibleDesc = 'isVisible_DESC',
   TextAsc = 'text_ASC',
   TextDesc = 'text_DESC',
   RepliesToAsc = 'repliesTo_ASC',
@@ -9152,8 +9295,10 @@ export type ProposalDiscussionPostUpdatedEventWhereUniqueInput = {
 }
 
 export type ProposalDiscussionPostUpdateInput = {
+  discussionThread?: Maybe<Scalars['ID']>
   author?: Maybe<Scalars['ID']>
   status?: Maybe<Scalars['JSONObject']>
+  isVisible?: Maybe<Scalars['Boolean']>
   text?: Maybe<Scalars['String']>
   repliesTo?: Maybe<Scalars['ID']>
 }
@@ -9183,9 +9328,13 @@ export type ProposalDiscussionPostWhereInput = {
   deletedAt_gte?: Maybe<Scalars['DateTime']>
   deletedById_eq?: Maybe<Scalars['ID']>
   deletedById_in?: Maybe<Array<Scalars['ID']>>
+  discussionThread_eq?: Maybe<Scalars['ID']>
+  discussionThread_in?: Maybe<Array<Scalars['ID']>>
   author_eq?: Maybe<Scalars['ID']>
   author_in?: Maybe<Array<Scalars['ID']>>
   status_json?: Maybe<Scalars['JSONObject']>
+  isVisible_eq?: Maybe<Scalars['Boolean']>
+  isVisible_in?: Maybe<Array<Scalars['Boolean']>>
   text_eq?: Maybe<Scalars['String']>
   text_contains?: Maybe<Scalars['String']>
   text_startsWith?: Maybe<Scalars['String']>
@@ -9193,6 +9342,7 @@ export type ProposalDiscussionPostWhereInput = {
   text_in?: Maybe<Array<Scalars['String']>>
   repliesTo_eq?: Maybe<Scalars['ID']>
   repliesTo_in?: Maybe<Array<Scalars['ID']>>
+  discussionThread?: Maybe<ProposalDiscussionThreadWhereInput>
   author?: Maybe<MembershipWhereInput>
   repliesTo?: Maybe<ProposalDiscussionPostWhereInput>
   updates_none?: Maybe<ProposalDiscussionPostUpdatedEventWhereInput>
@@ -9224,10 +9374,10 @@ export type ProposalDiscussionThread = BaseGraphQlObject & {
   version: Scalars['Int']
   proposal: Proposal
   proposalId: Scalars['String']
+  posts: Array<ProposalDiscussionPost>
   /** Current thread mode */
   mode: ProposalDiscussionThreadMode
-  proposaldiscussionthreadcreatedeventthread?: Maybe<Array<ProposalDiscussionThreadCreatedEvent>>
-  proposaldiscussionthreadmodechangedeventthread?: Maybe<Array<ProposalDiscussionThreadModeChangedEvent>>
+  modeChanges: Array<ProposalDiscussionThreadModeChangedEvent>
 }
 
 export type ProposalDiscussionThreadConnection = {
@@ -9236,131 +9386,6 @@ export type ProposalDiscussionThreadConnection = {
   pageInfo: PageInfo
 }
 
-export type ProposalDiscussionThreadCreatedEvent = Event &
-  BaseGraphQlObject & {
-    /** Hash of the extrinsic which caused the event to be emitted */
-    inExtrinsic?: Maybe<Scalars['String']>
-    /** Blocknumber of the block in which the event was emitted. */
-    inBlock: Scalars['Int']
-    /** Network the block was produced in */
-    network: Network
-    /** Index of event in block from which it was emitted. */
-    indexInBlock: Scalars['Int']
-    /** Filtering options for interface implementers */
-    type?: Maybe<EventTypeOptions>
-    id: Scalars['ID']
-    createdAt: Scalars['DateTime']
-    createdById: Scalars['String']
-    updatedAt?: Maybe<Scalars['DateTime']>
-    updatedById?: Maybe<Scalars['String']>
-    deletedAt?: Maybe<Scalars['DateTime']>
-    deletedById?: Maybe<Scalars['String']>
-    version: Scalars['Int']
-    thread: ProposalDiscussionThread
-    threadId: Scalars['String']
-  }
-
-export type ProposalDiscussionThreadCreatedEventConnection = {
-  totalCount: Scalars['Int']
-  edges: Array<ProposalDiscussionThreadCreatedEventEdge>
-  pageInfo: PageInfo
-}
-
-export type ProposalDiscussionThreadCreatedEventCreateInput = {
-  inExtrinsic?: Maybe<Scalars['String']>
-  inBlock: Scalars['Float']
-  network: Network
-  indexInBlock: Scalars['Float']
-  thread: Scalars['ID']
-}
-
-export type ProposalDiscussionThreadCreatedEventEdge = {
-  node: ProposalDiscussionThreadCreatedEvent
-  cursor: Scalars['String']
-}
-
-export enum ProposalDiscussionThreadCreatedEventOrderByInput {
-  CreatedAtAsc = 'createdAt_ASC',
-  CreatedAtDesc = 'createdAt_DESC',
-  UpdatedAtAsc = 'updatedAt_ASC',
-  UpdatedAtDesc = 'updatedAt_DESC',
-  DeletedAtAsc = 'deletedAt_ASC',
-  DeletedAtDesc = 'deletedAt_DESC',
-  InExtrinsicAsc = 'inExtrinsic_ASC',
-  InExtrinsicDesc = 'inExtrinsic_DESC',
-  InBlockAsc = 'inBlock_ASC',
-  InBlockDesc = 'inBlock_DESC',
-  NetworkAsc = 'network_ASC',
-  NetworkDesc = 'network_DESC',
-  IndexInBlockAsc = 'indexInBlock_ASC',
-  IndexInBlockDesc = 'indexInBlock_DESC',
-  ThreadAsc = 'thread_ASC',
-  ThreadDesc = 'thread_DESC',
-}
-
-export type ProposalDiscussionThreadCreatedEventUpdateInput = {
-  inExtrinsic?: Maybe<Scalars['String']>
-  inBlock?: Maybe<Scalars['Float']>
-  network?: Maybe<Network>
-  indexInBlock?: Maybe<Scalars['Float']>
-  thread?: Maybe<Scalars['ID']>
-}
-
-export type ProposalDiscussionThreadCreatedEventWhereInput = {
-  id_eq?: Maybe<Scalars['ID']>
-  id_in?: Maybe<Array<Scalars['ID']>>
-  createdAt_eq?: Maybe<Scalars['DateTime']>
-  createdAt_lt?: Maybe<Scalars['DateTime']>
-  createdAt_lte?: Maybe<Scalars['DateTime']>
-  createdAt_gt?: Maybe<Scalars['DateTime']>
-  createdAt_gte?: Maybe<Scalars['DateTime']>
-  createdById_eq?: Maybe<Scalars['ID']>
-  createdById_in?: Maybe<Array<Scalars['ID']>>
-  updatedAt_eq?: Maybe<Scalars['DateTime']>
-  updatedAt_lt?: Maybe<Scalars['DateTime']>
-  updatedAt_lte?: Maybe<Scalars['DateTime']>
-  updatedAt_gt?: Maybe<Scalars['DateTime']>
-  updatedAt_gte?: Maybe<Scalars['DateTime']>
-  updatedById_eq?: Maybe<Scalars['ID']>
-  updatedById_in?: Maybe<Array<Scalars['ID']>>
-  deletedAt_all?: Maybe<Scalars['Boolean']>
-  deletedAt_eq?: Maybe<Scalars['DateTime']>
-  deletedAt_lt?: Maybe<Scalars['DateTime']>
-  deletedAt_lte?: Maybe<Scalars['DateTime']>
-  deletedAt_gt?: Maybe<Scalars['DateTime']>
-  deletedAt_gte?: Maybe<Scalars['DateTime']>
-  deletedById_eq?: Maybe<Scalars['ID']>
-  deletedById_in?: Maybe<Array<Scalars['ID']>>
-  inExtrinsic_eq?: Maybe<Scalars['String']>
-  inExtrinsic_contains?: Maybe<Scalars['String']>
-  inExtrinsic_startsWith?: Maybe<Scalars['String']>
-  inExtrinsic_endsWith?: Maybe<Scalars['String']>
-  inExtrinsic_in?: Maybe<Array<Scalars['String']>>
-  inBlock_eq?: Maybe<Scalars['Int']>
-  inBlock_gt?: Maybe<Scalars['Int']>
-  inBlock_gte?: Maybe<Scalars['Int']>
-  inBlock_lt?: Maybe<Scalars['Int']>
-  inBlock_lte?: Maybe<Scalars['Int']>
-  inBlock_in?: Maybe<Array<Scalars['Int']>>
-  network_eq?: Maybe<Network>
-  network_in?: Maybe<Array<Network>>
-  indexInBlock_eq?: Maybe<Scalars['Int']>
-  indexInBlock_gt?: Maybe<Scalars['Int']>
-  indexInBlock_gte?: Maybe<Scalars['Int']>
-  indexInBlock_lt?: Maybe<Scalars['Int']>
-  indexInBlock_lte?: Maybe<Scalars['Int']>
-  indexInBlock_in?: Maybe<Array<Scalars['Int']>>
-  thread_eq?: Maybe<Scalars['ID']>
-  thread_in?: Maybe<Array<Scalars['ID']>>
-  thread?: Maybe<ProposalDiscussionThreadWhereInput>
-  AND?: Maybe<Array<ProposalDiscussionThreadCreatedEventWhereInput>>
-  OR?: Maybe<Array<ProposalDiscussionThreadCreatedEventWhereInput>>
-}
-
-export type ProposalDiscussionThreadCreatedEventWhereUniqueInput = {
-  id: Scalars['ID']
-}
-
 export type ProposalDiscussionThreadCreateInput = {
   proposal: Scalars['ID']
   mode: Scalars['JSONObject']
@@ -9613,12 +9638,12 @@ export type ProposalDiscussionThreadWhereInput = {
   proposal_in?: Maybe<Array<Scalars['ID']>>
   mode_json?: Maybe<Scalars['JSONObject']>
   proposal?: Maybe<ProposalWhereInput>
-  proposaldiscussionthreadcreatedeventthread_none?: Maybe<ProposalDiscussionThreadCreatedEventWhereInput>
-  proposaldiscussionthreadcreatedeventthread_some?: Maybe<ProposalDiscussionThreadCreatedEventWhereInput>
-  proposaldiscussionthreadcreatedeventthread_every?: Maybe<ProposalDiscussionThreadCreatedEventWhereInput>
-  proposaldiscussionthreadmodechangedeventthread_none?: Maybe<ProposalDiscussionThreadModeChangedEventWhereInput>
-  proposaldiscussionthreadmodechangedeventthread_some?: Maybe<ProposalDiscussionThreadModeChangedEventWhereInput>
-  proposaldiscussionthreadmodechangedeventthread_every?: Maybe<ProposalDiscussionThreadModeChangedEventWhereInput>
+  posts_none?: Maybe<ProposalDiscussionPostWhereInput>
+  posts_some?: Maybe<ProposalDiscussionPostWhereInput>
+  posts_every?: Maybe<ProposalDiscussionPostWhereInput>
+  modeChanges_none?: Maybe<ProposalDiscussionThreadModeChangedEventWhereInput>
+  modeChanges_some?: Maybe<ProposalDiscussionThreadModeChangedEventWhereInput>
+  modeChanges_every?: Maybe<ProposalDiscussionThreadModeChangedEventWhereInput>
   AND?: Maybe<Array<ProposalDiscussionThreadWhereInput>>
   OR?: Maybe<Array<ProposalDiscussionThreadWhereInput>>
 }
@@ -9870,6 +9895,8 @@ export enum ProposalOrderByInput {
   ExactExecutionBlockDesc = 'exactExecutionBlock_DESC',
   CouncilApprovalsAsc = 'councilApprovals_ASC',
   CouncilApprovalsDesc = 'councilApprovals_DESC',
+  IsFinalizedAsc = 'isFinalized_ASC',
+  IsFinalizedDesc = 'isFinalized_DESC',
   StatusSetAtBlockAsc = 'statusSetAtBlock_ASC',
   StatusSetAtBlockDesc = 'statusSetAtBlock_DESC',
   StatusSetAtTimeAsc = 'statusSetAtTime_ASC',
@@ -10149,6 +10176,7 @@ export type ProposalUpdateInput = {
   exactExecutionBlock?: Maybe<Scalars['Float']>
   councilApprovals?: Maybe<Scalars['Float']>
   status?: Maybe<Scalars['JSONObject']>
+  isFinalized?: Maybe<Scalars['Boolean']>
   statusSetAtBlock?: Maybe<Scalars['Float']>
   statusSetAtTime?: Maybe<Scalars['DateTime']>
 }
@@ -10385,6 +10413,8 @@ export type ProposalWhereInput = {
   councilApprovals_lte?: Maybe<Scalars['Int']>
   councilApprovals_in?: Maybe<Array<Scalars['Int']>>
   status_json?: Maybe<Scalars['JSONObject']>
+  isFinalized_eq?: Maybe<Scalars['Boolean']>
+  isFinalized_in?: Maybe<Array<Scalars['Boolean']>>
   statusSetAtBlock_eq?: Maybe<Scalars['Int']>
   statusSetAtBlock_gt?: Maybe<Scalars['Int']>
   statusSetAtBlock_gte?: Maybe<Scalars['Int']>
@@ -10484,6 +10514,9 @@ export type Query = {
   forumPosts: Array<ForumPost>
   forumPostByUniqueInput?: Maybe<ForumPost>
   forumPostsConnection: ForumPostConnection
+  forumThreadTags: Array<ForumThreadTag>
+  forumThreadTagByUniqueInput?: Maybe<ForumThreadTag>
+  forumThreadTagsConnection: ForumThreadTagConnection
   forumThreads: Array<ForumThread>
   forumThreadByUniqueInput?: Maybe<ForumThread>
   forumThreadsConnection: ForumThreadConnection
@@ -10592,9 +10625,6 @@ export type Query = {
   proposalDiscussionPosts: Array<ProposalDiscussionPost>
   proposalDiscussionPostByUniqueInput?: Maybe<ProposalDiscussionPost>
   proposalDiscussionPostsConnection: ProposalDiscussionPostConnection
-  proposalDiscussionThreadCreatedEvents: Array<ProposalDiscussionThreadCreatedEvent>
-  proposalDiscussionThreadCreatedEventByUniqueInput?: Maybe<ProposalDiscussionThreadCreatedEvent>
-  proposalDiscussionThreadCreatedEventsConnection: ProposalDiscussionThreadCreatedEventConnection
   proposalDiscussionThreadModeChangedEvents: Array<ProposalDiscussionThreadModeChangedEvent>
   proposalDiscussionThreadModeChangedEventByUniqueInput?: Maybe<ProposalDiscussionThreadModeChangedEvent>
   proposalDiscussionThreadModeChangedEventsConnection: ProposalDiscussionThreadModeChangedEventConnection
@@ -10666,15 +10696,15 @@ export type Query = {
   threadDeletedEvents: Array<ThreadDeletedEvent>
   threadDeletedEventByUniqueInput?: Maybe<ThreadDeletedEvent>
   threadDeletedEventsConnection: ThreadDeletedEventConnection
+  threadMetadataUpdatedEvents: Array<ThreadMetadataUpdatedEvent>
+  threadMetadataUpdatedEventByUniqueInput?: Maybe<ThreadMetadataUpdatedEvent>
+  threadMetadataUpdatedEventsConnection: ThreadMetadataUpdatedEventConnection
   threadModeratedEvents: Array<ThreadModeratedEvent>
   threadModeratedEventByUniqueInput?: Maybe<ThreadModeratedEvent>
   threadModeratedEventsConnection: ThreadModeratedEventConnection
   threadMovedEvents: Array<ThreadMovedEvent>
   threadMovedEventByUniqueInput?: Maybe<ThreadMovedEvent>
   threadMovedEventsConnection: ThreadMovedEventConnection
-  threadTitleUpdatedEvents: Array<ThreadTitleUpdatedEvent>
-  threadTitleUpdatedEventByUniqueInput?: Maybe<ThreadTitleUpdatedEvent>
-  threadTitleUpdatedEventsConnection: ThreadTitleUpdatedEventConnection
   upcomingWorkingGroupOpenings: Array<UpcomingWorkingGroupOpening>
   upcomingWorkingGroupOpeningByUniqueInput?: Maybe<UpcomingWorkingGroupOpening>
   upcomingWorkingGroupOpeningsConnection: UpcomingWorkingGroupOpeningConnection
@@ -11135,6 +11165,26 @@ export type QueryForumPostsConnectionArgs = {
   orderBy?: Maybe<Array<ForumPostOrderByInput>>
 }
 
+export type QueryForumThreadTagsArgs = {
+  offset?: Maybe<Scalars['Int']>
+  limit?: Maybe<Scalars['Int']>
+  where?: Maybe<ForumThreadTagWhereInput>
+  orderBy?: Maybe<Array<ForumThreadTagOrderByInput>>
+}
+
+export type QueryForumThreadTagByUniqueInputArgs = {
+  where: ForumThreadTagWhereUniqueInput
+}
+
+export type QueryForumThreadTagsConnectionArgs = {
+  first?: Maybe<Scalars['Int']>
+  after?: Maybe<Scalars['String']>
+  last?: Maybe<Scalars['Int']>
+  before?: Maybe<Scalars['String']>
+  where?: Maybe<ForumThreadTagWhereInput>
+  orderBy?: Maybe<Array<ForumThreadTagOrderByInput>>
+}
+
 export type QueryForumThreadsArgs = {
   offset?: Maybe<Scalars['Int']>
   limit?: Maybe<Scalars['Int']>
@@ -11855,26 +11905,6 @@ export type QueryProposalDiscussionPostsConnectionArgs = {
   orderBy?: Maybe<Array<ProposalDiscussionPostOrderByInput>>
 }
 
-export type QueryProposalDiscussionThreadCreatedEventsArgs = {
-  offset?: Maybe<Scalars['Int']>
-  limit?: Maybe<Scalars['Int']>
-  where?: Maybe<ProposalDiscussionThreadCreatedEventWhereInput>
-  orderBy?: Maybe<Array<ProposalDiscussionThreadCreatedEventOrderByInput>>
-}
-
-export type QueryProposalDiscussionThreadCreatedEventByUniqueInputArgs = {
-  where: ProposalDiscussionThreadCreatedEventWhereUniqueInput
-}
-
-export type QueryProposalDiscussionThreadCreatedEventsConnectionArgs = {
-  first?: Maybe<Scalars['Int']>
-  after?: Maybe<Scalars['String']>
-  last?: Maybe<Scalars['Int']>
-  before?: Maybe<Scalars['String']>
-  where?: Maybe<ProposalDiscussionThreadCreatedEventWhereInput>
-  orderBy?: Maybe<Array<ProposalDiscussionThreadCreatedEventOrderByInput>>
-}
-
 export type QueryProposalDiscussionThreadModeChangedEventsArgs = {
   offset?: Maybe<Scalars['Int']>
   limit?: Maybe<Scalars['Int']>
@@ -12352,6 +12382,26 @@ export type QueryThreadDeletedEventsConnectionArgs = {
   orderBy?: Maybe<Array<ThreadDeletedEventOrderByInput>>
 }
 
+export type QueryThreadMetadataUpdatedEventsArgs = {
+  offset?: Maybe<Scalars['Int']>
+  limit?: Maybe<Scalars['Int']>
+  where?: Maybe<ThreadMetadataUpdatedEventWhereInput>
+  orderBy?: Maybe<Array<ThreadMetadataUpdatedEventOrderByInput>>
+}
+
+export type QueryThreadMetadataUpdatedEventByUniqueInputArgs = {
+  where: ThreadMetadataUpdatedEventWhereUniqueInput
+}
+
+export type QueryThreadMetadataUpdatedEventsConnectionArgs = {
+  first?: Maybe<Scalars['Int']>
+  after?: Maybe<Scalars['String']>
+  last?: Maybe<Scalars['Int']>
+  before?: Maybe<Scalars['String']>
+  where?: Maybe<ThreadMetadataUpdatedEventWhereInput>
+  orderBy?: Maybe<Array<ThreadMetadataUpdatedEventOrderByInput>>
+}
+
 export type QueryThreadModeratedEventsArgs = {
   offset?: Maybe<Scalars['Int']>
   limit?: Maybe<Scalars['Int']>
@@ -12392,26 +12442,6 @@ export type QueryThreadMovedEventsConnectionArgs = {
   orderBy?: Maybe<Array<ThreadMovedEventOrderByInput>>
 }
 
-export type QueryThreadTitleUpdatedEventsArgs = {
-  offset?: Maybe<Scalars['Int']>
-  limit?: Maybe<Scalars['Int']>
-  where?: Maybe<ThreadTitleUpdatedEventWhereInput>
-  orderBy?: Maybe<Array<ThreadTitleUpdatedEventOrderByInput>>
-}
-
-export type QueryThreadTitleUpdatedEventByUniqueInputArgs = {
-  where: ThreadTitleUpdatedEventWhereUniqueInput
-}
-
-export type QueryThreadTitleUpdatedEventsConnectionArgs = {
-  first?: Maybe<Scalars['Int']>
-  after?: Maybe<Scalars['String']>
-  last?: Maybe<Scalars['Int']>
-  before?: Maybe<Scalars['String']>
-  where?: Maybe<ThreadTitleUpdatedEventWhereInput>
-  orderBy?: Maybe<Array<ThreadTitleUpdatedEventOrderByInput>>
-}
-
 export type QueryUpcomingWorkingGroupOpeningsArgs = {
   offset?: Maybe<Scalars['Int']>
   limit?: Maybe<Scalars['Int']>
@@ -15377,6 +15407,139 @@ export type ThreadDeletedEventWhereUniqueInput = {
   id: Scalars['ID']
 }
 
+export type ThreadMetadataUpdatedEvent = BaseGraphQlObject & {
+  id: Scalars['ID']
+  createdAt: Scalars['DateTime']
+  createdById: Scalars['String']
+  updatedAt?: Maybe<Scalars['DateTime']>
+  updatedById?: Maybe<Scalars['String']>
+  deletedAt?: Maybe<Scalars['DateTime']>
+  deletedById?: Maybe<Scalars['String']>
+  version: Scalars['Int']
+  /** Hash of the extrinsic which caused the event to be emitted */
+  inExtrinsic?: Maybe<Scalars['String']>
+  /** Blocknumber of the block in which the event was emitted. */
+  inBlock: Scalars['Int']
+  /** Network the block was produced in */
+  network: Network
+  /** Index of event in block from which it was emitted. */
+  indexInBlock: Scalars['Int']
+  thread: ForumThread
+  threadId: Scalars['String']
+  /** New title of the thread */
+  newTitle?: Maybe<Scalars['String']>
+}
+
+export type ThreadMetadataUpdatedEventConnection = {
+  totalCount: Scalars['Int']
+  edges: Array<ThreadMetadataUpdatedEventEdge>
+  pageInfo: PageInfo
+}
+
+export type ThreadMetadataUpdatedEventCreateInput = {
+  inExtrinsic?: Maybe<Scalars['String']>
+  inBlock: Scalars['Float']
+  network: Network
+  indexInBlock: Scalars['Float']
+  thread: Scalars['ID']
+  newTitle?: Maybe<Scalars['String']>
+}
+
+export type ThreadMetadataUpdatedEventEdge = {
+  node: ThreadMetadataUpdatedEvent
+  cursor: Scalars['String']
+}
+
+export enum ThreadMetadataUpdatedEventOrderByInput {
+  CreatedAtAsc = 'createdAt_ASC',
+  CreatedAtDesc = 'createdAt_DESC',
+  UpdatedAtAsc = 'updatedAt_ASC',
+  UpdatedAtDesc = 'updatedAt_DESC',
+  DeletedAtAsc = 'deletedAt_ASC',
+  DeletedAtDesc = 'deletedAt_DESC',
+  InExtrinsicAsc = 'inExtrinsic_ASC',
+  InExtrinsicDesc = 'inExtrinsic_DESC',
+  InBlockAsc = 'inBlock_ASC',
+  InBlockDesc = 'inBlock_DESC',
+  NetworkAsc = 'network_ASC',
+  NetworkDesc = 'network_DESC',
+  IndexInBlockAsc = 'indexInBlock_ASC',
+  IndexInBlockDesc = 'indexInBlock_DESC',
+  ThreadAsc = 'thread_ASC',
+  ThreadDesc = 'thread_DESC',
+  NewTitleAsc = 'newTitle_ASC',
+  NewTitleDesc = 'newTitle_DESC',
+}
+
+export type ThreadMetadataUpdatedEventUpdateInput = {
+  inExtrinsic?: Maybe<Scalars['String']>
+  inBlock?: Maybe<Scalars['Float']>
+  network?: Maybe<Network>
+  indexInBlock?: Maybe<Scalars['Float']>
+  thread?: Maybe<Scalars['ID']>
+  newTitle?: Maybe<Scalars['String']>
+}
+
+export type ThreadMetadataUpdatedEventWhereInput = {
+  id_eq?: Maybe<Scalars['ID']>
+  id_in?: Maybe<Array<Scalars['ID']>>
+  createdAt_eq?: Maybe<Scalars['DateTime']>
+  createdAt_lt?: Maybe<Scalars['DateTime']>
+  createdAt_lte?: Maybe<Scalars['DateTime']>
+  createdAt_gt?: Maybe<Scalars['DateTime']>
+  createdAt_gte?: Maybe<Scalars['DateTime']>
+  createdById_eq?: Maybe<Scalars['ID']>
+  createdById_in?: Maybe<Array<Scalars['ID']>>
+  updatedAt_eq?: Maybe<Scalars['DateTime']>
+  updatedAt_lt?: Maybe<Scalars['DateTime']>
+  updatedAt_lte?: Maybe<Scalars['DateTime']>
+  updatedAt_gt?: Maybe<Scalars['DateTime']>
+  updatedAt_gte?: Maybe<Scalars['DateTime']>
+  updatedById_eq?: Maybe<Scalars['ID']>
+  updatedById_in?: Maybe<Array<Scalars['ID']>>
+  deletedAt_all?: Maybe<Scalars['Boolean']>
+  deletedAt_eq?: Maybe<Scalars['DateTime']>
+  deletedAt_lt?: Maybe<Scalars['DateTime']>
+  deletedAt_lte?: Maybe<Scalars['DateTime']>
+  deletedAt_gt?: Maybe<Scalars['DateTime']>
+  deletedAt_gte?: Maybe<Scalars['DateTime']>
+  deletedById_eq?: Maybe<Scalars['ID']>
+  deletedById_in?: Maybe<Array<Scalars['ID']>>
+  inExtrinsic_eq?: Maybe<Scalars['String']>
+  inExtrinsic_contains?: Maybe<Scalars['String']>
+  inExtrinsic_startsWith?: Maybe<Scalars['String']>
+  inExtrinsic_endsWith?: Maybe<Scalars['String']>
+  inExtrinsic_in?: Maybe<Array<Scalars['String']>>
+  inBlock_eq?: Maybe<Scalars['Int']>
+  inBlock_gt?: Maybe<Scalars['Int']>
+  inBlock_gte?: Maybe<Scalars['Int']>
+  inBlock_lt?: Maybe<Scalars['Int']>
+  inBlock_lte?: Maybe<Scalars['Int']>
+  inBlock_in?: Maybe<Array<Scalars['Int']>>
+  network_eq?: Maybe<Network>
+  network_in?: Maybe<Array<Network>>
+  indexInBlock_eq?: Maybe<Scalars['Int']>
+  indexInBlock_gt?: Maybe<Scalars['Int']>
+  indexInBlock_gte?: Maybe<Scalars['Int']>
+  indexInBlock_lt?: Maybe<Scalars['Int']>
+  indexInBlock_lte?: Maybe<Scalars['Int']>
+  indexInBlock_in?: Maybe<Array<Scalars['Int']>>
+  thread_eq?: Maybe<Scalars['ID']>
+  thread_in?: Maybe<Array<Scalars['ID']>>
+  newTitle_eq?: Maybe<Scalars['String']>
+  newTitle_contains?: Maybe<Scalars['String']>
+  newTitle_startsWith?: Maybe<Scalars['String']>
+  newTitle_endsWith?: Maybe<Scalars['String']>
+  newTitle_in?: Maybe<Array<Scalars['String']>>
+  thread?: Maybe<ForumThreadWhereInput>
+  AND?: Maybe<Array<ThreadMetadataUpdatedEventWhereInput>>
+  OR?: Maybe<Array<ThreadMetadataUpdatedEventWhereInput>>
+}
+
+export type ThreadMetadataUpdatedEventWhereUniqueInput = {
+  id: Scalars['ID']
+}
+
 export type ThreadModeratedEvent = BaseGraphQlObject & {
   id: Scalars['ID']
   createdAt: Scalars['DateTime']
@@ -15745,139 +15908,6 @@ export type ThreadStatusRemoved = {
   threadDeletedEvent?: Maybe<ThreadDeletedEvent>
 }
 
-export type ThreadTitleUpdatedEvent = BaseGraphQlObject & {
-  id: Scalars['ID']
-  createdAt: Scalars['DateTime']
-  createdById: Scalars['String']
-  updatedAt?: Maybe<Scalars['DateTime']>
-  updatedById?: Maybe<Scalars['String']>
-  deletedAt?: Maybe<Scalars['DateTime']>
-  deletedById?: Maybe<Scalars['String']>
-  version: Scalars['Int']
-  /** Hash of the extrinsic which caused the event to be emitted */
-  inExtrinsic?: Maybe<Scalars['String']>
-  /** Blocknumber of the block in which the event was emitted. */
-  inBlock: Scalars['Int']
-  /** Network the block was produced in */
-  network: Network
-  /** Index of event in block from which it was emitted. */
-  indexInBlock: Scalars['Int']
-  thread: ForumThread
-  threadId: Scalars['String']
-  /** New title of the thread */
-  newTitle: Scalars['String']
-}
-
-export type ThreadTitleUpdatedEventConnection = {
-  totalCount: Scalars['Int']
-  edges: Array<ThreadTitleUpdatedEventEdge>
-  pageInfo: PageInfo
-}
-
-export type ThreadTitleUpdatedEventCreateInput = {
-  inExtrinsic?: Maybe<Scalars['String']>
-  inBlock: Scalars['Float']
-  network: Network
-  indexInBlock: Scalars['Float']
-  thread: Scalars['ID']
-  newTitle: Scalars['String']
-}
-
-export type ThreadTitleUpdatedEventEdge = {
-  node: ThreadTitleUpdatedEvent
-  cursor: Scalars['String']
-}
-
-export enum ThreadTitleUpdatedEventOrderByInput {
-  CreatedAtAsc = 'createdAt_ASC',
-  CreatedAtDesc = 'createdAt_DESC',
-  UpdatedAtAsc = 'updatedAt_ASC',
-  UpdatedAtDesc = 'updatedAt_DESC',
-  DeletedAtAsc = 'deletedAt_ASC',
-  DeletedAtDesc = 'deletedAt_DESC',
-  InExtrinsicAsc = 'inExtrinsic_ASC',
-  InExtrinsicDesc = 'inExtrinsic_DESC',
-  InBlockAsc = 'inBlock_ASC',
-  InBlockDesc = 'inBlock_DESC',
-  NetworkAsc = 'network_ASC',
-  NetworkDesc = 'network_DESC',
-  IndexInBlockAsc = 'indexInBlock_ASC',
-  IndexInBlockDesc = 'indexInBlock_DESC',
-  ThreadAsc = 'thread_ASC',
-  ThreadDesc = 'thread_DESC',
-  NewTitleAsc = 'newTitle_ASC',
-  NewTitleDesc = 'newTitle_DESC',
-}
-
-export type ThreadTitleUpdatedEventUpdateInput = {
-  inExtrinsic?: Maybe<Scalars['String']>
-  inBlock?: Maybe<Scalars['Float']>
-  network?: Maybe<Network>
-  indexInBlock?: Maybe<Scalars['Float']>
-  thread?: Maybe<Scalars['ID']>
-  newTitle?: Maybe<Scalars['String']>
-}
-
-export type ThreadTitleUpdatedEventWhereInput = {
-  id_eq?: Maybe<Scalars['ID']>
-  id_in?: Maybe<Array<Scalars['ID']>>
-  createdAt_eq?: Maybe<Scalars['DateTime']>
-  createdAt_lt?: Maybe<Scalars['DateTime']>
-  createdAt_lte?: Maybe<Scalars['DateTime']>
-  createdAt_gt?: Maybe<Scalars['DateTime']>
-  createdAt_gte?: Maybe<Scalars['DateTime']>
-  createdById_eq?: Maybe<Scalars['ID']>
-  createdById_in?: Maybe<Array<Scalars['ID']>>
-  updatedAt_eq?: Maybe<Scalars['DateTime']>
-  updatedAt_lt?: Maybe<Scalars['DateTime']>
-  updatedAt_lte?: Maybe<Scalars['DateTime']>
-  updatedAt_gt?: Maybe<Scalars['DateTime']>
-  updatedAt_gte?: Maybe<Scalars['DateTime']>
-  updatedById_eq?: Maybe<Scalars['ID']>
-  updatedById_in?: Maybe<Array<Scalars['ID']>>
-  deletedAt_all?: Maybe<Scalars['Boolean']>
-  deletedAt_eq?: Maybe<Scalars['DateTime']>
-  deletedAt_lt?: Maybe<Scalars['DateTime']>
-  deletedAt_lte?: Maybe<Scalars['DateTime']>
-  deletedAt_gt?: Maybe<Scalars['DateTime']>
-  deletedAt_gte?: Maybe<Scalars['DateTime']>
-  deletedById_eq?: Maybe<Scalars['ID']>
-  deletedById_in?: Maybe<Array<Scalars['ID']>>
-  inExtrinsic_eq?: Maybe<Scalars['String']>
-  inExtrinsic_contains?: Maybe<Scalars['String']>
-  inExtrinsic_startsWith?: Maybe<Scalars['String']>
-  inExtrinsic_endsWith?: Maybe<Scalars['String']>
-  inExtrinsic_in?: Maybe<Array<Scalars['String']>>
-  inBlock_eq?: Maybe<Scalars['Int']>
-  inBlock_gt?: Maybe<Scalars['Int']>
-  inBlock_gte?: Maybe<Scalars['Int']>
-  inBlock_lt?: Maybe<Scalars['Int']>
-  inBlock_lte?: Maybe<Scalars['Int']>
-  inBlock_in?: Maybe<Array<Scalars['Int']>>
-  network_eq?: Maybe<Network>
-  network_in?: Maybe<Array<Network>>
-  indexInBlock_eq?: Maybe<Scalars['Int']>
-  indexInBlock_gt?: Maybe<Scalars['Int']>
-  indexInBlock_gte?: Maybe<Scalars['Int']>
-  indexInBlock_lt?: Maybe<Scalars['Int']>
-  indexInBlock_lte?: Maybe<Scalars['Int']>
-  indexInBlock_in?: Maybe<Array<Scalars['Int']>>
-  thread_eq?: Maybe<Scalars['ID']>
-  thread_in?: Maybe<Array<Scalars['ID']>>
-  newTitle_eq?: Maybe<Scalars['String']>
-  newTitle_contains?: Maybe<Scalars['String']>
-  newTitle_startsWith?: Maybe<Scalars['String']>
-  newTitle_endsWith?: Maybe<Scalars['String']>
-  newTitle_in?: Maybe<Array<Scalars['String']>>
-  thread?: Maybe<ForumThreadWhereInput>
-  AND?: Maybe<Array<ThreadTitleUpdatedEventWhereInput>>
-  OR?: Maybe<Array<ThreadTitleUpdatedEventWhereInput>>
-}
-
-export type ThreadTitleUpdatedEventWhereUniqueInput = {
-  id: Scalars['ID']
-}
-
 export type UnlockBlogPostProposalDetails = {
   /** The blog post that should be unlocked */
   blogPost: Scalars['String']

+ 12 - 5
tests/integration-tests/src/graphql/queries/forum.graphql

@@ -65,6 +65,7 @@ fragment ForumPostFields on ForumPost {
       }
     }
   }
+  isVisible
   origin {
     __typename
     ... on PostOriginThreadInitial {
@@ -90,7 +91,7 @@ fragment ForumPostFields on ForumPost {
   }
 }
 
-fragment ForumThreadWithPostsFields on ForumThread {
+fragment ForumThreadWithInitialPost on ForumThread {
   id
   createdAt
   updatedAt
@@ -101,7 +102,7 @@ fragment ForumThreadWithPostsFields on ForumThread {
     id
   }
   title
-  posts {
+  initialPost {
     ...ForumPostFields
   }
   poll {
@@ -120,6 +121,8 @@ fragment ForumThreadWithPostsFields on ForumThread {
   isSticky
   createdInEvent {
     id
+    title
+    text
   }
   status {
     __typename
@@ -139,12 +142,16 @@ fragment ForumThreadWithPostsFields on ForumThread {
       }
     }
   }
-  titleUpdates {
+  isVisible
+  metadataUpdates {
     id
   }
   movedInEvents {
     id
   }
+  tags {
+    id
+  }
 }
 
 query getCategoriesByIds($ids: [ID!]) {
@@ -153,9 +160,9 @@ query getCategoriesByIds($ids: [ID!]) {
   }
 }
 
-query getThreadsWithPostsByIds($ids: [ID!]) {
+query getThreadsWithInitialPostsByIds($ids: [ID!]) {
   forumThreads(where: { id_in: $ids }) {
-    ...ForumThreadWithPostsFields
+    ...ForumThreadWithInitialPost
   }
 }
 

+ 6 - 4
tests/integration-tests/src/graphql/queries/forumEvents.graphql

@@ -66,6 +66,8 @@ fragment ThreadCreatedEventFields on ThreadCreatedEvent {
   network
   inExtrinsic
   indexInBlock
+  title
+  text
   thread {
     id
   }
@@ -77,7 +79,7 @@ query getThreadCreatedEventsByEventIds($eventIds: [ID!]) {
   }
 }
 
-fragment ThreadTitleUpdatedEventFields on ThreadTitleUpdatedEvent {
+fragment ThreadMetadataUpdatedEventFields on ThreadMetadataUpdatedEvent {
   id
   createdAt
   inBlock
@@ -90,9 +92,9 @@ fragment ThreadTitleUpdatedEventFields on ThreadTitleUpdatedEvent {
   newTitle
 }
 
-query getThreadTitleUpdatedEventsByEventIds($eventIds: [ID!]) {
-  threadTitleUpdatedEvents(where: { id_in: $eventIds }) {
-    ...ThreadTitleUpdatedEventFields
+query getThreadMetadataUpdatedEventsByEventIds($eventIds: [ID!]) {
+  threadMetadataUpdatedEvents(where: { id_in: $eventIds }) {
+    ...ThreadMetadataUpdatedEventFields
   }
 }
 

+ 7 - 0
tests/integration-tests/src/graphql/queries/proposals.graphql

@@ -264,11 +264,18 @@ fragment ProposalFields on Proposal {
   }
   statusSetAtBlock
   statusSetAtTime
+  isFinalized
   createdInEvent {
     id
     inBlock
     inExtrinsic
   }
+  discussionThread {
+    id
+    mode {
+      ...ProposalDiscussionThreadModeFields
+    }
+  }
 }
 
 query getProposalsByIds($ids: [ID!]) {

+ 76 - 0
tests/integration-tests/src/graphql/queries/proposalsDiscussion.graphql

@@ -0,0 +1,76 @@
+fragment ProposalDiscussionThreadModeFields on ProposalDiscussionThreadMode {
+  __typename
+  ... on ProposalDiscussionThreadModeClosed {
+    whitelist {
+      members {
+        id
+      }
+    }
+  }
+}
+
+fragment ProposalDiscussionPostStatusFields on ProposalDiscussionPostStatus {
+  __typename
+  ... on ProposalDiscussionPostStatusLocked {
+    deletedInEvent {
+      id
+    }
+  }
+  ... on ProposalDiscussionPostStatusRemoved {
+    deletedInEvent {
+      id
+    }
+  }
+}
+
+fragment ProposalDiscussionThreadFields on ProposalDiscussionThread {
+  id
+  proposal {
+    id
+  }
+  posts {
+    id
+  }
+  mode {
+    ...ProposalDiscussionThreadModeFields
+  }
+  modeChanges {
+    id
+  }
+}
+
+query getProposalDiscussionThreadsByIds($ids: [ID!]) {
+  proposalDiscussionThreads(where: { id_in: $ids }) {
+    ...ProposalDiscussionThreadFields
+  }
+}
+
+fragment ProposalDiscussionPostFields on ProposalDiscussionPost {
+  id
+  discussionThread {
+    id
+  }
+  author {
+    id
+  }
+  status {
+    ...ProposalDiscussionPostStatusFields
+  }
+  isVisible
+  text
+  repliesTo {
+    id
+  }
+  updates {
+    id
+  }
+  createdInEvent {
+    id
+  }
+}
+
+query getProposalDiscussionPostsByIds($ids: [ID!]) {
+  proposalDiscussionPosts(where: { id_in: $ids }) {
+    ...ProposalDiscussionPostFields
+  }
+}

+ 82 - 0
tests/integration-tests/src/graphql/queries/proposalsDiscussionEvents.graphql

@@ -0,0 +1,82 @@
+fragment ProposalDiscussionPostCreatedEventFields on ProposalDiscussionPostCreatedEvent {
+  id
+  createdAt
+  inExtrinsic
+  inBlock
+  network
+  indexInBlock
+  post {
+    id
+  }
+  text
+}
+
+query getProposalDiscussionPostCreatedEvents($eventIds: [ID!]) {
+  proposalDiscussionPostCreatedEvents(where: { id_in: $eventIds }) {
+    ...ProposalDiscussionPostCreatedEventFields
+  }
+}
+
+fragment ProposalDiscussionPostUpdatedEventFields on ProposalDiscussionPostUpdatedEvent {
+  id
+  createdAt
+  inExtrinsic
+  inBlock
+  network
+  indexInBlock
+  post {
+    id
+  }
+  text
+}
+
+query getProposalDiscussionPostUpdatedEvents($eventIds: [ID!]) {
+  proposalDiscussionPostUpdatedEvents(where: { id_in: $eventIds }) {
+    ...ProposalDiscussionPostUpdatedEventFields
+  }
+}
+
+fragment ProposalDiscussionThreadModeChangedEventFields on ProposalDiscussionThreadModeChangedEvent {
+  id
+  createdAt
+  inExtrinsic
+  inBlock
+  network
+  indexInBlock
+  thread {
+    id
+  }
+  newMode {
+    ...ProposalDiscussionThreadModeFields
+  }
+  actor {
+    id
+  }
+}
+
+query getProposalDiscussionThreadModeChangedEvents($eventIds: [ID!]) {
+  proposalDiscussionThreadModeChangedEvents(where: { id_in: $eventIds }) {
+    ...ProposalDiscussionThreadModeChangedEventFields
+  }
+}
+
+fragment ProposalDiscussionPostDeletedEventFields on ProposalDiscussionPostDeletedEvent {
+  id
+  createdAt
+  inExtrinsic
+  inBlock
+  network
+  indexInBlock
+  post {
+    id
+  }
+  actor {
+    id
+  }
+}
+
+query getProposalDiscussionPostDeletedEvents($eventIds: [ID!]) {
+  proposalDiscussionPostDeletedEvents(where: { id_in: $eventIds }) {
+    ...ProposalDiscussionPostDeletedEventFields
+  }
+}

+ 2 - 0
tests/integration-tests/src/scenarios/forum.ts

@@ -4,12 +4,14 @@ import threads from '../flows/forum/threads'
 import posts from '../flows/forum/posts'
 import moderation from '../flows/forum/moderation'
 import leadOpening from '../flows/working-groups/leadOpening'
+import threadTags from '../flows/forum/threadTags'
 import { scenario } from '../Scenario'
 
 scenario(async ({ job }) => {
   const sudoHireLead = job('hiring working group leads', leadOpening)
   job('forum categories', categories).requires(sudoHireLead)
   job('forum threads', threads).requires(sudoHireLead)
+  job('forum thread tags', threadTags).requires(sudoHireLead)
   job('forum polls', polls).requires(sudoHireLead)
   job('forum posts', posts).requires(sudoHireLead)
   job('forum moderation', moderation).requires(sudoHireLead)

+ 5 - 1
tests/integration-tests/src/scenarios/full.ts

@@ -3,6 +3,7 @@ import polls from '../flows/forum/polls'
 import threads from '../flows/forum/threads'
 import posts from '../flows/forum/posts'
 import moderation from '../flows/forum/moderation'
+import threadTags from '../flows/forum/threadTags'
 import leadOpening from '../flows/working-groups/leadOpening'
 import creatingMemberships from '../flows/membership/creatingMemberships'
 import updatingMemberProfile from '../flows/membership/updatingProfile'
@@ -23,6 +24,7 @@ import electCouncil from '../flows/council/elect'
 import runtimeUpgradeProposal from '../flows/proposals/runtimeUpgradeProposal'
 import exactExecutionBlock from '../flows/proposals/exactExecutionBlock'
 import expireProposal from '../flows/proposals/expireProposal'
+import proposalsDiscussion from '../flows/proposalsDiscussion'
 import { scenario } from '../Scenario'
 
 scenario(async ({ job, env }) => {
@@ -49,12 +51,13 @@ scenario(async ({ job, env }) => {
   job('managing staking accounts', managingStakingAccounts).after(membershipSystemJob)
 
   // Proposals:
-  const proposalsJob = job('proposals', [
+  const proposalsJob = job('proposals & proposal discussion', [
     proposals,
     cancellingProposals,
     vetoProposal,
     exactExecutionBlock,
     expireProposal,
+    proposalsDiscussion,
   ]).requires(membershipSystemJob)
 
   // Working groups
@@ -68,6 +71,7 @@ scenario(async ({ job, env }) => {
   // Forum:
   job('forum categories', categories).requires(sudoHireLead)
   job('forum threads', threads).requires(sudoHireLead)
+  job('forum thread tags', threadTags).requires(sudoHireLead)
   job('forum polls', polls).requires(sudoHireLead)
   job('forum posts', posts).requires(sudoHireLead)
   job('forum moderation', moderation).requires(sudoHireLead)

+ 9 - 3
tests/integration-tests/src/scenarios/proposals.ts

@@ -5,6 +5,7 @@ import electCouncil from '../flows/council/elect'
 import runtimeUpgradeProposal from '../flows/proposals/runtimeUpgradeProposal'
 import exactExecutionBlock from '../flows/proposals/exactExecutionBlock'
 import expireProposal from '../flows/proposals/expireProposal'
+import proposalsDiscussion from '../flows/proposalsDiscussion'
 import { scenario } from '../Scenario'
 
 scenario(async ({ job, env }) => {
@@ -12,7 +13,12 @@ scenario(async ({ job, env }) => {
   const runtimeUpgradeProposalJob = env.RUNTIME_UPGRADE_TARGET_WASM_PATH
     ? job('runtime upgrade proposal', runtimeUpgradeProposal).requires(councilJob)
     : undefined
-  job('proposals', [proposals, cancellingProposals, vetoProposal, exactExecutionBlock, expireProposal]).requires(
-    runtimeUpgradeProposalJob || councilJob
-  )
+  job('proposals & proposal discussion', [
+    proposals,
+    cancellingProposals,
+    vetoProposal,
+    exactExecutionBlock,
+    expireProposal,
+    proposalsDiscussion,
+  ]).requires(runtimeUpgradeProposalJob || councilJob)
 })

+ 8 - 0
tests/integration-tests/src/scenarios/proposalsDiscussion.ts

@@ -0,0 +1,8 @@
+import electCouncil from '../flows/council/elect'
+import proposalsDiscussion from '../flows/proposalsDiscussion'
+import { scenario } from '../Scenario'
+
+scenario(async ({ job, env }) => {
+  const councilJob = job('electing council', electCouncil)
+  job('proposal discussion', [proposalsDiscussion]).requires(councilJob)
+})

+ 14 - 2
tests/integration-tests/src/types.ts

@@ -109,13 +109,24 @@ export interface ProposalCreatedEventDetails extends EventDetails {
 }
 
 export type ProposalsEngineEventName =
-  | 'ProposalCreated'
   | 'ProposalStatusUpdated'
   | 'ProposalDecisionMade'
   | 'ProposalExecuted'
   | 'Voted'
   | 'ProposalCancelled'
 
+export type ProposalsCodexEventName = 'ProposalCreated'
+export type ProposalsDiscussionEventName =
+  | 'ThreadCreated'
+  | 'PostCreated'
+  | 'PostUpdated'
+  | 'ThreadModeChanged'
+  | 'PostDeleted'
+
+export interface ProposalDiscussionPostCreatedEventDetails extends EventDetails {
+  postId: PostId
+}
+
 export type ProposalType = keyof typeof ProposalDetails.typeDefinitions
 export type ProposalDetailsJsonByType<T extends ProposalType = ProposalType> = CreateInterface<
   InstanceType<ProposalDetails['typeDefinitions'][T]>
@@ -139,6 +150,7 @@ export interface CategoryCreatedEventDetails extends EventDetails {
 
 export interface ThreadCreatedEventDetails extends EventDetails {
   threadId: ThreadId
+  postId: PostId
 }
 
 export interface PostAddedEventDetails extends EventDetails {
@@ -152,7 +164,7 @@ export type ForumEventName =
   | 'ThreadCreated'
   | 'ThreadModerated'
   | 'ThreadUpdated'
-  | 'ThreadTitleUpdated'
+  | 'ThreadMetadataUpdated'
   | 'ThreadDeleted'
   | 'ThreadMoved'
   | 'PostAdded'

+ 4 - 0
tests/integration-tests/src/utils.ts

@@ -95,6 +95,10 @@ export class Utils {
     )
   }
 
+  public static asText(textOrHex: string): string {
+    return Utils.bytesToString(createType('Bytes', textOrHex))
+  }
+
   public static assert(condition: any, msg?: string): asserts condition {
     if (!condition) {
       throw new Error(msg || 'Assertion failed')

File diff suppressed because it is too large
+ 1 - 1
types/augment-codec/all.ts


+ 176 - 7
types/augment-codec/augment-api-consts.ts

@@ -3,7 +3,7 @@
 
 import type { Vec, u32, u64, u8 } from '@polkadot/types';
 import type { MaxNumber, ProposalParameters } from './all';
-import type { Balance, BalanceOf, BlockNumber, Moment, Perbill, RuntimeDbWeight, Weight } from '@polkadot/types/interfaces/runtime';
+import type { Balance, BalanceOf, BlockNumber, LockIdentifier, Moment, Perbill, RuntimeDbWeight, Weight } from '@polkadot/types/interfaces/runtime';
 import type { SessionIndex } from '@polkadot/types/interfaces/session';
 import type { EraIndex } from '@polkadot/types/interfaces/staking';
 import type { WeightToFeeCoefficient } from '@polkadot/types/interfaces/support';
@@ -33,6 +33,10 @@ declare module '@polkadot/api/types/consts' {
       existentialDeposit: Balance & AugmentedConst<ApiType>;
     };
     bounty: {
+      /**
+       * Exports const - bounty lock id.
+       **/
+      bountyLockId: LockIdentifier & AugmentedConst<ApiType>;
       /**
        * Exports const - max work entry number for a closed assurance type contract bounty.
        **/
@@ -58,9 +62,30 @@ declare module '@polkadot/api/types/consts' {
     };
     contentDirectoryWorkingGroup: {
       /**
-       * Exports const -  max simultaneous active worker number.
+       * Stake needed to create an opening.
+       **/
+      leaderOpeningStake: Balance & AugmentedConst<ApiType>;
+      /**
+       * Exports const
+       * Max simultaneous active worker number.
        **/
       maxWorkerNumberLimit: u32 & AugmentedConst<ApiType>;
+      /**
+       * Minimum stake required for applying into an opening.
+       **/
+      minimumApplicationStake: Balance & AugmentedConst<ApiType>;
+      /**
+       * Defines min unstaking period in the group.
+       **/
+      minUnstakingPeriodLimit: BlockNumber & AugmentedConst<ApiType>;
+      /**
+       * Defines the period every worker gets paid in blocks.
+       **/
+      rewardPeriod: u32 & AugmentedConst<ApiType>;
+      /**
+       * Staking handler lock id.
+       **/
+      stakingHandlerLockId: LockIdentifier & AugmentedConst<ApiType>;
     };
     council: {
       /**
@@ -71,6 +96,14 @@ declare module '@polkadot/api/types/consts' {
        * Interval between automatic budget refills.
        **/
       budgetRefillPeriod: BlockNumber & AugmentedConst<ApiType>;
+      /**
+       * Exports const - candidacy lock id.
+       **/
+      candidacyLockId: LockIdentifier & AugmentedConst<ApiType>;
+      /**
+       * Exports const - councilor lock id.
+       **/
+      councilorLockId: LockIdentifier & AugmentedConst<ApiType>;
       /**
        * Council member count
        **/
@@ -103,19 +136,76 @@ declare module '@polkadot/api/types/consts' {
        **/
       windowSize: BlockNumber & AugmentedConst<ApiType>;
     };
+    forum: {
+      /**
+       * Exports const
+       * Deposit needed to create a post
+       **/
+      postDeposit: BalanceOf & AugmentedConst<ApiType>;
+      /**
+       * Deposit needed to create a thread
+       **/
+      threadDeposit: BalanceOf & AugmentedConst<ApiType>;
+    };
     forumWorkingGroup: {
       /**
-       * Exports const -  max simultaneous active worker number.
+       * Stake needed to create an opening.
+       **/
+      leaderOpeningStake: Balance & AugmentedConst<ApiType>;
+      /**
+       * Exports const
+       * Max simultaneous active worker number.
        **/
       maxWorkerNumberLimit: u32 & AugmentedConst<ApiType>;
+      /**
+       * Minimum stake required for applying into an opening.
+       **/
+      minimumApplicationStake: Balance & AugmentedConst<ApiType>;
+      /**
+       * Defines min unstaking period in the group.
+       **/
+      minUnstakingPeriodLimit: BlockNumber & AugmentedConst<ApiType>;
+      /**
+       * Defines the period every worker gets paid in blocks.
+       **/
+      rewardPeriod: u32 & AugmentedConst<ApiType>;
+      /**
+       * Staking handler lock id.
+       **/
+      stakingHandlerLockId: LockIdentifier & AugmentedConst<ApiType>;
     };
     gatewayWorkingGroup: {
       /**
-       * Exports const -  max simultaneous active worker number.
+       * Stake needed to create an opening.
+       **/
+      leaderOpeningStake: Balance & AugmentedConst<ApiType>;
+      /**
+       * Exports const
+       * Max simultaneous active worker number.
        **/
       maxWorkerNumberLimit: u32 & AugmentedConst<ApiType>;
+      /**
+       * Minimum stake required for applying into an opening.
+       **/
+      minimumApplicationStake: Balance & AugmentedConst<ApiType>;
+      /**
+       * Defines min unstaking period in the group.
+       **/
+      minUnstakingPeriodLimit: BlockNumber & AugmentedConst<ApiType>;
+      /**
+       * Defines the period every worker gets paid in blocks.
+       **/
+      rewardPeriod: u32 & AugmentedConst<ApiType>;
+      /**
+       * Staking handler lock id.
+       **/
+      stakingHandlerLockId: LockIdentifier & AugmentedConst<ApiType>;
     };
     members: {
+      /**
+       * Exports const - Stake needed to candidate as staking account.
+       **/
+      candidateStake: BalanceOf & AugmentedConst<ApiType>;
       /**
        * Exports const - default balance for the invited member.
        **/
@@ -124,22 +214,72 @@ declare module '@polkadot/api/types/consts' {
        * Exports const - default membership fee.
        **/
       defaultMembershipPrice: BalanceOf & AugmentedConst<ApiType>;
+      /**
+       * Exports const - invited member lock id.
+       **/
+      invitedMemberLockId: LockIdentifier & AugmentedConst<ApiType>;
       /**
        * Exports const - maximum percent value of the membership fee for the referral cut.
        **/
       referralCutMaximumPercent: u8 & AugmentedConst<ApiType>;
+      /**
+       * Exports const - staking candidate lock id.
+       **/
+      stakingCandidateLockId: LockIdentifier & AugmentedConst<ApiType>;
     };
     membershipWorkingGroup: {
       /**
-       * Exports const -  max simultaneous active worker number.
+       * Stake needed to create an opening.
+       **/
+      leaderOpeningStake: Balance & AugmentedConst<ApiType>;
+      /**
+       * Exports const
+       * Max simultaneous active worker number.
        **/
       maxWorkerNumberLimit: u32 & AugmentedConst<ApiType>;
+      /**
+       * Minimum stake required for applying into an opening.
+       **/
+      minimumApplicationStake: Balance & AugmentedConst<ApiType>;
+      /**
+       * Defines min unstaking period in the group.
+       **/
+      minUnstakingPeriodLimit: BlockNumber & AugmentedConst<ApiType>;
+      /**
+       * Defines the period every worker gets paid in blocks.
+       **/
+      rewardPeriod: u32 & AugmentedConst<ApiType>;
+      /**
+       * Staking handler lock id.
+       **/
+      stakingHandlerLockId: LockIdentifier & AugmentedConst<ApiType>;
     };
     operationsWorkingGroup: {
       /**
-       * Exports const -  max simultaneous active worker number.
+       * Stake needed to create an opening.
+       **/
+      leaderOpeningStake: Balance & AugmentedConst<ApiType>;
+      /**
+       * Exports const
+       * Max simultaneous active worker number.
        **/
       maxWorkerNumberLimit: u32 & AugmentedConst<ApiType>;
+      /**
+       * Minimum stake required for applying into an opening.
+       **/
+      minimumApplicationStake: Balance & AugmentedConst<ApiType>;
+      /**
+       * Defines min unstaking period in the group.
+       **/
+      minUnstakingPeriodLimit: BlockNumber & AugmentedConst<ApiType>;
+      /**
+       * Defines the period every worker gets paid in blocks.
+       **/
+      rewardPeriod: u32 & AugmentedConst<ApiType>;
+      /**
+       * Staking handler lock id.
+       **/
+      stakingHandlerLockId: LockIdentifier & AugmentedConst<ApiType>;
     };
     proposalsCodex: {
       /**
@@ -237,6 +377,10 @@ declare module '@polkadot/api/types/consts' {
        * be slashed (burned).
        **/
       rejectionFee: BalanceOf & AugmentedConst<ApiType>;
+      /**
+       * Exports const - staking handler lock id.
+       **/
+      stakingHandlerLockId: LockIdentifier & AugmentedConst<ApiType>;
       /**
        * Exports const -  max allowed proposal title length.
        **/
@@ -256,6 +400,10 @@ declare module '@polkadot/api/types/consts' {
        * Duration of revealing stage (number of blocks)
        **/
       revealStageDuration: BlockNumber & AugmentedConst<ApiType>;
+      /**
+       * Exports const - staking handler lock id.
+       **/
+      stakingHandlerLockId: LockIdentifier & AugmentedConst<ApiType>;
       /**
        * Duration of voting stage (number of blocks)
        **/
@@ -308,9 +456,30 @@ declare module '@polkadot/api/types/consts' {
     };
     storageWorkingGroup: {
       /**
-       * Exports const -  max simultaneous active worker number.
+       * Stake needed to create an opening.
+       **/
+      leaderOpeningStake: Balance & AugmentedConst<ApiType>;
+      /**
+       * Exports const
+       * Max simultaneous active worker number.
        **/
       maxWorkerNumberLimit: u32 & AugmentedConst<ApiType>;
+      /**
+       * Minimum stake required for applying into an opening.
+       **/
+      minimumApplicationStake: Balance & AugmentedConst<ApiType>;
+      /**
+       * Defines min unstaking period in the group.
+       **/
+      minUnstakingPeriodLimit: BlockNumber & AugmentedConst<ApiType>;
+      /**
+       * Defines the period every worker gets paid in blocks.
+       **/
+      rewardPeriod: u32 & AugmentedConst<ApiType>;
+      /**
+       * Staking handler lock id.
+       **/
+      stakingHandlerLockId: LockIdentifier & AugmentedConst<ApiType>;
     };
     system: {
       /**

+ 11 - 17
types/augment-codec/augment-api-events.ts

@@ -1,9 +1,8 @@
 // Auto-generated via `yarn polkadot-types-from-chain`, do not edit
 /* eslint-disable */
 
-import type { BTreeSet, Bytes, Option, Text, Vec, bool, u32, u64, u8 } from '@polkadot/types';
-import type { ITuple } from '@polkadot/types/types';
-import type { ActorId, ApplicationId, ApplicationIdToWorkerIdMap, ApplyOnOpeningParameters, BalanceKind, BountyActor, BountyCreationParameters, BountyId, BuyMembershipParameters, CategoryId, Channel, ChannelCategory, ChannelCategoryCreationParameters, ChannelCategoryId, ChannelCategoryUpdateParameters, ChannelCreationParameters, ChannelId, ChannelOwnershipTransferRequest, ChannelOwnershipTransferRequestId, ChannelUpdateParameters, ContentActor, ContentId, ContentParameters, CuratorGroupId, CuratorId, DataObjectStorageRelationshipId, DataObjectType, DataObjectTypeId, EntryId, ExecutionStatus, ForumUserId, GeneralProposalParameters, InviteMembershipParameters, IsCensored, MemberId, MemoText, ModeratorId, NewAsset, OpeningId, OpeningType, OptionResult, OracleJudgment, ParticipantId, PersonCreationParameters, PersonId, PersonUpdateParameters, PlaylistCreationParameters, PlaylistId, PlaylistUpdateParameters, Poll, PostId, PostReactionId, PrivilegedActor, ProposalDecision, ProposalDetailsOf, ProposalId, ProposalStatus, ReplyId, RewardPaymentType, Series, SeriesId, SeriesParameters, StakePolicy, StorageObjectOwner, StorageProviderId, ThreadId, ThreadMode, Title, UpdatedBody, UpdatedTitle, UploadingStatus, VideoCategoryCreationParameters, VideoCategoryId, VideoCategoryUpdateParameters, VideoCreationParameters, VideoId, VideoUpdateParameters, VoteKind, VoucherLimit, WorkerId, WorkingGroup } from './all';
+import type { BTreeMap, BTreeSet, Bytes, Option, Text, Vec, bool, u32, u64, u8 } from '@polkadot/types';
+import type { ActorId, ApplicationId, ApplicationIdToWorkerIdMap, ApplyOnOpeningParameters, BalanceKind, BountyActor, BountyCreationParameters, BountyId, BuyMembershipParameters, CategoryId, Channel, ChannelCategory, ChannelCategoryCreationParameters, ChannelCategoryId, ChannelCategoryUpdateParameters, ChannelCreationParameters, ChannelId, ChannelOwnershipTransferRequest, ChannelOwnershipTransferRequestId, ChannelUpdateParameters, ContentActor, ContentId, ContentParameters, CuratorGroupId, CuratorId, DataObjectStorageRelationshipId, DataObjectType, DataObjectTypeId, EntryId, ExecutionStatus, ExtendedPostId, ForumUserId, GeneralProposalParameters, InviteMembershipParameters, IsCensored, MemberId, MemoText, ModeratorId, NewAsset, OpeningId, OpeningType, OptionResult, OracleJudgment, ParticipantId, PersonCreationParameters, PersonId, PersonUpdateParameters, PlaylistCreationParameters, PlaylistId, PlaylistUpdateParameters, PollInput, PostId, PostReactionId, PrivilegedActor, ProposalDecision, ProposalDetailsOf, ProposalId, ProposalStatus, ReplyId, RewardPaymentType, Series, SeriesId, SeriesParameters, StakePolicy, StorageObjectOwner, StorageProviderId, ThreadId, ThreadMode, Title, UpdatedBody, UpdatedTitle, UploadingStatus, VideoCategoryCreationParameters, VideoCategoryId, VideoCategoryUpdateParameters, VideoCreationParameters, VideoId, VideoUpdateParameters, VoteKind, VoucherLimit, WorkerId, WorkingGroup } from './all';
 import type { BalanceStatus } from '@polkadot/types/interfaces/balances';
 import type { AuthorityId } from '@polkadot/types/interfaces/consensus';
 import type { AuthorityList } from '@polkadot/types/interfaces/grandpa';
@@ -622,7 +621,7 @@ declare module '@polkadot/api/types/events' {
       /**
        * Post with givne id was deleted.
        **/
-      PostDeleted: AugmentedEvent<ApiType, [Bytes, ForumUserId, Vec<ITuple<[CategoryId, ThreadId, PostId, bool]>>]>;
+      PostDeleted: AugmentedEvent<ApiType, [Bytes, ForumUserId, BTreeMap<ExtendedPostId, bool>]>;
       /**
        * Post with givne id was moderated.
        **/
@@ -638,12 +637,17 @@ declare module '@polkadot/api/types/events' {
       PostTextUpdated: AugmentedEvent<ApiType, [PostId, ForumUserId, CategoryId, ThreadId, Bytes]>;
       /**
        * A thread with given id was created.
+       * A third argument reflects the initial post id of the thread.
        **/
-      ThreadCreated: AugmentedEvent<ApiType, [ThreadId, ForumUserId, CategoryId, Bytes, Bytes, Option<Poll>]>;
+      ThreadCreated: AugmentedEvent<ApiType, [CategoryId, ThreadId, PostId, ForumUserId, Bytes, Bytes, Option<PollInput>]>;
       /**
        * A thread was deleted.
        **/
       ThreadDeleted: AugmentedEvent<ApiType, [ThreadId, ForumUserId, CategoryId, bool]>;
+      /**
+       * A thread metadata given id was updated.
+       **/
+      ThreadMetadataUpdated: AugmentedEvent<ApiType, [ThreadId, ForumUserId, CategoryId, Bytes]>;
       /**
        * A thread with given id was moderated.
        **/
@@ -652,10 +656,6 @@ declare module '@polkadot/api/types/events' {
        * A thread was moved to new category
        **/
       ThreadMoved: AugmentedEvent<ApiType, [ThreadId, CategoryId, PrivilegedActor, CategoryId]>;
-      /**
-       * A thread with given id was moderated.
-       **/
-      ThreadTitleUpdated: AugmentedEvent<ApiType, [ThreadId, ForumUserId, CategoryId, Bytes]>;
       /**
        * A thread with given id was updated.
        * The second argument reflects the new archival status of the thread.
@@ -1424,10 +1424,11 @@ declare module '@polkadot/api/types/events' {
       /**
        * A proposal was created
        * Params:
+       * - Id of a newly created proposal after it was saved in storage.
        * - General proposal parameter. Parameters shared by all proposals
        * - Proposal Details. Parameter of proposal with a variant for each kind of proposal
        **/
-      ProposalCreated: AugmentedEvent<ApiType, [GeneralProposalParameters, ProposalDetailsOf]>;
+      ProposalCreated: AugmentedEvent<ApiType, [ProposalId, GeneralProposalParameters, ProposalDetailsOf]>;
     };
     proposalsDiscussion: {
       /**
@@ -1459,13 +1460,6 @@ declare module '@polkadot/api/types/events' {
        * - Id of the proposal
        **/
       ProposalCancelled: AugmentedEvent<ApiType, [MemberId, ProposalId]>;
-      /**
-       * Emits on proposal creation.
-       * Params:
-       * - Member id of a proposer.
-       * - Id of a newly created proposal after it was saved in storage.
-       **/
-      ProposalCreated: AugmentedEvent<ApiType, [MemberId, ProposalId]>;
       /**
        * Emits on getting a proposal status decision.
        * Params:

+ 16 - 16
types/augment-codec/augment-api-query.ts

@@ -140,7 +140,7 @@ declare module '@polkadot/api/types/storage' {
       /**
        * Reply by unique blog, post and reply identificators
        **/
-      replyById: AugmentedQueryDoubleMap<ApiType, (key1: PostId | AnyNumber | Uint8Array, key2: ReplyId | AnyNumber | Uint8Array) => Observable<Reply>, [PostId, ReplyId]>;
+      replyById: AugmentedQuery<ApiType, (arg1: PostId | AnyNumber | Uint8Array, arg2: ReplyId | AnyNumber | Uint8Array) => Observable<Reply>, [PostId, ReplyId]>;
     };
     bounty: {
       /**
@@ -150,7 +150,7 @@ declare module '@polkadot/api/types/storage' {
       /**
        * Double map for bounty funding. It stores a member or council funding for bounties.
        **/
-      bountyContributions: AugmentedQueryDoubleMap<ApiType, (key1: BountyId | AnyNumber | Uint8Array, key2: BountyActor | { Council: any } | { Member: any } | string | Uint8Array) => Observable<BalanceOf>, [BountyId, BountyActor]>;
+      bountyContributions: AugmentedQuery<ApiType, (arg1: BountyId | AnyNumber | Uint8Array, arg2: BountyActor | { Council: any } | { Member: any } | string | Uint8Array) => Observable<BalanceOf>, [BountyId, BountyActor]>;
       /**
        * Count of all bounties that have been created.
        **/
@@ -349,7 +349,7 @@ declare module '@polkadot/api/types/storage' {
       /**
        * Moderator set for each Category
        **/
-      categoryByModerator: AugmentedQueryDoubleMap<ApiType, (key1: CategoryId | AnyNumber | Uint8Array, key2: ModeratorId | AnyNumber | Uint8Array) => Observable<ITuple<[]>>, [CategoryId, ModeratorId]>;
+      categoryByModerator: AugmentedQuery<ApiType, (arg1: CategoryId | AnyNumber | Uint8Array, arg2: ModeratorId | AnyNumber | Uint8Array) => Observable<ITuple<[]>>, [CategoryId, ModeratorId]>;
       /**
        * Counter for all existing categories.
        **/
@@ -373,15 +373,15 @@ declare module '@polkadot/api/types/storage' {
       /**
        * Unique thread poll voters. This private double map prevents double voting.
        **/
-      pollVotes: AugmentedQueryDoubleMap<ApiType, (key1: ThreadId | AnyNumber | Uint8Array, key2: ForumUserId | AnyNumber | Uint8Array) => Observable<bool>, [ThreadId, ForumUserId]>;
+      pollVotes: AugmentedQuery<ApiType, (arg1: ThreadId | AnyNumber | Uint8Array, arg2: ForumUserId | AnyNumber | Uint8Array) => Observable<bool>, [ThreadId, ForumUserId]>;
       /**
        * Map post identifier to corresponding post.
        **/
-      postById: AugmentedQueryDoubleMap<ApiType, (key1: ThreadId | AnyNumber | Uint8Array, key2: PostId | AnyNumber | Uint8Array) => Observable<Post>, [ThreadId, PostId]>;
+      postById: AugmentedQuery<ApiType, (arg1: ThreadId | AnyNumber | Uint8Array, arg2: PostId | AnyNumber | Uint8Array) => Observable<Post>, [ThreadId, PostId]>;
       /**
        * Map thread identifier to corresponding thread.
        **/
-      threadById: AugmentedQueryDoubleMap<ApiType, (key1: CategoryId | AnyNumber | Uint8Array, key2: ThreadId | AnyNumber | Uint8Array) => Observable<ThreadOf>, [CategoryId, ThreadId]>;
+      threadById: AugmentedQuery<ApiType, (arg1: CategoryId | AnyNumber | Uint8Array, arg2: ThreadId | AnyNumber | Uint8Array) => Observable<ThreadOf>, [CategoryId, ThreadId]>;
     };
     forumWorkingGroup: {
       /**
@@ -518,7 +518,7 @@ declare module '@polkadot/api/types/storage' {
        * For each session index, we keep a mapping of `T::ValidatorId` to the
        * number of blocks authored by the given authority.
        **/
-      authoredBlocks: AugmentedQueryDoubleMap<ApiType, (key1: SessionIndex | AnyNumber | Uint8Array, key2: ValidatorId | string | Uint8Array) => Observable<u32>, [SessionIndex, ValidatorId]>;
+      authoredBlocks: AugmentedQuery<ApiType, (arg1: SessionIndex | AnyNumber | Uint8Array, arg2: ValidatorId | string | Uint8Array) => Observable<u32>, [SessionIndex, ValidatorId]>;
       /**
        * The block number after which it's ok to send heartbeats in current session.
        * 
@@ -536,7 +536,7 @@ declare module '@polkadot/api/types/storage' {
        * For each session index, we keep a mapping of `AuthIndex` to
        * `offchain::OpaqueNetworkState`.
        **/
-      receivedHeartbeats: AugmentedQueryDoubleMap<ApiType, (key1: SessionIndex | AnyNumber | Uint8Array, key2: AuthIndex | AnyNumber | Uint8Array) => Observable<Option<Bytes>>, [SessionIndex, AuthIndex]>;
+      receivedHeartbeats: AugmentedQuery<ApiType, (arg1: SessionIndex | AnyNumber | Uint8Array, arg2: AuthIndex | AnyNumber | Uint8Array) => Observable<Option<Bytes>>, [SessionIndex, AuthIndex]>;
     };
     members: {
       /**
@@ -631,7 +631,7 @@ declare module '@polkadot/api/types/storage' {
       /**
        * A vector of reports of the same kind that happened at the same time slot.
        **/
-      concurrentReportsIndex: AugmentedQueryDoubleMap<ApiType, (key1: Kind | string | Uint8Array, key2: OpaqueTimeSlot | string | Uint8Array) => Observable<Vec<ReportIdOf>>, [Kind, OpaqueTimeSlot]>;
+      concurrentReportsIndex: AugmentedQuery<ApiType, (arg1: Kind | string | Uint8Array, arg2: OpaqueTimeSlot | string | Uint8Array) => Observable<Vec<ReportIdOf>>, [Kind, OpaqueTimeSlot]>;
       /**
        * Deferred reports that have been rejected by the offence handler and need to be submitted
        * at a later time.
@@ -715,7 +715,7 @@ declare module '@polkadot/api/types/storage' {
       /**
        * Map thread id and post id to corresponding post.
        **/
-      postThreadIdByPostId: AugmentedQueryDoubleMap<ApiType, (key1: ThreadId | AnyNumber | Uint8Array, key2: PostId | AnyNumber | Uint8Array) => Observable<DiscussionPost>, [ThreadId, PostId]>;
+      postThreadIdByPostId: AugmentedQuery<ApiType, (arg1: ThreadId | AnyNumber | Uint8Array, arg2: PostId | AnyNumber | Uint8Array) => Observable<DiscussionPost>, [ThreadId, PostId]>;
       /**
        * Map thread identifier to corresponding thread.
        **/
@@ -745,7 +745,7 @@ declare module '@polkadot/api/types/storage' {
       /**
        * Double map for preventing duplicate votes. Should be cleaned after usage.
        **/
-      voteExistsByProposalByVoter: AugmentedQueryDoubleMap<ApiType, (key1: ProposalId | AnyNumber | Uint8Array, key2: MemberId | AnyNumber | Uint8Array) => Observable<VoteKind>, [ProposalId, MemberId]>;
+      voteExistsByProposalByVoter: AugmentedQuery<ApiType, (arg1: ProposalId | AnyNumber | Uint8Array, arg2: MemberId | AnyNumber | Uint8Array) => Observable<VoteKind>, [ProposalId, MemberId]>;
     };
     randomnessCollectiveFlip: {
       /**
@@ -857,7 +857,7 @@ declare module '@polkadot/api/types/storage' {
        * Is it removed after `HISTORY_DEPTH` eras.
        * If stakers hasn't been set or has been removed then empty exposure is returned.
        **/
-      erasStakers: AugmentedQueryDoubleMap<ApiType, (key1: EraIndex | AnyNumber | Uint8Array, key2: AccountId | string | Uint8Array) => Observable<Exposure>, [EraIndex, AccountId]>;
+      erasStakers: AugmentedQuery<ApiType, (arg1: EraIndex | AnyNumber | Uint8Array, arg2: AccountId | string | Uint8Array) => Observable<Exposure>, [EraIndex, AccountId]>;
       /**
        * Clipped Exposure of validator at era.
        * 
@@ -871,7 +871,7 @@ declare module '@polkadot/api/types/storage' {
        * Is it removed after `HISTORY_DEPTH` eras.
        * If stakers hasn't been set or has been removed then empty exposure is returned.
        **/
-      erasStakersClipped: AugmentedQueryDoubleMap<ApiType, (key1: EraIndex | AnyNumber | Uint8Array, key2: AccountId | string | Uint8Array) => Observable<Exposure>, [EraIndex, AccountId]>;
+      erasStakersClipped: AugmentedQuery<ApiType, (arg1: EraIndex | AnyNumber | Uint8Array, arg2: AccountId | string | Uint8Array) => Observable<Exposure>, [EraIndex, AccountId]>;
       /**
        * The session index at which the era start for the last `HISTORY_DEPTH` eras.
        **/
@@ -888,7 +888,7 @@ declare module '@polkadot/api/types/storage' {
        * 
        * Is it removed after `HISTORY_DEPTH` eras.
        **/
-      erasValidatorPrefs: AugmentedQueryDoubleMap<ApiType, (key1: EraIndex | AnyNumber | Uint8Array, key2: AccountId | string | Uint8Array) => Observable<ValidatorPrefs>, [EraIndex, AccountId]>;
+      erasValidatorPrefs: AugmentedQuery<ApiType, (arg1: EraIndex | AnyNumber | Uint8Array, arg2: AccountId | string | Uint8Array) => Observable<ValidatorPrefs>, [EraIndex, AccountId]>;
       /**
        * The total validator era payout for the last `HISTORY_DEPTH` eras.
        * 
@@ -935,7 +935,7 @@ declare module '@polkadot/api/types/storage' {
       /**
        * All slashing events on nominators, mapped by era to the highest slash value of the era.
        **/
-      nominatorSlashInEra: AugmentedQueryDoubleMap<ApiType, (key1: EraIndex | AnyNumber | Uint8Array, key2: AccountId | string | Uint8Array) => Observable<Option<BalanceOf>>, [EraIndex, AccountId]>;
+      nominatorSlashInEra: AugmentedQuery<ApiType, (arg1: EraIndex | AnyNumber | Uint8Array, arg2: AccountId | string | Uint8Array) => Observable<Option<BalanceOf>>, [EraIndex, AccountId]>;
       /**
        * Where the reward payment should be made. Keyed by stash.
        **/
@@ -998,7 +998,7 @@ declare module '@polkadot/api/types/storage' {
        * All slashing events on validators, mapped by era to the highest slash proportion
        * and slash value of the era.
        **/
-      validatorSlashInEra: AugmentedQueryDoubleMap<ApiType, (key1: EraIndex | AnyNumber | Uint8Array, key2: AccountId | string | Uint8Array) => Observable<Option<ITuple<[Perbill, BalanceOf]>>>, [EraIndex, AccountId]>;
+      validatorSlashInEra: AugmentedQuery<ApiType, (arg1: EraIndex | AnyNumber | Uint8Array, arg2: AccountId | string | Uint8Array) => Observable<Option<ITuple<[Perbill, BalanceOf]>>>, [EraIndex, AccountId]>;
     };
     storageWorkingGroup: {
       /**

+ 31 - 5
types/augment-codec/augment-api-rpc.ts

@@ -1,15 +1,15 @@
 // Auto-generated via `yarn polkadot-types-from-chain`, do not edit
 /* eslint-disable */
 
-import type { Metadata } from '@polkadot/metadata';
-import type { Bytes, HashMap, Json, Null, Option, StorageKey, Text, U256, U64, Vec, bool, u32, u64 } from '@polkadot/types';
+import type { Bytes, HashMap, Json, Metadata, Null, Option, StorageKey, Text, U256, U64, Vec, bool, u32, u64 } from '@polkadot/types';
 import type { AnyNumber, Codec, IExtrinsic, Observable } from '@polkadot/types/types';
 import type { ExtrinsicOrHash, ExtrinsicStatus } from '@polkadot/types/interfaces/author';
 import type { EpochAuthorship } from '@polkadot/types/interfaces/babe';
+import type { BeefySignedCommitment } from '@polkadot/types/interfaces/beefy';
 import type { BlockHash } from '@polkadot/types/interfaces/chain';
 import type { PrefixedStorageKey } from '@polkadot/types/interfaces/childstate';
 import type { AuthorityId } from '@polkadot/types/interfaces/consensus';
-import type { ContractCallRequest, ContractExecResult } from '@polkadot/types/interfaces/contracts';
+import type { ContractCallRequest, ContractExecResult, ContractInstantiateResult, InstantiateRequest } from '@polkadot/types/interfaces/contracts';
 import type { CreatedBlock } from '@polkadot/types/interfaces/engine';
 import type { EthAccount, EthCallRequest, EthFilter, EthFilterChanges, EthLog, EthReceipt, EthRichBlock, EthSubKind, EthSubParams, EthSyncStatus, EthTransaction, EthTransactionRequest, EthWork } from '@polkadot/types/interfaces/eth';
 import type { Extrinsic } from '@polkadot/types/interfaces/extrinsics';
@@ -19,7 +19,7 @@ import type { StorageKind } from '@polkadot/types/interfaces/offchain';
 import type { FeeDetails, RuntimeDispatchInfo } from '@polkadot/types/interfaces/payment';
 import type { RpcMethods } from '@polkadot/types/interfaces/rpc';
 import type { AccountId, BlockNumber, H160, H256, H64, Hash, Header, Index, Justification, KeyValue, SignedBlock, StorageData } from '@polkadot/types/interfaces/runtime';
-import type { ReadProof, RuntimeVersion } from '@polkadot/types/interfaces/state';
+import type { ReadProof, RuntimeVersion, TraceBlockResponse } from '@polkadot/types/interfaces/state';
 import type { ApplyExtrinsicResult, ChainProperties, ChainType, Health, NetworkState, NodeRole, PeerInfo, SyncState } from '@polkadot/types/interfaces/system';
 
 declare module '@polkadot/rpc-core/types.jsonrpc' {
@@ -64,6 +64,12 @@ declare module '@polkadot/rpc-core/types.jsonrpc' {
        **/
       epochAuthorship: AugmentedRpc<() => Observable<HashMap<AuthorityId, EpochAuthorship>>>;
     };
+    beefy: {
+      /**
+       * Returns the block most recently finalized by BEEFY, alongside side its justification.
+       **/
+      subscribeJustifications: AugmentedRpc<() => Observable<BeefySignedCommitment>>;
+    };
     chain: {
       /**
        * Get header and body of a relay chain block
@@ -99,6 +105,10 @@ declare module '@polkadot/rpc-core/types.jsonrpc' {
        * Returns the keys with prefix from a child storage, leave empty to get all the keys
        **/
       getKeys: AugmentedRpc<(childKey: PrefixedStorageKey | string | Uint8Array, prefix: StorageKey | string | Uint8Array | any, at?: Hash | string | Uint8Array) => Observable<Vec<StorageKey>>>;
+      /**
+       * Returns the keys with prefix from a child storage with pagination support
+       **/
+      getKeysPaged: AugmentedRpc<(childKey: PrefixedStorageKey | string | Uint8Array, prefix: StorageKey | string | Uint8Array | any, count: u32 | AnyNumber | Uint8Array, startKey?: StorageKey | string | Uint8Array | any, at?: Hash | string | Uint8Array) => Observable<Vec<StorageKey>>>;
       /**
        * Returns a child storage entry at a specific block state
        **/
@@ -121,6 +131,10 @@ declare module '@polkadot/rpc-core/types.jsonrpc' {
        * Returns the value under a specified storage key in a contract
        **/
       getStorage: AugmentedRpc<(address: AccountId | string | Uint8Array, key: H256 | string | Uint8Array, at?: BlockHash | string | Uint8Array) => Observable<Option<Bytes>>>;
+      /**
+       * Instantiate a new contract
+       **/
+      instantiate: AugmentedRpc<(request: InstantiateRequest | { origin?: any; endowment?: any; gasLimit?: any; code?: any; data?: any; salt?: any } | string | Uint8Array, at?: BlockHash | string | Uint8Array) => Observable<ContractInstantiateResult>>;
       /**
        * Returns the projected time a given contract will be able to sustain paying its rent
        **/
@@ -142,7 +156,7 @@ declare module '@polkadot/rpc-core/types.jsonrpc' {
        **/
       accounts: AugmentedRpc<() => Observable<Vec<H160>>>;
       /**
-       * Returns balance of the given account.
+       * Returns the blockNumber
        **/
       blockNumber: AugmentedRpc<() => Observable<U256>>;
       /**
@@ -371,6 +385,10 @@ declare module '@polkadot/rpc-core/types.jsonrpc' {
        * Retrieves the keys with prefix of a specific child storage
        **/
       getChildKeys: AugmentedRpc<(childStorageKey: StorageKey | string | Uint8Array | any, childDefinition: StorageKey | string | Uint8Array | any, childType: u32 | AnyNumber | Uint8Array, key: StorageKey | string | Uint8Array | any, at?: BlockHash | string | Uint8Array) => Observable<Vec<StorageKey>>>;
+      /**
+       * Returns proof of storage for child key entries at a specific block state.
+       **/
+      getChildReadProof: AugmentedRpc<(childStorageKey: PrefixedStorageKey | string | Uint8Array, keys: Vec<StorageKey> | (StorageKey | string | Uint8Array | any)[], at?: BlockHash | string | Uint8Array) => Observable<ReadProof>>;
       /**
        * Retrieves the child storage for a key
        **/
@@ -435,6 +453,10 @@ declare module '@polkadot/rpc-core/types.jsonrpc' {
        * Subscribes to storage changes for the provided keys
        **/
       subscribeStorage: AugmentedRpc<<T = Codec[]>(keys?: Vec<StorageKey> | (StorageKey | string | Uint8Array | any)[]) => Observable<T>>;
+      /**
+       * Provides a way to trace the re-execution of a single block
+       **/
+      traceBlock: AugmentedRpc<(block: Hash | string | Uint8Array, targets: Option<Text> | null | object | string | Uint8Array, storageKeys: Option<Text> | null | object | string | Uint8Array) => Observable<TraceBlockResponse>>;
     };
     syncstate: {
       /**
@@ -503,6 +525,10 @@ declare module '@polkadot/rpc-core/types.jsonrpc' {
        * Remove a reserved peer
        **/
       removeReservedPeer: AugmentedRpc<(peerId: Text | string) => Observable<Text>>;
+      /**
+       * Returns the list of reserved peers
+       **/
+      reservedPeers: AugmentedRpc<() => Observable<Vec<Text>>>;
       /**
        * Resets the log filter to Substrate defaults
        **/

+ 9 - 9
types/augment-codec/augment-api-tx.ts

@@ -1,9 +1,9 @@
 // Auto-generated via `yarn polkadot-types-from-chain`, do not edit
 /* eslint-disable */
 
-import type { BTreeSet, Bytes, Compact, Option, Vec, bool, u16, u32, u64, u8 } from '@polkadot/types';
-import type { AnyNumber, ITuple } from '@polkadot/types/types';
-import type { ActorId, ApplicationId, ApplyOnOpeningParameters, BalanceKind, BountyActor, BountyCreationParameters, BountyId, BuyMembershipParameters, CategoryId, ChannelCategoryCreationParameters, ChannelCategoryId, ChannelCategoryUpdateParameters, ChannelCreationParameters, ChannelId, ChannelOwnershipTransferRequest, ChannelOwnershipTransferRequestId, ChannelUpdateParameters, ContentActor, ContentId, ContentParameters, CuratorGroupId, CuratorId, DataObjectStorageRelationshipId, DataObjectType, DataObjectTypeId, EntryId, ForumUserId, FundingRequestParameters, GeneralProposalParameters, InviteMembershipParameters, MemberId, MemoText, ModeratorId, ObjectOwner, OpeningId, OpeningType, OracleJudgment, ParticipantId, PersonActor, PersonCreationParameters, PersonId, PersonUpdateParameters, PlaylistCreationParameters, PlaylistId, PlaylistUpdateParameters, Poll, PostId, PostReactionId, PrivilegedActor, ProposalDetailsOf, ProposalId, ReplyId, ReplyToDelete, SeriesId, SeriesParameters, StakePolicy, StorageProviderId, ThreadId, ThreadMode, VideoCategoryCreationParameters, VideoCategoryId, VideoCategoryUpdateParameters, VideoCreationParameters, VideoId, VideoUpdateParameters, VoteKind, WorkerId, WorkingGroup } from './all';
+import type { BTreeMap, BTreeSet, Bytes, Compact, Option, Vec, bool, u16, u32, u64, u8 } from '@polkadot/types';
+import type { AnyNumber } from '@polkadot/types/types';
+import type { ActorId, ApplicationId, ApplyOnOpeningParameters, BalanceKind, BountyActor, BountyCreationParameters, BountyId, BuyMembershipParameters, CategoryId, ChannelCategoryCreationParameters, ChannelCategoryId, ChannelCategoryUpdateParameters, ChannelCreationParameters, ChannelId, ChannelOwnershipTransferRequest, ChannelOwnershipTransferRequestId, ChannelUpdateParameters, ContentActor, ContentId, ContentParameters, CuratorGroupId, CuratorId, DataObjectStorageRelationshipId, DataObjectType, DataObjectTypeId, EntryId, ExtendedPostId, ForumUserId, FundingRequestParameters, GeneralProposalParameters, InviteMembershipParameters, MemberId, MemoText, ModeratorId, ObjectOwner, OpeningId, OpeningType, OracleJudgment, ParticipantId, PersonActor, PersonCreationParameters, PersonId, PersonUpdateParameters, PlaylistCreationParameters, PlaylistId, PlaylistUpdateParameters, PollInput, PostId, PostReactionId, PrivilegedActor, ProposalDetailsOf, ProposalId, ReplyId, ReplyToDelete, SeriesId, SeriesParameters, StakePolicy, StorageProviderId, ThreadId, ThreadMode, VideoCategoryCreationParameters, VideoCategoryId, VideoCategoryUpdateParameters, VideoCreationParameters, VideoId, VideoUpdateParameters, VoteKind, WorkerId, WorkingGroup } from './all';
 import type { BabeEquivocationProof } from '@polkadot/types/interfaces/babe';
 import type { Extrinsic, Signature } from '@polkadot/types/interfaces/extrinsics';
 import type { GrandpaEquivocationProof, KeyOwnerProof } from '@polkadot/types/interfaces/grandpa';
@@ -862,7 +862,7 @@ declare module '@polkadot/api/types/submittable' {
        * - O(W)
        * # </weight>
        **/
-      createThread: AugmentedSubmittable<(forumUserId: ForumUserId | AnyNumber | Uint8Array, categoryId: CategoryId | AnyNumber | Uint8Array, title: Bytes | string | Uint8Array, text: Bytes | string | Uint8Array, poll: Option<Poll> | null | object | string | Uint8Array) => SubmittableExtrinsic<ApiType>, [ForumUserId, CategoryId, Bytes, Bytes, Option<Poll>]>;
+      createThread: AugmentedSubmittable<(forumUserId: ForumUserId | AnyNumber | Uint8Array, categoryId: CategoryId | AnyNumber | Uint8Array, metadata: Bytes | string | Uint8Array, text: Bytes | string | Uint8Array, pollInput: Option<PollInput> | null | object | string | Uint8Array) => SubmittableExtrinsic<ApiType>, [ForumUserId, CategoryId, Bytes, Bytes, Option<PollInput>]>;
       /**
        * Delete category
        * 
@@ -891,7 +891,7 @@ declare module '@polkadot/api/types/submittable' {
        * - O(W + P)
        * # </weight>
        **/
-      deletePosts: AugmentedSubmittable<(forumUserId: ForumUserId | AnyNumber | Uint8Array, posts: Vec<ITuple<[CategoryId, ThreadId, PostId, bool]>> | ([CategoryId | AnyNumber | Uint8Array, ThreadId | AnyNumber | Uint8Array, PostId | AnyNumber | Uint8Array, bool | boolean | Uint8Array])[], rationale: Bytes | string | Uint8Array) => SubmittableExtrinsic<ApiType>, [ForumUserId, Vec<ITuple<[CategoryId, ThreadId, PostId, bool]>>, Bytes]>;
+      deletePosts: AugmentedSubmittable<(forumUserId: ForumUserId | AnyNumber | Uint8Array, posts: BTreeMap<ExtendedPostId, bool>, rationale: Bytes | string | Uint8Array) => SubmittableExtrinsic<ApiType>, [ForumUserId, BTreeMap<ExtendedPostId, bool>, Bytes]>;
       /**
        * Delete thread
        * 
@@ -932,7 +932,7 @@ declare module '@polkadot/api/types/submittable' {
        * - O(W)
        * # </weight>
        **/
-      editThreadTitle: AugmentedSubmittable<(forumUserId: ForumUserId | AnyNumber | Uint8Array, categoryId: CategoryId | AnyNumber | Uint8Array, threadId: ThreadId | AnyNumber | Uint8Array, newTitle: Bytes | string | Uint8Array) => SubmittableExtrinsic<ApiType>, [ForumUserId, CategoryId, ThreadId, Bytes]>;
+      editThreadMetadata: AugmentedSubmittable<(forumUserId: ForumUserId | AnyNumber | Uint8Array, categoryId: CategoryId | AnyNumber | Uint8Array, threadId: ThreadId | AnyNumber | Uint8Array, newMetadata: Bytes | string | Uint8Array) => SubmittableExtrinsic<ApiType>, [ForumUserId, CategoryId, ThreadId, Bytes]>;
       /**
        * Moderate post
        * 
@@ -2392,7 +2392,7 @@ declare module '@polkadot/api/types/submittable' {
        * - DbWrites per key id: `KeyOwner`
        * # </weight>
        **/
-      setKeys: AugmentedSubmittable<(keys: Keys, proof: Bytes | string | Uint8Array) => SubmittableExtrinsic<ApiType>, [Keys, Bytes]>;
+      setKeys: AugmentedSubmittable<(keys: Keys | string | Uint8Array, proof: Bytes | string | Uint8Array) => SubmittableExtrinsic<ApiType>, [Keys, Bytes]>;
     };
     staking: {
       /**
@@ -2419,7 +2419,7 @@ declare module '@polkadot/api/types/submittable' {
        * - Write: Bonded, Payee, [Origin Account], Locks, Ledger
        * # </weight>
        **/
-      bond: AugmentedSubmittable<(controller: LookupSource | string | Uint8Array, value: Compact<BalanceOf> | AnyNumber | Uint8Array, payee: RewardDestination | { Staked: any } | { Stash: any } | { Controller: any } | { Account: any } | string | Uint8Array) => SubmittableExtrinsic<ApiType>, [LookupSource, Compact<BalanceOf>, RewardDestination]>;
+      bond: AugmentedSubmittable<(controller: LookupSource | string | Uint8Array, value: Compact<BalanceOf> | AnyNumber | Uint8Array, payee: RewardDestination | { Staked: any } | { Stash: any } | { Controller: any } | { Account: any } | { None: any } | string | Uint8Array) => SubmittableExtrinsic<ApiType>, [LookupSource, Compact<BalanceOf>, RewardDestination]>;
       /**
        * Add some extra amount that have appeared in the stash `free_balance` into the balance up
        * for staking.
@@ -2709,7 +2709,7 @@ declare module '@polkadot/api/types/submittable' {
        * - Write: Payee
        * # </weight>
        **/
-      setPayee: AugmentedSubmittable<(payee: RewardDestination | { Staked: any } | { Stash: any } | { Controller: any } | { Account: any } | string | Uint8Array) => SubmittableExtrinsic<ApiType>, [RewardDestination]>;
+      setPayee: AugmentedSubmittable<(payee: RewardDestination | { Staked: any } | { Stash: any } | { Controller: any } | { Account: any } | { None: any } | string | Uint8Array) => SubmittableExtrinsic<ApiType>, [RewardDestination]>;
       /**
        * Sets the ideal number of validators.
        * 

File diff suppressed because it is too large
+ 0 - 1
types/augment-codec/augment-types.ts


+ 23 - 9
types/augment/all/defs.json

@@ -112,17 +112,19 @@
         "sticky_thread_ids": "Vec<ThreadId>"
     },
     "Thread": {
-        "title_hash": "Hash",
+        "metadata_hash": "Hash",
         "category_id": "CategoryId",
         "author_id": "ForumUserId",
-        "archived": "bool",
         "poll": "Option<Poll>",
-        "num_direct_posts": "u32"
+        "cleanup_pay_off": "u128",
+        "number_of_posts": "u64"
     },
     "Post": {
         "thread_id": "ThreadId",
         "text_hash": "Hash",
-        "author_id": "ForumUserId"
+        "author_id": "ForumUserId",
+        "cleanup_pay_off": "u128",
+        "last_edited": "u32"
     },
     "PollAlternative": {
         "alternative_text_hash": "Hash",
@@ -139,13 +141,23 @@
             "Moderator": "ModeratorId"
         }
     },
+    "PollInput": {
+        "description": "Bytes",
+        "end_time": "u64",
+        "poll_alternatives": "Vec<Bytes>"
+    },
     "ThreadOf": {
-        "title_hash": "Hash",
+        "metadata_hash": "Hash",
         "category_id": "CategoryId",
         "author_id": "ForumUserId",
-        "archived": "bool",
         "poll": "Option<Poll>",
-        "num_direct_posts": "u32"
+        "cleanup_pay_off": "u128",
+        "number_of_posts": "u64"
+    },
+    "ExtendedPostId": {
+        "category_id": "CategoryId",
+        "thread_id": "ThreadId",
+        "post_id": "PostId"
     },
     "ApplicationId": "u64",
     "Application": {
@@ -183,7 +195,8 @@
         "created": "u32",
         "description_hash": "Bytes",
         "stake_policy": "StakePolicy",
-        "reward_per_block": "Option<u128>"
+        "reward_per_block": "Option<u128>",
+        "creation_stake": "u128"
     },
     "OpeningId": "u64",
     "StakePolicy": {
@@ -682,5 +695,6 @@
         }
     },
     "MaxNumber": "u32",
-    "IsCensored": "bool"
+    "IsCensored": "bool",
+    "AccountInfo": "AccountInfoWithRefCount"
 }

+ 27 - 6
types/augment/all/types.ts

@@ -4,6 +4,10 @@
 import type { BTreeMap, BTreeSet, Bytes, Enum, Option, Struct, Text, U8aFixed, Vec, bool, u128, u16, u32, u64, u8 } from '@polkadot/types';
 import type { ITuple } from '@polkadot/types/types';
 import type { AccountId, Balance, Hash } from '@polkadot/types/interfaces/runtime';
+import type { AccountInfoWithRefCount } from '@polkadot/types/interfaces/system';
+
+/** @name AccountInfo */
+export interface AccountInfo extends AccountInfoWithRefCount {}
 
 /** @name ActorId */
 export interface ActorId extends u64 {}
@@ -357,6 +361,13 @@ export interface ExecutionStatus extends Enum {
   readonly asExecutionFailed: ExecutionFailed;
 }
 
+/** @name ExtendedPostId */
+export interface ExtendedPostId extends Struct {
+  readonly category_id: CategoryId;
+  readonly thread_id: ThreadId;
+  readonly post_id: PostId;
+}
+
 /** @name FillOpeningParameters */
 export interface FillOpeningParameters extends Struct {
   readonly opening_id: OpeningId;
@@ -478,6 +489,7 @@ export interface Opening extends Struct {
   readonly description_hash: Bytes;
   readonly stake_policy: StakePolicy;
   readonly reward_per_block: Option<u128>;
+  readonly creation_stake: u128;
 }
 
 /** @name OpeningId */
@@ -582,11 +594,20 @@ export interface PollAlternative extends Struct {
   readonly vote_count: u32;
 }
 
+/** @name PollInput */
+export interface PollInput extends Struct {
+  readonly description: Bytes;
+  readonly end_time: u64;
+  readonly poll_alternatives: Vec<Bytes>;
+}
+
 /** @name Post */
 export interface Post extends Struct {
   readonly thread_id: ThreadId;
   readonly text_hash: Hash;
   readonly author_id: ForumUserId;
+  readonly cleanup_pay_off: u128;
+  readonly last_edited: u32;
 }
 
 /** @name PostId */
@@ -878,12 +899,12 @@ export interface TerminateRoleParameters extends Struct {
 
 /** @name Thread */
 export interface Thread extends Struct {
-  readonly title_hash: Hash;
+  readonly metadata_hash: Hash;
   readonly category_id: CategoryId;
   readonly author_id: ForumUserId;
-  readonly archived: bool;
   readonly poll: Option<Poll>;
-  readonly num_direct_posts: u32;
+  readonly cleanup_pay_off: u128;
+  readonly number_of_posts: u64;
 }
 
 /** @name ThreadId */
@@ -898,12 +919,12 @@ export interface ThreadMode extends Enum {
 
 /** @name ThreadOf */
 export interface ThreadOf extends Struct {
-  readonly title_hash: Hash;
+  readonly metadata_hash: Hash;
   readonly category_id: CategoryId;
   readonly author_id: ForumUserId;
-  readonly archived: bool;
   readonly poll: Option<Poll>;
-  readonly num_direct_posts: u32;
+  readonly cleanup_pay_off: u128;
+  readonly number_of_posts: u64;
 }
 
 /** @name Title */

+ 176 - 7
types/augment/augment-api-consts.ts

@@ -3,7 +3,7 @@
 
 import type { Vec, u32, u64, u8 } from '@polkadot/types';
 import type { MaxNumber, ProposalParameters } from './all';
-import type { Balance, BalanceOf, BlockNumber, Moment, Perbill, RuntimeDbWeight, Weight } from '@polkadot/types/interfaces/runtime';
+import type { Balance, BalanceOf, BlockNumber, LockIdentifier, Moment, Perbill, RuntimeDbWeight, Weight } from '@polkadot/types/interfaces/runtime';
 import type { SessionIndex } from '@polkadot/types/interfaces/session';
 import type { EraIndex } from '@polkadot/types/interfaces/staking';
 import type { WeightToFeeCoefficient } from '@polkadot/types/interfaces/support';
@@ -33,6 +33,10 @@ declare module '@polkadot/api/types/consts' {
       existentialDeposit: Balance & AugmentedConst<ApiType>;
     };
     bounty: {
+      /**
+       * Exports const - bounty lock id.
+       **/
+      bountyLockId: LockIdentifier & AugmentedConst<ApiType>;
       /**
        * Exports const - max work entry number for a closed assurance type contract bounty.
        **/
@@ -58,9 +62,30 @@ declare module '@polkadot/api/types/consts' {
     };
     contentDirectoryWorkingGroup: {
       /**
-       * Exports const -  max simultaneous active worker number.
+       * Stake needed to create an opening.
+       **/
+      leaderOpeningStake: Balance & AugmentedConst<ApiType>;
+      /**
+       * Exports const
+       * Max simultaneous active worker number.
        **/
       maxWorkerNumberLimit: u32 & AugmentedConst<ApiType>;
+      /**
+       * Minimum stake required for applying into an opening.
+       **/
+      minimumApplicationStake: Balance & AugmentedConst<ApiType>;
+      /**
+       * Defines min unstaking period in the group.
+       **/
+      minUnstakingPeriodLimit: BlockNumber & AugmentedConst<ApiType>;
+      /**
+       * Defines the period every worker gets paid in blocks.
+       **/
+      rewardPeriod: u32 & AugmentedConst<ApiType>;
+      /**
+       * Staking handler lock id.
+       **/
+      stakingHandlerLockId: LockIdentifier & AugmentedConst<ApiType>;
     };
     council: {
       /**
@@ -71,6 +96,14 @@ declare module '@polkadot/api/types/consts' {
        * Interval between automatic budget refills.
        **/
       budgetRefillPeriod: BlockNumber & AugmentedConst<ApiType>;
+      /**
+       * Exports const - candidacy lock id.
+       **/
+      candidacyLockId: LockIdentifier & AugmentedConst<ApiType>;
+      /**
+       * Exports const - councilor lock id.
+       **/
+      councilorLockId: LockIdentifier & AugmentedConst<ApiType>;
       /**
        * Council member count
        **/
@@ -103,19 +136,76 @@ declare module '@polkadot/api/types/consts' {
        **/
       windowSize: BlockNumber & AugmentedConst<ApiType>;
     };
+    forum: {
+      /**
+       * Exports const
+       * Deposit needed to create a post
+       **/
+      postDeposit: BalanceOf & AugmentedConst<ApiType>;
+      /**
+       * Deposit needed to create a thread
+       **/
+      threadDeposit: BalanceOf & AugmentedConst<ApiType>;
+    };
     forumWorkingGroup: {
       /**
-       * Exports const -  max simultaneous active worker number.
+       * Stake needed to create an opening.
+       **/
+      leaderOpeningStake: Balance & AugmentedConst<ApiType>;
+      /**
+       * Exports const
+       * Max simultaneous active worker number.
        **/
       maxWorkerNumberLimit: u32 & AugmentedConst<ApiType>;
+      /**
+       * Minimum stake required for applying into an opening.
+       **/
+      minimumApplicationStake: Balance & AugmentedConst<ApiType>;
+      /**
+       * Defines min unstaking period in the group.
+       **/
+      minUnstakingPeriodLimit: BlockNumber & AugmentedConst<ApiType>;
+      /**
+       * Defines the period every worker gets paid in blocks.
+       **/
+      rewardPeriod: u32 & AugmentedConst<ApiType>;
+      /**
+       * Staking handler lock id.
+       **/
+      stakingHandlerLockId: LockIdentifier & AugmentedConst<ApiType>;
     };
     gatewayWorkingGroup: {
       /**
-       * Exports const -  max simultaneous active worker number.
+       * Stake needed to create an opening.
+       **/
+      leaderOpeningStake: Balance & AugmentedConst<ApiType>;
+      /**
+       * Exports const
+       * Max simultaneous active worker number.
        **/
       maxWorkerNumberLimit: u32 & AugmentedConst<ApiType>;
+      /**
+       * Minimum stake required for applying into an opening.
+       **/
+      minimumApplicationStake: Balance & AugmentedConst<ApiType>;
+      /**
+       * Defines min unstaking period in the group.
+       **/
+      minUnstakingPeriodLimit: BlockNumber & AugmentedConst<ApiType>;
+      /**
+       * Defines the period every worker gets paid in blocks.
+       **/
+      rewardPeriod: u32 & AugmentedConst<ApiType>;
+      /**
+       * Staking handler lock id.
+       **/
+      stakingHandlerLockId: LockIdentifier & AugmentedConst<ApiType>;
     };
     members: {
+      /**
+       * Exports const - Stake needed to candidate as staking account.
+       **/
+      candidateStake: BalanceOf & AugmentedConst<ApiType>;
       /**
        * Exports const - default balance for the invited member.
        **/
@@ -124,22 +214,72 @@ declare module '@polkadot/api/types/consts' {
        * Exports const - default membership fee.
        **/
       defaultMembershipPrice: BalanceOf & AugmentedConst<ApiType>;
+      /**
+       * Exports const - invited member lock id.
+       **/
+      invitedMemberLockId: LockIdentifier & AugmentedConst<ApiType>;
       /**
        * Exports const - maximum percent value of the membership fee for the referral cut.
        **/
       referralCutMaximumPercent: u8 & AugmentedConst<ApiType>;
+      /**
+       * Exports const - staking candidate lock id.
+       **/
+      stakingCandidateLockId: LockIdentifier & AugmentedConst<ApiType>;
     };
     membershipWorkingGroup: {
       /**
-       * Exports const -  max simultaneous active worker number.
+       * Stake needed to create an opening.
+       **/
+      leaderOpeningStake: Balance & AugmentedConst<ApiType>;
+      /**
+       * Exports const
+       * Max simultaneous active worker number.
        **/
       maxWorkerNumberLimit: u32 & AugmentedConst<ApiType>;
+      /**
+       * Minimum stake required for applying into an opening.
+       **/
+      minimumApplicationStake: Balance & AugmentedConst<ApiType>;
+      /**
+       * Defines min unstaking period in the group.
+       **/
+      minUnstakingPeriodLimit: BlockNumber & AugmentedConst<ApiType>;
+      /**
+       * Defines the period every worker gets paid in blocks.
+       **/
+      rewardPeriod: u32 & AugmentedConst<ApiType>;
+      /**
+       * Staking handler lock id.
+       **/
+      stakingHandlerLockId: LockIdentifier & AugmentedConst<ApiType>;
     };
     operationsWorkingGroup: {
       /**
-       * Exports const -  max simultaneous active worker number.
+       * Stake needed to create an opening.
+       **/
+      leaderOpeningStake: Balance & AugmentedConst<ApiType>;
+      /**
+       * Exports const
+       * Max simultaneous active worker number.
        **/
       maxWorkerNumberLimit: u32 & AugmentedConst<ApiType>;
+      /**
+       * Minimum stake required for applying into an opening.
+       **/
+      minimumApplicationStake: Balance & AugmentedConst<ApiType>;
+      /**
+       * Defines min unstaking period in the group.
+       **/
+      minUnstakingPeriodLimit: BlockNumber & AugmentedConst<ApiType>;
+      /**
+       * Defines the period every worker gets paid in blocks.
+       **/
+      rewardPeriod: u32 & AugmentedConst<ApiType>;
+      /**
+       * Staking handler lock id.
+       **/
+      stakingHandlerLockId: LockIdentifier & AugmentedConst<ApiType>;
     };
     proposalsCodex: {
       /**
@@ -237,6 +377,10 @@ declare module '@polkadot/api/types/consts' {
        * be slashed (burned).
        **/
       rejectionFee: BalanceOf & AugmentedConst<ApiType>;
+      /**
+       * Exports const - staking handler lock id.
+       **/
+      stakingHandlerLockId: LockIdentifier & AugmentedConst<ApiType>;
       /**
        * Exports const -  max allowed proposal title length.
        **/
@@ -256,6 +400,10 @@ declare module '@polkadot/api/types/consts' {
        * Duration of revealing stage (number of blocks)
        **/
       revealStageDuration: BlockNumber & AugmentedConst<ApiType>;
+      /**
+       * Exports const - staking handler lock id.
+       **/
+      stakingHandlerLockId: LockIdentifier & AugmentedConst<ApiType>;
       /**
        * Duration of voting stage (number of blocks)
        **/
@@ -308,9 +456,30 @@ declare module '@polkadot/api/types/consts' {
     };
     storageWorkingGroup: {
       /**
-       * Exports const -  max simultaneous active worker number.
+       * Stake needed to create an opening.
+       **/
+      leaderOpeningStake: Balance & AugmentedConst<ApiType>;
+      /**
+       * Exports const
+       * Max simultaneous active worker number.
        **/
       maxWorkerNumberLimit: u32 & AugmentedConst<ApiType>;
+      /**
+       * Minimum stake required for applying into an opening.
+       **/
+      minimumApplicationStake: Balance & AugmentedConst<ApiType>;
+      /**
+       * Defines min unstaking period in the group.
+       **/
+      minUnstakingPeriodLimit: BlockNumber & AugmentedConst<ApiType>;
+      /**
+       * Defines the period every worker gets paid in blocks.
+       **/
+      rewardPeriod: u32 & AugmentedConst<ApiType>;
+      /**
+       * Staking handler lock id.
+       **/
+      stakingHandlerLockId: LockIdentifier & AugmentedConst<ApiType>;
     };
     system: {
       /**

+ 11 - 17
types/augment/augment-api-events.ts

@@ -1,9 +1,8 @@
 // Auto-generated via `yarn polkadot-types-from-chain`, do not edit
 /* eslint-disable */
 
-import type { BTreeSet, Bytes, Option, Text, Vec, bool, u32, u64, u8 } from '@polkadot/types';
-import type { ITuple } from '@polkadot/types/types';
-import type { ActorId, ApplicationId, ApplicationIdToWorkerIdMap, ApplyOnOpeningParameters, BalanceKind, BountyActor, BountyCreationParameters, BountyId, BuyMembershipParameters, CategoryId, Channel, ChannelCategory, ChannelCategoryCreationParameters, ChannelCategoryId, ChannelCategoryUpdateParameters, ChannelCreationParameters, ChannelId, ChannelOwnershipTransferRequest, ChannelOwnershipTransferRequestId, ChannelUpdateParameters, ContentActor, ContentId, ContentParameters, CuratorGroupId, CuratorId, DataObjectStorageRelationshipId, DataObjectType, DataObjectTypeId, EntryId, ExecutionStatus, ForumUserId, GeneralProposalParameters, InviteMembershipParameters, IsCensored, MemberId, MemoText, ModeratorId, NewAsset, OpeningId, OpeningType, OptionResult, OracleJudgment, ParticipantId, PersonCreationParameters, PersonId, PersonUpdateParameters, PlaylistCreationParameters, PlaylistId, PlaylistUpdateParameters, Poll, PostId, PostReactionId, PrivilegedActor, ProposalDecision, ProposalDetailsOf, ProposalId, ProposalStatus, ReplyId, RewardPaymentType, Series, SeriesId, SeriesParameters, StakePolicy, StorageObjectOwner, StorageProviderId, ThreadId, ThreadMode, Title, UpdatedBody, UpdatedTitle, UploadingStatus, VideoCategoryCreationParameters, VideoCategoryId, VideoCategoryUpdateParameters, VideoCreationParameters, VideoId, VideoUpdateParameters, VoteKind, VoucherLimit, WorkerId, WorkingGroup } from './all';
+import type { BTreeMap, BTreeSet, Bytes, Option, Text, Vec, bool, u32, u64, u8 } from '@polkadot/types';
+import type { ActorId, ApplicationId, ApplicationIdToWorkerIdMap, ApplyOnOpeningParameters, BalanceKind, BountyActor, BountyCreationParameters, BountyId, BuyMembershipParameters, CategoryId, Channel, ChannelCategory, ChannelCategoryCreationParameters, ChannelCategoryId, ChannelCategoryUpdateParameters, ChannelCreationParameters, ChannelId, ChannelOwnershipTransferRequest, ChannelOwnershipTransferRequestId, ChannelUpdateParameters, ContentActor, ContentId, ContentParameters, CuratorGroupId, CuratorId, DataObjectStorageRelationshipId, DataObjectType, DataObjectTypeId, EntryId, ExecutionStatus, ExtendedPostId, ForumUserId, GeneralProposalParameters, InviteMembershipParameters, IsCensored, MemberId, MemoText, ModeratorId, NewAsset, OpeningId, OpeningType, OptionResult, OracleJudgment, ParticipantId, PersonCreationParameters, PersonId, PersonUpdateParameters, PlaylistCreationParameters, PlaylistId, PlaylistUpdateParameters, PollInput, PostId, PostReactionId, PrivilegedActor, ProposalDecision, ProposalDetailsOf, ProposalId, ProposalStatus, ReplyId, RewardPaymentType, Series, SeriesId, SeriesParameters, StakePolicy, StorageObjectOwner, StorageProviderId, ThreadId, ThreadMode, Title, UpdatedBody, UpdatedTitle, UploadingStatus, VideoCategoryCreationParameters, VideoCategoryId, VideoCategoryUpdateParameters, VideoCreationParameters, VideoId, VideoUpdateParameters, VoteKind, VoucherLimit, WorkerId, WorkingGroup } from './all';
 import type { BalanceStatus } from '@polkadot/types/interfaces/balances';
 import type { AuthorityId } from '@polkadot/types/interfaces/consensus';
 import type { AuthorityList } from '@polkadot/types/interfaces/grandpa';
@@ -622,7 +621,7 @@ declare module '@polkadot/api/types/events' {
       /**
        * Post with givne id was deleted.
        **/
-      PostDeleted: AugmentedEvent<ApiType, [Bytes, ForumUserId, Vec<ITuple<[CategoryId, ThreadId, PostId, bool]>>]>;
+      PostDeleted: AugmentedEvent<ApiType, [Bytes, ForumUserId, BTreeMap<ExtendedPostId, bool>]>;
       /**
        * Post with givne id was moderated.
        **/
@@ -638,12 +637,17 @@ declare module '@polkadot/api/types/events' {
       PostTextUpdated: AugmentedEvent<ApiType, [PostId, ForumUserId, CategoryId, ThreadId, Bytes]>;
       /**
        * A thread with given id was created.
+       * A third argument reflects the initial post id of the thread.
        **/
-      ThreadCreated: AugmentedEvent<ApiType, [ThreadId, ForumUserId, CategoryId, Bytes, Bytes, Option<Poll>]>;
+      ThreadCreated: AugmentedEvent<ApiType, [CategoryId, ThreadId, PostId, ForumUserId, Bytes, Bytes, Option<PollInput>]>;
       /**
        * A thread was deleted.
        **/
       ThreadDeleted: AugmentedEvent<ApiType, [ThreadId, ForumUserId, CategoryId, bool]>;
+      /**
+       * A thread metadata given id was updated.
+       **/
+      ThreadMetadataUpdated: AugmentedEvent<ApiType, [ThreadId, ForumUserId, CategoryId, Bytes]>;
       /**
        * A thread with given id was moderated.
        **/
@@ -652,10 +656,6 @@ declare module '@polkadot/api/types/events' {
        * A thread was moved to new category
        **/
       ThreadMoved: AugmentedEvent<ApiType, [ThreadId, CategoryId, PrivilegedActor, CategoryId]>;
-      /**
-       * A thread with given id was moderated.
-       **/
-      ThreadTitleUpdated: AugmentedEvent<ApiType, [ThreadId, ForumUserId, CategoryId, Bytes]>;
       /**
        * A thread with given id was updated.
        * The second argument reflects the new archival status of the thread.
@@ -1424,10 +1424,11 @@ declare module '@polkadot/api/types/events' {
       /**
        * A proposal was created
        * Params:
+       * - Id of a newly created proposal after it was saved in storage.
        * - General proposal parameter. Parameters shared by all proposals
        * - Proposal Details. Parameter of proposal with a variant for each kind of proposal
        **/
-      ProposalCreated: AugmentedEvent<ApiType, [GeneralProposalParameters, ProposalDetailsOf]>;
+      ProposalCreated: AugmentedEvent<ApiType, [ProposalId, GeneralProposalParameters, ProposalDetailsOf]>;
     };
     proposalsDiscussion: {
       /**
@@ -1459,13 +1460,6 @@ declare module '@polkadot/api/types/events' {
        * - Id of the proposal
        **/
       ProposalCancelled: AugmentedEvent<ApiType, [MemberId, ProposalId]>;
-      /**
-       * Emits on proposal creation.
-       * Params:
-       * - Member id of a proposer.
-       * - Id of a newly created proposal after it was saved in storage.
-       **/
-      ProposalCreated: AugmentedEvent<ApiType, [MemberId, ProposalId]>;
       /**
        * Emits on getting a proposal status decision.
        * Params:

+ 16 - 16
types/augment/augment-api-query.ts

@@ -140,7 +140,7 @@ declare module '@polkadot/api/types/storage' {
       /**
        * Reply by unique blog, post and reply identificators
        **/
-      replyById: AugmentedQueryDoubleMap<ApiType, (key1: PostId | AnyNumber | Uint8Array, key2: ReplyId | AnyNumber | Uint8Array) => Observable<Reply>, [PostId, ReplyId]>;
+      replyById: AugmentedQuery<ApiType, (arg1: PostId | AnyNumber | Uint8Array, arg2: ReplyId | AnyNumber | Uint8Array) => Observable<Reply>, [PostId, ReplyId]>;
     };
     bounty: {
       /**
@@ -150,7 +150,7 @@ declare module '@polkadot/api/types/storage' {
       /**
        * Double map for bounty funding. It stores a member or council funding for bounties.
        **/
-      bountyContributions: AugmentedQueryDoubleMap<ApiType, (key1: BountyId | AnyNumber | Uint8Array, key2: BountyActor | { Council: any } | { Member: any } | string | Uint8Array) => Observable<BalanceOf>, [BountyId, BountyActor]>;
+      bountyContributions: AugmentedQuery<ApiType, (arg1: BountyId | AnyNumber | Uint8Array, arg2: BountyActor | { Council: any } | { Member: any } | string | Uint8Array) => Observable<BalanceOf>, [BountyId, BountyActor]>;
       /**
        * Count of all bounties that have been created.
        **/
@@ -349,7 +349,7 @@ declare module '@polkadot/api/types/storage' {
       /**
        * Moderator set for each Category
        **/
-      categoryByModerator: AugmentedQueryDoubleMap<ApiType, (key1: CategoryId | AnyNumber | Uint8Array, key2: ModeratorId | AnyNumber | Uint8Array) => Observable<ITuple<[]>>, [CategoryId, ModeratorId]>;
+      categoryByModerator: AugmentedQuery<ApiType, (arg1: CategoryId | AnyNumber | Uint8Array, arg2: ModeratorId | AnyNumber | Uint8Array) => Observable<ITuple<[]>>, [CategoryId, ModeratorId]>;
       /**
        * Counter for all existing categories.
        **/
@@ -373,15 +373,15 @@ declare module '@polkadot/api/types/storage' {
       /**
        * Unique thread poll voters. This private double map prevents double voting.
        **/
-      pollVotes: AugmentedQueryDoubleMap<ApiType, (key1: ThreadId | AnyNumber | Uint8Array, key2: ForumUserId | AnyNumber | Uint8Array) => Observable<bool>, [ThreadId, ForumUserId]>;
+      pollVotes: AugmentedQuery<ApiType, (arg1: ThreadId | AnyNumber | Uint8Array, arg2: ForumUserId | AnyNumber | Uint8Array) => Observable<bool>, [ThreadId, ForumUserId]>;
       /**
        * Map post identifier to corresponding post.
        **/
-      postById: AugmentedQueryDoubleMap<ApiType, (key1: ThreadId | AnyNumber | Uint8Array, key2: PostId | AnyNumber | Uint8Array) => Observable<Post>, [ThreadId, PostId]>;
+      postById: AugmentedQuery<ApiType, (arg1: ThreadId | AnyNumber | Uint8Array, arg2: PostId | AnyNumber | Uint8Array) => Observable<Post>, [ThreadId, PostId]>;
       /**
        * Map thread identifier to corresponding thread.
        **/
-      threadById: AugmentedQueryDoubleMap<ApiType, (key1: CategoryId | AnyNumber | Uint8Array, key2: ThreadId | AnyNumber | Uint8Array) => Observable<ThreadOf>, [CategoryId, ThreadId]>;
+      threadById: AugmentedQuery<ApiType, (arg1: CategoryId | AnyNumber | Uint8Array, arg2: ThreadId | AnyNumber | Uint8Array) => Observable<ThreadOf>, [CategoryId, ThreadId]>;
     };
     forumWorkingGroup: {
       /**
@@ -518,7 +518,7 @@ declare module '@polkadot/api/types/storage' {
        * For each session index, we keep a mapping of `T::ValidatorId` to the
        * number of blocks authored by the given authority.
        **/
-      authoredBlocks: AugmentedQueryDoubleMap<ApiType, (key1: SessionIndex | AnyNumber | Uint8Array, key2: ValidatorId | string | Uint8Array) => Observable<u32>, [SessionIndex, ValidatorId]>;
+      authoredBlocks: AugmentedQuery<ApiType, (arg1: SessionIndex | AnyNumber | Uint8Array, arg2: ValidatorId | string | Uint8Array) => Observable<u32>, [SessionIndex, ValidatorId]>;
       /**
        * The block number after which it's ok to send heartbeats in current session.
        * 
@@ -536,7 +536,7 @@ declare module '@polkadot/api/types/storage' {
        * For each session index, we keep a mapping of `AuthIndex` to
        * `offchain::OpaqueNetworkState`.
        **/
-      receivedHeartbeats: AugmentedQueryDoubleMap<ApiType, (key1: SessionIndex | AnyNumber | Uint8Array, key2: AuthIndex | AnyNumber | Uint8Array) => Observable<Option<Bytes>>, [SessionIndex, AuthIndex]>;
+      receivedHeartbeats: AugmentedQuery<ApiType, (arg1: SessionIndex | AnyNumber | Uint8Array, arg2: AuthIndex | AnyNumber | Uint8Array) => Observable<Option<Bytes>>, [SessionIndex, AuthIndex]>;
     };
     members: {
       /**
@@ -631,7 +631,7 @@ declare module '@polkadot/api/types/storage' {
       /**
        * A vector of reports of the same kind that happened at the same time slot.
        **/
-      concurrentReportsIndex: AugmentedQueryDoubleMap<ApiType, (key1: Kind | string | Uint8Array, key2: OpaqueTimeSlot | string | Uint8Array) => Observable<Vec<ReportIdOf>>, [Kind, OpaqueTimeSlot]>;
+      concurrentReportsIndex: AugmentedQuery<ApiType, (arg1: Kind | string | Uint8Array, arg2: OpaqueTimeSlot | string | Uint8Array) => Observable<Vec<ReportIdOf>>, [Kind, OpaqueTimeSlot]>;
       /**
        * Deferred reports that have been rejected by the offence handler and need to be submitted
        * at a later time.
@@ -715,7 +715,7 @@ declare module '@polkadot/api/types/storage' {
       /**
        * Map thread id and post id to corresponding post.
        **/
-      postThreadIdByPostId: AugmentedQueryDoubleMap<ApiType, (key1: ThreadId | AnyNumber | Uint8Array, key2: PostId | AnyNumber | Uint8Array) => Observable<DiscussionPost>, [ThreadId, PostId]>;
+      postThreadIdByPostId: AugmentedQuery<ApiType, (arg1: ThreadId | AnyNumber | Uint8Array, arg2: PostId | AnyNumber | Uint8Array) => Observable<DiscussionPost>, [ThreadId, PostId]>;
       /**
        * Map thread identifier to corresponding thread.
        **/
@@ -745,7 +745,7 @@ declare module '@polkadot/api/types/storage' {
       /**
        * Double map for preventing duplicate votes. Should be cleaned after usage.
        **/
-      voteExistsByProposalByVoter: AugmentedQueryDoubleMap<ApiType, (key1: ProposalId | AnyNumber | Uint8Array, key2: MemberId | AnyNumber | Uint8Array) => Observable<VoteKind>, [ProposalId, MemberId]>;
+      voteExistsByProposalByVoter: AugmentedQuery<ApiType, (arg1: ProposalId | AnyNumber | Uint8Array, arg2: MemberId | AnyNumber | Uint8Array) => Observable<VoteKind>, [ProposalId, MemberId]>;
     };
     randomnessCollectiveFlip: {
       /**
@@ -857,7 +857,7 @@ declare module '@polkadot/api/types/storage' {
        * Is it removed after `HISTORY_DEPTH` eras.
        * If stakers hasn't been set or has been removed then empty exposure is returned.
        **/
-      erasStakers: AugmentedQueryDoubleMap<ApiType, (key1: EraIndex | AnyNumber | Uint8Array, key2: AccountId | string | Uint8Array) => Observable<Exposure>, [EraIndex, AccountId]>;
+      erasStakers: AugmentedQuery<ApiType, (arg1: EraIndex | AnyNumber | Uint8Array, arg2: AccountId | string | Uint8Array) => Observable<Exposure>, [EraIndex, AccountId]>;
       /**
        * Clipped Exposure of validator at era.
        * 
@@ -871,7 +871,7 @@ declare module '@polkadot/api/types/storage' {
        * Is it removed after `HISTORY_DEPTH` eras.
        * If stakers hasn't been set or has been removed then empty exposure is returned.
        **/
-      erasStakersClipped: AugmentedQueryDoubleMap<ApiType, (key1: EraIndex | AnyNumber | Uint8Array, key2: AccountId | string | Uint8Array) => Observable<Exposure>, [EraIndex, AccountId]>;
+      erasStakersClipped: AugmentedQuery<ApiType, (arg1: EraIndex | AnyNumber | Uint8Array, arg2: AccountId | string | Uint8Array) => Observable<Exposure>, [EraIndex, AccountId]>;
       /**
        * The session index at which the era start for the last `HISTORY_DEPTH` eras.
        **/
@@ -888,7 +888,7 @@ declare module '@polkadot/api/types/storage' {
        * 
        * Is it removed after `HISTORY_DEPTH` eras.
        **/
-      erasValidatorPrefs: AugmentedQueryDoubleMap<ApiType, (key1: EraIndex | AnyNumber | Uint8Array, key2: AccountId | string | Uint8Array) => Observable<ValidatorPrefs>, [EraIndex, AccountId]>;
+      erasValidatorPrefs: AugmentedQuery<ApiType, (arg1: EraIndex | AnyNumber | Uint8Array, arg2: AccountId | string | Uint8Array) => Observable<ValidatorPrefs>, [EraIndex, AccountId]>;
       /**
        * The total validator era payout for the last `HISTORY_DEPTH` eras.
        * 
@@ -935,7 +935,7 @@ declare module '@polkadot/api/types/storage' {
       /**
        * All slashing events on nominators, mapped by era to the highest slash value of the era.
        **/
-      nominatorSlashInEra: AugmentedQueryDoubleMap<ApiType, (key1: EraIndex | AnyNumber | Uint8Array, key2: AccountId | string | Uint8Array) => Observable<Option<BalanceOf>>, [EraIndex, AccountId]>;
+      nominatorSlashInEra: AugmentedQuery<ApiType, (arg1: EraIndex | AnyNumber | Uint8Array, arg2: AccountId | string | Uint8Array) => Observable<Option<BalanceOf>>, [EraIndex, AccountId]>;
       /**
        * Where the reward payment should be made. Keyed by stash.
        **/
@@ -998,7 +998,7 @@ declare module '@polkadot/api/types/storage' {
        * All slashing events on validators, mapped by era to the highest slash proportion
        * and slash value of the era.
        **/
-      validatorSlashInEra: AugmentedQueryDoubleMap<ApiType, (key1: EraIndex | AnyNumber | Uint8Array, key2: AccountId | string | Uint8Array) => Observable<Option<ITuple<[Perbill, BalanceOf]>>>, [EraIndex, AccountId]>;
+      validatorSlashInEra: AugmentedQuery<ApiType, (arg1: EraIndex | AnyNumber | Uint8Array, arg2: AccountId | string | Uint8Array) => Observable<Option<ITuple<[Perbill, BalanceOf]>>>, [EraIndex, AccountId]>;
     };
     storageWorkingGroup: {
       /**

+ 31 - 5
types/augment/augment-api-rpc.ts

@@ -1,15 +1,15 @@
 // Auto-generated via `yarn polkadot-types-from-chain`, do not edit
 /* eslint-disable */
 
-import type { Metadata } from '@polkadot/metadata';
-import type { Bytes, HashMap, Json, Null, Option, StorageKey, Text, U256, U64, Vec, bool, u32, u64 } from '@polkadot/types';
+import type { Bytes, HashMap, Json, Metadata, Null, Option, StorageKey, Text, U256, U64, Vec, bool, u32, u64 } from '@polkadot/types';
 import type { AnyNumber, Codec, IExtrinsic, Observable } from '@polkadot/types/types';
 import type { ExtrinsicOrHash, ExtrinsicStatus } from '@polkadot/types/interfaces/author';
 import type { EpochAuthorship } from '@polkadot/types/interfaces/babe';
+import type { BeefySignedCommitment } from '@polkadot/types/interfaces/beefy';
 import type { BlockHash } from '@polkadot/types/interfaces/chain';
 import type { PrefixedStorageKey } from '@polkadot/types/interfaces/childstate';
 import type { AuthorityId } from '@polkadot/types/interfaces/consensus';
-import type { ContractCallRequest, ContractExecResult } from '@polkadot/types/interfaces/contracts';
+import type { ContractCallRequest, ContractExecResult, ContractInstantiateResult, InstantiateRequest } from '@polkadot/types/interfaces/contracts';
 import type { CreatedBlock } from '@polkadot/types/interfaces/engine';
 import type { EthAccount, EthCallRequest, EthFilter, EthFilterChanges, EthLog, EthReceipt, EthRichBlock, EthSubKind, EthSubParams, EthSyncStatus, EthTransaction, EthTransactionRequest, EthWork } from '@polkadot/types/interfaces/eth';
 import type { Extrinsic } from '@polkadot/types/interfaces/extrinsics';
@@ -19,7 +19,7 @@ import type { StorageKind } from '@polkadot/types/interfaces/offchain';
 import type { FeeDetails, RuntimeDispatchInfo } from '@polkadot/types/interfaces/payment';
 import type { RpcMethods } from '@polkadot/types/interfaces/rpc';
 import type { AccountId, BlockNumber, H160, H256, H64, Hash, Header, Index, Justification, KeyValue, SignedBlock, StorageData } from '@polkadot/types/interfaces/runtime';
-import type { ReadProof, RuntimeVersion } from '@polkadot/types/interfaces/state';
+import type { ReadProof, RuntimeVersion, TraceBlockResponse } from '@polkadot/types/interfaces/state';
 import type { ApplyExtrinsicResult, ChainProperties, ChainType, Health, NetworkState, NodeRole, PeerInfo, SyncState } from '@polkadot/types/interfaces/system';
 
 declare module '@polkadot/rpc-core/types.jsonrpc' {
@@ -64,6 +64,12 @@ declare module '@polkadot/rpc-core/types.jsonrpc' {
        **/
       epochAuthorship: AugmentedRpc<() => Observable<HashMap<AuthorityId, EpochAuthorship>>>;
     };
+    beefy: {
+      /**
+       * Returns the block most recently finalized by BEEFY, alongside side its justification.
+       **/
+      subscribeJustifications: AugmentedRpc<() => Observable<BeefySignedCommitment>>;
+    };
     chain: {
       /**
        * Get header and body of a relay chain block
@@ -99,6 +105,10 @@ declare module '@polkadot/rpc-core/types.jsonrpc' {
        * Returns the keys with prefix from a child storage, leave empty to get all the keys
        **/
       getKeys: AugmentedRpc<(childKey: PrefixedStorageKey | string | Uint8Array, prefix: StorageKey | string | Uint8Array | any, at?: Hash | string | Uint8Array) => Observable<Vec<StorageKey>>>;
+      /**
+       * Returns the keys with prefix from a child storage with pagination support
+       **/
+      getKeysPaged: AugmentedRpc<(childKey: PrefixedStorageKey | string | Uint8Array, prefix: StorageKey | string | Uint8Array | any, count: u32 | AnyNumber | Uint8Array, startKey?: StorageKey | string | Uint8Array | any, at?: Hash | string | Uint8Array) => Observable<Vec<StorageKey>>>;
       /**
        * Returns a child storage entry at a specific block state
        **/
@@ -121,6 +131,10 @@ declare module '@polkadot/rpc-core/types.jsonrpc' {
        * Returns the value under a specified storage key in a contract
        **/
       getStorage: AugmentedRpc<(address: AccountId | string | Uint8Array, key: H256 | string | Uint8Array, at?: BlockHash | string | Uint8Array) => Observable<Option<Bytes>>>;
+      /**
+       * Instantiate a new contract
+       **/
+      instantiate: AugmentedRpc<(request: InstantiateRequest | { origin?: any; endowment?: any; gasLimit?: any; code?: any; data?: any; salt?: any } | string | Uint8Array, at?: BlockHash | string | Uint8Array) => Observable<ContractInstantiateResult>>;
       /**
        * Returns the projected time a given contract will be able to sustain paying its rent
        **/
@@ -142,7 +156,7 @@ declare module '@polkadot/rpc-core/types.jsonrpc' {
        **/
       accounts: AugmentedRpc<() => Observable<Vec<H160>>>;
       /**
-       * Returns balance of the given account.
+       * Returns the blockNumber
        **/
       blockNumber: AugmentedRpc<() => Observable<U256>>;
       /**
@@ -371,6 +385,10 @@ declare module '@polkadot/rpc-core/types.jsonrpc' {
        * Retrieves the keys with prefix of a specific child storage
        **/
       getChildKeys: AugmentedRpc<(childStorageKey: StorageKey | string | Uint8Array | any, childDefinition: StorageKey | string | Uint8Array | any, childType: u32 | AnyNumber | Uint8Array, key: StorageKey | string | Uint8Array | any, at?: BlockHash | string | Uint8Array) => Observable<Vec<StorageKey>>>;
+      /**
+       * Returns proof of storage for child key entries at a specific block state.
+       **/
+      getChildReadProof: AugmentedRpc<(childStorageKey: PrefixedStorageKey | string | Uint8Array, keys: Vec<StorageKey> | (StorageKey | string | Uint8Array | any)[], at?: BlockHash | string | Uint8Array) => Observable<ReadProof>>;
       /**
        * Retrieves the child storage for a key
        **/
@@ -435,6 +453,10 @@ declare module '@polkadot/rpc-core/types.jsonrpc' {
        * Subscribes to storage changes for the provided keys
        **/
       subscribeStorage: AugmentedRpc<<T = Codec[]>(keys?: Vec<StorageKey> | (StorageKey | string | Uint8Array | any)[]) => Observable<T>>;
+      /**
+       * Provides a way to trace the re-execution of a single block
+       **/
+      traceBlock: AugmentedRpc<(block: Hash | string | Uint8Array, targets: Option<Text> | null | object | string | Uint8Array, storageKeys: Option<Text> | null | object | string | Uint8Array) => Observable<TraceBlockResponse>>;
     };
     syncstate: {
       /**
@@ -503,6 +525,10 @@ declare module '@polkadot/rpc-core/types.jsonrpc' {
        * Remove a reserved peer
        **/
       removeReservedPeer: AugmentedRpc<(peerId: Text | string) => Observable<Text>>;
+      /**
+       * Returns the list of reserved peers
+       **/
+      reservedPeers: AugmentedRpc<() => Observable<Vec<Text>>>;
       /**
        * Resets the log filter to Substrate defaults
        **/

Some files were not shown because too many files changed in this diff