${noprofileinfo ? generateLogoHero() : `
- ${generateMetadataHeader(fetchCachedProfileEvent(0) as Event)}
+ ${generateMetadataHeader(fetchCachedMyProfileEvent(0) as Event)}
- ${generateMetadataSummary(fetchCachedProfileEvent(0), !uptodate)}
- ${generateContactsSummary(fetchCachedProfileEvent(3), !uptodate)}
- ${generateRelaysSummary(fetchCachedProfileEvent(10002), !uptodate)}
+ ${generateMetadataSummary(fetchCachedMyProfileEvent(0), !uptodate)}
+ ${generateContactsSummary(fetchCachedMyProfileEvent(3), !uptodate)}
+ ${generateRelaysSummary(fetchCachedMyProfileEvent(10002), !uptodate)}
`}
${generateBackupHeroHeading(uptodate, noprofileinfo, hadlatest)}
@@ -158,10 +158,10 @@ export const LoadProfileHome = () => {
donwloada.onclick = (event) => {
event.preventDefault();
const jsonStr = JSON.stringify([
- ...(fetchCachedProfileEventHistory(0) || []),
- ...(fetchCachedProfileEventHistory(2) || []),
- ...(fetchCachedProfileEventHistory(10002) || []),
- ...(fetchCachedProfileEventHistory(3) || []),
+ ...(fetchCachedMyProfileEventHistory(0) || []),
+ ...(fetchCachedMyProfileEventHistory(2) || []),
+ ...(fetchCachedMyProfileEventHistory(10002) || []),
+ ...(fetchCachedMyProfileEventHistory(3) || []),
]);
const element = document.createElement('a');
element.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(jsonStr)}`);
diff --git a/src/LoadRelaysPage.ts b/src/LoadRelaysPage.ts
index b3c400a..8feebf3 100644
--- a/src/LoadRelaysPage.ts
+++ b/src/LoadRelaysPage.ts
@@ -1,4 +1,4 @@
-import { fetchCachedProfileEvent, submitUnsignedEvent } from './fetchEvents';
+import { fetchCachedMyProfileEvent, submitUnsignedEvent } from './fetchEvents';
import { Kind10002Event, Kind10002Tag, loadBackupHistory } from './LoadHistory';
import { localStorageGetItem } from './LocalStorage';
@@ -94,7 +94,7 @@ const loadRelayForm = (RootElementID:string) => {
(document.getElementById(RootElementID) as HTMLDivElement)
.innerHTML = `
Relays
- ${generateRelayForm(fetchCachedProfileEvent(10002) as Kind10002Event)}
+ ${generateRelayForm(fetchCachedMyProfileEvent(10002) as Kind10002Event)}
`;
// form submit event
(document.getElementById('relayssubmitbutton') as HTMLButtonElement).onclick = (event) => {
diff --git a/src/fetchEvents.test.ts b/src/fetchEvents.test.ts
index 4906d9b..25fa3e2 100644
--- a/src/fetchEvents.test.ts
+++ b/src/fetchEvents.test.ts
@@ -1,7 +1,7 @@
import { Event } from 'nostr-tools';
import SampleEvents from './SampleEvents';
import {
- storeMyProfileEvent, fetchCachedProfileEventHistory, fetchCachedProfileEvent,
+ storeMyProfileEvent, fetchCachedMyProfileEventHistory, fetchCachedMyProfileEvent,
fetchMyProfileEvents,
} from './fetchEvents';
import * as LocalStorage from './LocalStorage';
@@ -43,7 +43,7 @@ describe('', () => {
describe('stores event, returns true and calls updateLastUpdateDate() when', () => {
test('first event submitted', () => {
const r = storeMyProfileEvent({ ...SampleEvents[`kind${kind}`] });
- expect(fetchCachedProfileEventHistory(kind)).toEqual([{ ...SampleEvents[`kind${kind}`] }]);
+ expect(fetchCachedMyProfileEventHistory(kind)).toEqual([{ ...SampleEvents[`kind${kind}`] }]);
expect(r).toBeTruthy();
expect(updateLastUpdatedSpy).toHaveBeenCalledTimes(1);
});
@@ -56,7 +56,7 @@ describe('', () => {
storeMyProfileEvent(a[0]);
const r2 = storeMyProfileEvent(a[1]);
const r3 = storeMyProfileEvent(a[2]);
- const r = fetchCachedProfileEventHistory(kind);
+ const r = fetchCachedMyProfileEventHistory(kind);
expect(r).toContainEqual(a[0]);
expect(r).toContainEqual(a[1]);
expect(r).toContainEqual(a[2]);
@@ -69,14 +69,14 @@ describe('', () => {
test('event from a different pubkey submitted', () => {
const r = storeMyProfileEvent({ ...SampleEvents[`kind${kind}`], pubkey: '1' });
expect(r).toBeFalsy();
- expect(fetchCachedProfileEventHistory(kind)).toBeNull();
+ expect(fetchCachedMyProfileEventHistory(kind)).toBeNull();
expect(updateLastUpdatedSpy).toHaveBeenCalledTimes(0);
});
test('duplicate events (events with the same id)', () => {
storeMyProfileEvent({ ...SampleEvents[`kind${kind}`] });
const r = storeMyProfileEvent({ ...SampleEvents[`kind${kind}`], content: 'different' });
expect(r).toBeFalsy();
- expect(fetchCachedProfileEventHistory(kind)).toEqual([{ ...SampleEvents[`kind${kind}`] }]);
+ expect(fetchCachedMyProfileEventHistory(kind)).toEqual([{ ...SampleEvents[`kind${kind}`] }]);
expect(updateLastUpdatedSpy).toHaveBeenCalledTimes(1);
});
});
@@ -86,11 +86,11 @@ describe('', () => {
expect(updateLastUpdatedSpy).toHaveBeenCalledTimes(0);
});
});
- describe('fetchCachedProfileEventHistory', () => {
+ describe('fetchCachedMyProfileEventHistory', () => {
describe('returns array of events', () => {
test('single event', () => {
storeMyProfileEvent({ ...SampleEvents.kind0 });
- expect(fetchCachedProfileEventHistory(0)).toEqual([{ ...SampleEvents.kind0 }]);
+ expect(fetchCachedMyProfileEventHistory(0)).toEqual([{ ...SampleEvents.kind0 }]);
});
test('multiple events', () => {
const a = [
@@ -101,7 +101,7 @@ describe('', () => {
storeMyProfileEvent(a[0]);
storeMyProfileEvent(a[1]);
storeMyProfileEvent(a[2]);
- const r = fetchCachedProfileEventHistory(0);
+ const r = fetchCachedMyProfileEventHistory(0);
expect(r).toContainEqual(a[0]);
expect(r).toContainEqual(a[1]);
expect(r).toContainEqual(a[2]);
@@ -115,10 +115,10 @@ describe('', () => {
storeMyProfileEvent(a[0]);
storeMyProfileEvent(a[1]);
storeMyProfileEvent(a[2]);
- const r = fetchCachedProfileEventHistory(0);
+ const r = fetchCachedMyProfileEventHistory(0);
expect(r).toContainEqual(a[0]);
expect(r).toContainEqual(a[1]);
- expect(fetchCachedProfileEventHistory(3)).toContainEqual(a[2]);
+ expect(fetchCachedMyProfileEventHistory(3)).toContainEqual(a[2]);
});
test('ordered by created_at decending', () => {
const a = [
@@ -129,7 +129,7 @@ describe('', () => {
storeMyProfileEvent({ ...a[0] });
storeMyProfileEvent({ ...a[1] });
storeMyProfileEvent({ ...a[2] });
- const r = fetchCachedProfileEventHistory(0) as Event[];
+ const r = fetchCachedMyProfileEventHistory(0) as Event[];
expect(r[0]).toEqual(a[1]);
expect(r[1]).toEqual(a[2]);
expect(r[2]).toEqual(a[0]);
@@ -137,10 +137,10 @@ describe('', () => {
});
test('returns null if no events of kind present', () => {
storeMyProfileEvent({ ...SampleEvents.kind0 });
- expect(fetchCachedProfileEventHistory(3)).toBeNull();
+ expect(fetchCachedMyProfileEventHistory(3)).toBeNull();
});
});
- describe('fetchCachedProfileEvent', () => {
+ describe('fetchCachedMyProfileEvent', () => {
test('returns event of specified kind with largest created_at value', () => {
const a = [
{ ...SampleEvents.kind0 },
@@ -150,23 +150,23 @@ describe('', () => {
storeMyProfileEvent({ ...a[0] });
storeMyProfileEvent({ ...a[1] });
storeMyProfileEvent({ ...a[2] });
- const r = fetchCachedProfileEvent(0) as Event;
+ const r = fetchCachedMyProfileEvent(0) as Event;
expect(r).toEqual(a[1]);
});
test('returns null if no events of kind present', () => {
storeMyProfileEvent({ ...SampleEvents.kind0 });
- expect(fetchCachedProfileEventHistory(3)).toBeNull();
+ expect(fetchCachedMyProfileEventHistory(3)).toBeNull();
});
});
describe('fetchMyProfileEvents', () => {
- const fetchCachedProfileEventSpy = jest.spyOn(FetchEvents, 'fetchCachedProfileEvent');
+ const fetchCachedMyProfileEventSpy = jest.spyOn(FetchEvents, 'fetchCachedMyProfileEvent');
const mockEventProcessor = jest.fn();
const mockrequestEventsFromRelays = jest.spyOn(RelayManagement, 'requestEventsFromRelays');
const mockupdateLastFetchDate = jest.spyOn(FetchEvents, 'updateLastFetchDate');
const mocklastFetchDate = jest.spyOn(FetchEvents, 'lastFetchDate');
const mockisUptodate = jest.spyOn(FetchEvents, 'isUptodate');
beforeEach(async () => {
- fetchCachedProfileEventSpy.mockReset();
+ fetchCachedMyProfileEventSpy.mockReset();
mockEventProcessor.mockReset();
mockrequestEventsFromRelays.mockReset();
mockupdateLastFetchDate.mockReset();
@@ -176,7 +176,7 @@ describe('', () => {
describe('when isUptodate returns true', () => {
beforeEach(async () => {
mockisUptodate.mockReturnValue(true);
- fetchCachedProfileEventSpy.mockImplementation((kind) => {
+ fetchCachedMyProfileEventSpy.mockImplementation((kind) => {
if (kind === 0) return { ...SampleEvents.kind0 };
if (kind === 10002) return null;
if (kind === 2) return null;
@@ -192,15 +192,15 @@ describe('', () => {
expect(mockupdateLastFetchDate).toBeCalledTimes(0);
});
test('eventProcessor called with latest event from cache for each profile kind with event(s)', async () => {
- expect(fetchCachedProfileEventSpy).toHaveBeenCalledWith(0);
- expect(fetchCachedProfileEventSpy).toHaveBeenCalledWith(3);
+ expect(fetchCachedMyProfileEventSpy).toHaveBeenCalledWith(0);
+ expect(fetchCachedMyProfileEventSpy).toHaveBeenCalledWith(3);
expect(mockEventProcessor).toBeCalledWith({ ...SampleEvents.kind0 });
expect(mockEventProcessor).toBeCalledWith({ ...SampleEvents.kind3 });
expect(mockEventProcessor).toHaveBeenCalledTimes(2);
});
test('eventProcessor not called on profile event kind that isn\'t present', async () => {
- expect(fetchCachedProfileEventSpy).toHaveBeenCalledWith(10002);
- expect(fetchCachedProfileEventSpy).toHaveBeenCalledWith(2);
+ expect(fetchCachedMyProfileEventSpy).toHaveBeenCalledWith(10002);
+ expect(fetchCachedMyProfileEventSpy).toHaveBeenCalledWith(2);
expect(mockEventProcessor).toBeCalledTimes(2);
});
});
@@ -215,7 +215,7 @@ describe('', () => {
});
await fetchMyProfileEvents(SampleEvents.kind0.pubkey, mockEventProcessor);
};
- test('1 write relays, function called with custom relay and 2 default relays', async () => {
+ test('1 relay, function called with custom relay and 2 default relays', async () => {
storeMyProfileEvent({
...SampleEvents.kind10002,
tags: [
@@ -234,7 +234,7 @@ describe('', () => {
expect.anything(),
);
});
- test('2 write relays function called with custom relays and 1 default relays', async () => {
+ test('2 relays function called with custom relays and 1 default relays', async () => {
storeMyProfileEvent({
...SampleEvents.kind10002,
tags: [
@@ -254,7 +254,7 @@ describe('', () => {
expect.anything(),
);
});
- test('2 write relays including first defauly relay. function called with custom relays and 1 different default relays', async () => {
+ test('2 relays including first default relay. function called with custom relays and 1 different default relays', async () => {
storeMyProfileEvent({
...SampleEvents.kind10002,
tags: [
@@ -274,13 +274,13 @@ describe('', () => {
expect.anything(),
);
});
- test('with 4 write relays function called with all custom relays', async () => {
+ test('with 4 relays function called with all custom relays', async () => {
storeMyProfileEvent({
...SampleEvents.kind10002,
tags: [
['r', 'wss://alicerelay.example.com'],
['r', 'wss://brando-relay.com'],
- ['r', 'wss://expensive-relay.example2.com', 'write'],
+ ['r', 'wss://expensive-relay.example2.com', 'read'],
['r', 'wss://alicerelay.example3.com'],
],
});
@@ -297,28 +297,6 @@ describe('', () => {
expect.anything(),
);
});
- test('custom read relays ignored', async () => {
- storeMyProfileEvent({
- ...SampleEvents.kind10002,
- tags: [
- ['r', 'wss://alicerelay.example.com'],
- ['r', 'wss://brando-relay.com'],
- ['r', 'wss://expensive-relay.example2.com', 'write'],
- ['r', 'wss://nostr-relay.example.com', 'read'],
- ],
- });
- await doBefore();
- expect(mockrequestEventsFromRelays).toBeCalledWith(
- expect.anything(),
- expect.anything(),
- [
- 'wss://alicerelay.example.com',
- 'wss://brando-relay.com',
- 'wss://expensive-relay.example2.com',
- ],
- expect.anything(),
- );
- });
});
describe('and when no cached 10002 events are present and none are return', () => {
const mockstoreMyProfileEvent = jest.spyOn(FetchEvents, 'storeMyProfileEvent');
diff --git a/src/fetchEvents.ts b/src/fetchEvents.ts
index e26e1a1..9847f7c 100644
--- a/src/fetchEvents.ts
+++ b/src/fetchEvents.ts
@@ -71,7 +71,7 @@ export const storeMyProfileEvent = (event:Event): boolean => {
return true;
};
-export const fetchCachedProfileEventHistory = (
+export const fetchCachedMyProfileEventHistory = (
kind: 0 | 2 | 10002 | 3,
): null | [Event, ...Event[]] => {
// get data from local storage
@@ -85,19 +85,19 @@ export const fetchCachedProfileEventHistory = (
return a.sort((x, y) => y.created_at - x.created_at);
};
-export const fetchCachedProfileEvent = (kind: 0 | 2 | 10002 | 3): null | Event => {
- const a = fetchCachedProfileEventHistory(kind);
+export const fetchCachedMyProfileEvent = (kind: 0 | 2 | 10002 | 3): null | Event => {
+ const a = fetchCachedMyProfileEventHistory(kind);
if (a === null) return null;
// return Event in array with most recent created_at date
return a[0];
};
const getRelays = () => {
- const e = fetchCachedProfileEvent(10002);
- const mywriterelays = !e ? [] : e.tags.filter((r) => !r[2] || r[2] === 'write').map((r) => r[1]);
+ const e = fetchCachedMyProfileEvent(10002);
+ const myrelays = !e ? [] : e.tags.map((r) => r[1]);
// return minimum of 3 relays, filling in with default relays (removing duplicates)
- return mywriterelays.length > 3 ? mywriterelays : [...new Set([
- ...mywriterelays,
+ return myrelays.length > 3 ? myrelays : [...new Set([
+ ...myrelays,
'wss://relay.damus.io',
'wss://nostr-pub.wellorder.net',
'wss://nostr-relay.wlvs.space',
@@ -118,7 +118,7 @@ export const fetchMyProfileEvents = async (
}, starterrelays, [0, 2, 10002, 3]);
// if new 10002 event found with more write relays
if (
- fetchCachedProfileEvent(10002)?.tags
+ fetchCachedMyProfileEvent(10002)?.tags
.some((t) => starterrelays.indexOf(t[1]) === -1 && (!t[2] || t[2] === 'write'))
) {
// fetch events again to ensure we got all my profile events
@@ -129,12 +129,169 @@ export const fetchMyProfileEvents = async (
} else {
// for kinds 0, 2, 10002 and 3
[0, 2, 10002, 3].forEach((k) => {
- const e = fetchCachedProfileEvent(k as 0 | 2 | 10002 | 3);
+ const e = fetchCachedMyProfileEvent(k as 0 | 2 | 10002 | 3);
if (e !== null) profileEventProcesser(e);
});
}
};
+const UserProfileEvents:{
+ [pubkey: string]: {
+ [kind: number]: Event;
+ };
+} = {};
+
+const storeProfileEvent = (event:Event) => {
+ if (!UserProfileEvents[event.pubkey]) UserProfileEvents[event.pubkey] = {};
+ if (
+ // no event of kind for pubkey
+ !UserProfileEvents[event.pubkey][event.kind]
+ // newer event of kind recieved
+ || UserProfileEvents[event.pubkey][event.kind].created_at < event.created_at
+ ) {
+ // store it
+ UserProfileEvents[event.pubkey][event.kind] = event;
+ }
+};
+
+export const fetchMyContactsProfileEvents = async () => {
+ const c = fetchCachedMyProfileEvent(3);
+ if (!c || c.tags.length === 0) return;
+ const required = c.tags.filter((p) => !UserProfileEvents[p[1]]);
+ if (required.length > 0) {
+ await requestEventsFromRelays(
+ required.map((t) => t[1]),
+ storeProfileEvent,
+ getRelays(),
+ [0, 10002, 3],
+ );
+ // TODO: check 10002 events and ensure we have read for one of their write relays
+ }
+};
+
+export const fetchCachedProfileEvent = (pubkey:string, kind:0 | 10002 | 3):Event | null => {
+ if (localStorageGetItem('pubkey') === pubkey) return fetchCachedMyProfileEvent(kind);
+ if (!UserProfileEvents[pubkey]) return null;
+ if (!UserProfileEvents[pubkey][kind]) return null;
+ return UserProfileEvents[pubkey][kind];
+};
+
+export const fetchAllCachedProfileEvents = (
+ kind: 0 | 10002 | 3,
+):Event[] => Object.keys(UserProfileEvents)
+ .filter((p) => !!UserProfileEvents[p][kind])
+ .map((p) => UserProfileEvents[p][kind]);
+
+export const fetchProfileEvents = async (
+ pubkeys:[string, ...string[]],
+ kind:0 | 10002 | 3,
+ relays?: string[] | null,
+):Promise<[(Event | null), ...(Event | null)[]]> => {
+ const notcached = pubkeys.filter((p) => !fetchCachedProfileEvent(p, kind));
+ if (notcached.length > 0) {
+ await requestEventsFromRelays(
+ notcached,
+ storeProfileEvent,
+ relays || getRelays(),
+ [0, 10002, 3],
+ );
+ }
+ return pubkeys.map(
+ (p) => fetchCachedProfileEvent(p, kind),
+ ) as [(Event | null), ...(Event | null)[]];
+};
+
+export const fetchProfileEvent = async (
+ pubkey:string,
+ kind:0 | 10002 | 3,
+ relays?: string[] | null,
+):Promise
=> {
+ const r = await fetchProfileEvents([pubkey], kind, relays);
+ return r[0];
+};
+
+export const getContactMostPopularPetname = (pubkey: string):string | null => {
+ // considered implementing frank.david.erin model in nip-02 but I think the UX is to confusing
+ // get count of petnames for users by other contacts
+ const petnamecounts: { [petname: string]: number } = Object.keys(UserProfileEvents)
+ // returns petname or null
+ .map((pk) => {
+ if (!UserProfileEvents[pk][3]) return null;
+ const petnametag = UserProfileEvents[pk][3].tags.find((t) => t[1] === pubkey && t[3]);
+ if (petnametag) return petnametag[3];
+ return null;
+ })
+ // returns petname counts
+ .reduce((pv, c) => {
+ if (!c) return pv;
+ if (!pv[c]) return { ...pv, [c]: 1 };
+ return { ...pv, [c]: pv[c] + 1 };
+ }, {} as { [petname: string]: number });
+ if (petnamecounts.length === 0) return null;
+ // returns most frequent petname for user amoung contacts (appended with ' (?)')
+ return Object.keys(petnamecounts).sort((a, b) => petnamecounts[b] - petnamecounts[a])[0];
+};
+
+export const getMyPetnameForUser = (pubkey: string): string | null => {
+ const e = fetchCachedMyProfileEvent(3);
+ if (e) {
+ const mypetname = e.tags.find((t) => t[1] === pubkey && t[3]);
+ if (mypetname) return mypetname[3];
+ }
+ return null;
+};
+
+export const getMyRelayForUser = (pubkey: string): string | null => {
+ const e = fetchCachedMyProfileEvent(3);
+ if (e) {
+ const relay = e.tags.find((t) => t[1] === pubkey && t[2] && t[2] !== '');
+ if (relay) return relay[2];
+ }
+ return null;
+};
+
+export const isUserMyContact = (pubkey: string): boolean | null => {
+ const e = fetchCachedMyProfileEvent(3);
+ if (e) {
+ if (e.tags.some((t) => t[1] === pubkey)) return true;
+ return false;
+ }
+ return null;
+};
+
+export const getContactName = (pubkey: string):string => {
+ // my own name
+ if (localStorageGetItem('pubkey') === pubkey) {
+ const m = fetchCachedMyProfileEvent(0);
+ if (m) {
+ const { name } = JSON.parse(m.content);
+ if (name) return name;
+ }
+ } else {
+ // my petname for contact
+ const mypetname = getMyPetnameForUser(pubkey);
+ if (mypetname) return mypetname;
+ // TODO: what about displaying a common petname in brackets if vastly different from their name?
+ // their kind 0 name
+ if (UserProfileEvents[pubkey]) {
+ if (UserProfileEvents[pubkey][0]) {
+ const { name } = JSON.parse(UserProfileEvents[pubkey][0].content);
+ if (name) return name;
+ }
+ }
+ }
+ // most popular petname for user amoung contacts
+ const popularpetname = getContactMostPopularPetname(pubkey);
+ if (popularpetname) return `${popularpetname} (?)`;
+ // return shortened pubkey
+ /**
+ * TODO: add npubEncode
+ * npubEncode is imported from nostr-tools and causes the jest test runner to fail with:
+ * SyntaxError: Cannot use import statement outside a module
+ */
+ return `${pubkey.substring(0, 10)}...`;
+};
+
export const publishEvent = async (event:Event):Promise => {
const r = await publishEventToRelay(
event,
diff --git a/src/index.ts b/src/index.ts
index 820759f..51eef8b 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,6 +1,6 @@
import { Event, UnsignedEvent } from 'nostr-tools';
import { generateLogoHero, LoadProfileHome } from './LoadProfileHome';
-import { fetchMyProfileEvents } from './fetchEvents';
+import { fetchMyContactsProfileEvents, fetchMyProfileEvents } from './fetchEvents';
import { localStorageGetItem, localStorageSetItem } from './LocalStorage';
import { LoadMetadataPage } from './LoadMetadataPage';
import LoadContactsPage from './LoadContactsPage';
@@ -31,6 +31,8 @@ const loadProfile = async () => {
(document.getElementById('navmetadata') as HTMLElement).onclick = LoadMetadataPage;
(document.getElementById('navcontacts') as HTMLElement).onclick = LoadContactsPage;
(document.getElementById('navrelays') as HTMLElement).onclick = LoadRelaysPage;
+ // get events from my contacts
+ await fetchMyContactsProfileEvents();
};
const LoadLandingPage = () => {
diff --git a/src/style.scss b/src/style.scss
index 567d4ee..941096a 100644
--- a/src/style.scss
+++ b/src/style.scss
@@ -71,4 +71,41 @@ img[src=""] {
input[type="text"] {
margin-top: 17px;
}
+}
+
+.hide { display:none;}
+
+#contactdetails {
+ img {
+ display:inline-block;
+ max-width: 20%;
+ padding-right: 16px;
+ vertical-align:top;
+ }
+ .contactdetailsmain {
+ display:inline-block;
+ width:75%;
+ }
+ .contact-form-buttons {
+ margin-top: 34px;
+ }
+}
+
+#searchinput { margin-top: 16px; }
+
+#searchresults ul {
+ padding: 0;
+ margin: 0;
+ li {
+ list-style: none;
+ margin:0;
+ width:30%;
+ display: inline-block;
+ }
+ img {
+ max-width: 75px;
+ border-radius: 38px;
+ float: left;
+ margin: 10px 10px 10px 0;
+ }
}
\ No newline at end of file