diff --git a/src/components/Avatar.scss b/src/components/Avatar.scss
index 794064aa6030e767cca36210e44872493c2b9fbb..6767f2a686ddc9fb62b4ba7a66d1ebb053c7e15b 100644
--- a/src/components/Avatar.scss
+++ b/src/components/Avatar.scss
@@ -25,4 +25,5 @@ limitations under the License.
 
 .avatarNoCrop {
     border-radius: 0;
+    border: 0;
 }
diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx
index f150c6333e1d2998919cf8f3e58ad5fb3230c902..222790c789b083d986fc722273e3a5ceff7d7c8c 100644
--- a/src/components/Avatar.tsx
+++ b/src/components/Avatar.tsx
@@ -16,7 +16,7 @@ limitations under the License.
 
 import React, { useEffect, useState } from 'react';
 import classNames from 'classnames';
-import { Room, User } from '../matrix-cypher';
+import { Group, Room, User } from '../matrix-cypher';
 
 import { getMediaQueryFromMCX } from '../utils/cypher-wrapper';
 import logo from '../imgs/chat-icon.svg';
@@ -35,12 +35,15 @@ const Avatar: React.FC<IProps> = ({ className, avatarUrl, label }: IProps) => {
         setSrc(avatarUrl);
     }, [avatarUrl]);
 
+    const _className = classNames('avatar', className, {
+        avatarNoCrop: src === logo,
+    });
     return (
         <img
             src={src}
             onError={(): void => setSrc(logo)}
             alt={label}
-            className={classNames('avatar', className)}
+            className={_className}
         />
     );
 };
@@ -73,4 +76,17 @@ export const RoomAvatar: React.FC<IPropsRoomAvatar> = ({
     />
 );
 
+interface IPropsGroupAvatar {
+    group: Group;
+}
+
+export const GroupAvatar: React.FC<IPropsGroupAvatar> = ({
+    group,
+}: IPropsGroupAvatar) => (
+    <Avatar
+        avatarUrl={getMediaQueryFromMCX(group.avatar_url)}
+        label={group.name}
+    />
+);
+
 export default Avatar;
diff --git a/src/components/GroupPreview.scss b/src/components/GroupPreview.scss
new file mode 100644
index 0000000000000000000000000000000000000000..aae3aba17ed7294bb7811f83483eee8a01c65201
--- /dev/null
+++ b/src/components/GroupPreview.scss
@@ -0,0 +1,26 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.groupPreview {
+    > .avatar {
+        margin-bottom: 8px;
+    }
+
+    > h1 {
+        font-size: 24px;
+        margin-bottom: 4px;
+    }
+}
diff --git a/src/components/GroupPreview.tsx b/src/components/GroupPreview.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..91596cf2f858d6a0ae4144aa7024b23e3960f58b
--- /dev/null
+++ b/src/components/GroupPreview.tsx
@@ -0,0 +1,46 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from 'react';
+import { Group } from '../matrix-cypher';
+
+import { GroupAvatar } from './Avatar';
+
+import './GroupPreview.scss';
+
+interface IProps {
+    group: Group;
+}
+
+const GroupPreview: React.FC<IProps> = ({ group }: IProps) => {
+    const description = group.long_description
+        ? group.long_description
+        : group.short_description
+        ? group.short_description
+        : null;
+
+    const descriptionP = description ? <p>{description}</p> : null;
+
+    return (
+        <div className="groupPreview">
+            <GroupAvatar group={group} />
+            <h1>{group.name}</h1>
+            {descriptionP}
+        </div>
+    );
+};
+
+export default GroupPreview;
diff --git a/src/components/LinkPreview.tsx b/src/components/LinkPreview.tsx
index ae4170c8e4462624017c1d02e1b3aa3b624bf496..0372221e7301a1d8faaa5c150a1239176a11cb1c 100644
--- a/src/components/LinkPreview.tsx
+++ b/src/components/LinkPreview.tsx
@@ -22,6 +22,7 @@ import InviteTile from './InviteTile';
 import { SafeLink, LinkKind } from '../parser/types';
 import UserPreview, { WrappedInviterPreview } from './UserPreview';
 import EventPreview from './EventPreview';
+import GroupPreview from './GroupPreview';
 import HomeserverOptions from './HomeserverOptions';
 import DefaultPreview from './DefaultPreview';
 import Toggle from './Toggle';
@@ -31,6 +32,7 @@ import {
     getRoomFromAlias,
     getRoomFromPermalink,
     getUser,
+    getGroup,
 } from '../utils/cypher-wrapper';
 import { ClientContext } from '../contexts/ClientContext';
 import useHSs from '../utils/getHS';
@@ -86,6 +88,13 @@ const invite = async ({
                 />
             );
 
+        case LinkKind.GroupId:
+            return (
+                <GroupPreview
+                    group={await getGroup(clientAddress, link.identifier)}
+                />
+            );
+
         default:
             // Todo Implement events
             return <></>;
diff --git a/src/matrix-cypher/matrix-cypher.ts b/src/matrix-cypher/matrix-cypher.ts
index 75387a2047fcf749dd44b0f532395fedaf91871d..4eafb0345fc58ebedf251d68caad75de54183e3e 100644
--- a/src/matrix-cypher/matrix-cypher.ts
+++ b/src/matrix-cypher/matrix-cypher.ts
@@ -14,30 +14,24 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-/* eslint-disable import/first */
-
-// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
 // @ts-ignore
 import any from 'promise.any';
-any.shim()
+any.shim();
 
 import VersionSchema from './schemas/VersionSchema';
 import WellKnownSchema from './schemas/WellKnownSchema';
 import UserSchema, { User } from './schemas/UserSchema';
-import RoomAliasSchema, {
-    RoomAlias,
-} from './schemas/RoomAliasSchema';
+import RoomAliasSchema, { RoomAlias } from './schemas/RoomAliasSchema';
 import PublicRoomsSchema, {
     PublicRooms,
     Room,
 } from './schemas/PublicRoomsSchema';
-import EventSchema, {
-    Event,
-} from './schemas/EventSchema';
+import EventSchema, { Event } from './schemas/EventSchema';
+import GroupSchema, { Group } from './schemas/GroupSchema';
 import { ensure } from './utils/promises';
 import { prefixFetch, parseJSON } from './utils/fetch';
 
-
 /*
  * A client is a resolved homeserver name wrapped in a lambda'd fetch
  */
@@ -46,7 +40,7 @@ export type Client = (path: string) => Promise<Response>;
 /*
  * Confirms that the target homeserver is properly configured and operational
  */
-export const validateHS = (host: string) =>
+export const validateHS = (host: string): Promise<string> =>
     prefixFetch(host)('/_matrix/client/versions')
         .then(parseJSON)
         .then(VersionSchema.parse)
@@ -56,47 +50,43 @@ export const validateHS = (host: string) =>
  * Discovers the correct domain name for the host according to the spec's
  * discovery rules
  */
-export const discoverServer = (host: string) =>
+export const discoverServer = (host: string): Promise<string> =>
     prefixFetch(host)('/.well-known/matrix/client')
-        .then(resp => resp.ok
-            ? resp.json()
-                .then(WellKnownSchema.parse)
-                .then(content => {
-                    if (content === undefined) return host;
-                    else if (
-                        'm.homeserver' in content && content['m.homeserver']
-                    ) {
-                        return content['m.homeserver'].base_url
-                    } else {
-                        return host
-                    }
-                })
-            : ensure(
-                resp.status === 404,
-                () => host,
-            ),
+        .then((resp) =>
+            resp.ok
+                ? resp
+                      .json()
+                      .then(WellKnownSchema.parse)
+                      .then((content) => {
+                          if (content === undefined) return host;
+                          else if (
+                              'm.homeserver' in content &&
+                              content['m.homeserver']
+                          ) {
+                              return content['m.homeserver'].base_url;
+                          } else {
+                              return host;
+                          }
+                      })
+                : ensure(resp.status === 404, () => host)
         )
-        .then(validateHS)
-
+        .then(validateHS);
 
 /*
  * Takes a hs domain and resolves it to it's current domain and returns a
  * client
  */
 export async function client(host: string): Promise<Client> {
-    return prefixFetch(await discoverServer(host))
+    return prefixFetch(await discoverServer(host));
 }
 
 /*
  * Gets the details for a user
  */
-export function getUserDetails(
-    client: Client,
-    userId: string,
-): Promise<User> {
+export function getUserDetails(client: Client, userId: string): Promise<User> {
     return client(`/_matrix/client/r0/profile/${userId}`)
         .then(parseJSON)
-        .then(UserSchema.parse)
+        .then(UserSchema.parse);
 }
 
 /*
@@ -104,7 +94,7 @@ export function getUserDetails(
  */
 export function getRoomIdFromAlias(
     client: Client,
-    roomAlias: string,
+    roomAlias: string
 ): Promise<RoomAlias> {
     const encodedRoomAlias = encodeURIComponent(roomAlias);
     return client(`/_matrix/client/r0/directory/room/${encodedRoomAlias}`)
@@ -112,23 +102,6 @@ export function getRoomIdFromAlias(
         .then(RoomAliasSchema.parse);
 }
 
-/*
- * Gets the details of a room if that room is public
- */
-export function getRoomDetails(clients: Client[], roomId: string): Promise<Room> {
-    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-    // @ts-ignore
-    return Promise.any(clients.map(client => searchPublicRooms(client, roomId)));
-}
-
-/*
- * Gets a list of all public rooms on a hs
- */
-export function getPublicRooms(client: Client): Promise<PublicRooms> {
-    return getPublicRoomsUnsafe(client)
-        .then(PublicRoomsSchema.parse)
-}
-
 /*
  * Similar to getPubliRooms however id doesn't confirm the data returned from
  * the hs is correct
@@ -138,8 +111,14 @@ export function getPublicRooms(client: Client): Promise<PublicRooms> {
  */
 export function getPublicRoomsUnsafe(client: Client): Promise<PublicRooms> {
     // TODO: Do not assume server will return all results in one go
-    return client('/_matrix/client/r0/publicRooms')
-        .then(parseJSON)
+    return client('/_matrix/client/r0/publicRooms').then(parseJSON);
+}
+
+/*
+ * Gets a list of all public rooms on a hs
+ */
+export function getPublicRooms(client: Client): Promise<PublicRooms> {
+    return getPublicRoomsUnsafe(client).then(PublicRoomsSchema.parse);
 }
 
 /*
@@ -147,20 +126,33 @@ export function getPublicRoomsUnsafe(client: Client): Promise<PublicRooms> {
  */
 export function searchPublicRooms(
     client: Client,
-    roomId: string,
+    roomId: string
 ): Promise<Room> {
     // we use the unsage version here because the safe one is sloooow
-    return getPublicRoomsUnsafe(client)
-        .then(rooms => {
-            const [match] = rooms.chunk.filter(
-                chunk => chunk.room_id === roomId,
-            );
-            return match !== undefined
-                ? Promise.resolve(match)
-                : Promise.reject(new Error(
-                    `This server knowns no public room with id ${roomId}`,
-                ));
-        });
+    return getPublicRoomsUnsafe(client).then((rooms) => {
+        const [match] = rooms.chunk.filter((chunk) => chunk.room_id === roomId);
+        return match !== undefined
+            ? Promise.resolve(match)
+            : Promise.reject(
+                  new Error(
+                      `This server knowns no public room with id ${roomId}`
+                  )
+              );
+    });
+}
+
+/*
+ * Gets the details of a room if that room is public
+ */
+export function getRoomDetails(
+    clients: Client[],
+    roomId: string
+): Promise<Room> {
+    // eslint-disable-next-line @typescript-eslint/ban-ts-ignore
+    // @ts-ignore
+    return Promise.any(
+        clients.map((client) => searchPublicRooms(client, roomId))
+    );
 }
 
 /*
@@ -169,24 +161,33 @@ export function searchPublicRooms(
 export async function getEvent(
     client: Client,
     roomIdOrAlias: string,
-    eventId: string,
+    eventId: string
 ): Promise<Event> {
     return client(`/_matrix/client/r0/rooms/${roomIdOrAlias}/event/${eventId}`)
         .then(parseJSON)
         .then(EventSchema.parse);
 }
 
+/*
+ * Gets community information
+ */
+export async function getGroupDetails(
+    client: Client,
+    groupId: string
+): Promise<Group> {
+    return client(`/_matrix/client/r0/groups/${groupId}/profile`)
+        .then(parseJSON)
+        .then(GroupSchema.parse);
+}
+
 /*
  * Gets an mxc resource
  */
-export function convertMXCtoMediaQuery(
-    clientURL: string,
-    mxc: string,
-): string {
+export function convertMXCtoMediaQuery(clientURL: string, mxc: string): string {
     // mxc://matrix.org/EqMZYbAYhREvHXvYFyfxOlkf
-    const matches = mxc.match(/mxc:\/\/(.+)\/(.+)/)
+    const matches = mxc.match(/mxc:\/\/(.+)\/(.+)/);
     if (!matches) {
-        throw new Error(`mxc invalid: ${JSON.stringify({mxc})}`);
+        throw new Error(`mxc invalid: ${JSON.stringify({ mxc })}`);
     }
 
     return `${clientURL}/_matrix/media/r0/download/${matches[1]}/${matches[2]}`;
diff --git a/src/matrix-cypher/schemas/GroupSchema.ts b/src/matrix-cypher/schemas/GroupSchema.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e17c116c5fea7060d9a8a1b9e93cf4eea8a36e00
--- /dev/null
+++ b/src/matrix-cypher/schemas/GroupSchema.ts
@@ -0,0 +1,27 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { object, string, TypeOf } from 'zod';
+
+const GroupSchema = object({
+    name: string(),
+    avatar_url: string().optional(),
+    short_description: string().optional(),
+    long_description: string().optional(),
+});
+
+export type Group = TypeOf<typeof GroupSchema>;
+export default GroupSchema;
diff --git a/src/matrix-cypher/schemas/index.ts b/src/matrix-cypher/schemas/index.ts
index f7fa3c32a40b8a0ab33f28522c4178fda604680b..34281fb9719f7f50bb51844fcbff0f7c25a51449 100644
--- a/src/matrix-cypher/schemas/index.ts
+++ b/src/matrix-cypher/schemas/index.ts
@@ -15,10 +15,10 @@ limitations under the License.
 */
 
 export * from './EventSchema';
+export * from './GroupSchema';
 export * from './PublicRoomsSchema';
 export * from './RoomAliasSchema';
 export * from './UserSchema';
 export * from './VersionSchema';
 export * from './WellKnownSchema';
 export * from './index';
-
diff --git a/src/utils/cypher-wrapper.ts b/src/utils/cypher-wrapper.ts
index 7e1093eedd09ab4f8c3ee7b6bcfa95cd5a98b189..0e30ba0b2873106d92c0d2788ce2786707ef4248 100644
--- a/src/utils/cypher-wrapper.ts
+++ b/src/utils/cypher-wrapper.ts
@@ -24,10 +24,12 @@ import {
     Room,
     RoomAlias,
     User,
+    Group,
     getRoomIdFromAlias,
     searchPublicRooms,
     getUserDetails,
     convertMXCtoMediaQuery,
+    getGroupDetails,
 } from '../matrix-cypher';
 import { LinkKind, Permalink } from '../parser/types';
 
@@ -72,6 +74,11 @@ export const fallbackRoom = ({
     };
 };
 
+export const fallbackGroup = (groupId: string): Group => ({
+    name: groupId,
+    short_description: `The ${groupId} group`,
+});
+
 /*
  * Tries to fetch room details from an alias. If it fails it uses
  * a `fallbackRoom`
@@ -169,3 +176,15 @@ export function getMediaQueryFromMCX(mxc?: string): string {
         return '';
     }
 }
+
+export async function getGroup(
+    clientURL: string,
+    groupId: string
+): Promise<Group> {
+    try {
+        const resolvedClient = await client(clientURL);
+        return await getGroupDetails(resolvedClient, groupId);
+    } catch {
+        return fallbackGroup(groupId);
+    }
+}